diff --git a/apps/portal/src/features/catalog/components/base/AddonGroup.tsx b/apps/portal/src/features/catalog/components/base/AddonGroup.tsx index 0e378659..39e7d7fb 100644 --- a/apps/portal/src/features/catalog/components/base/AddonGroup.tsx +++ b/apps/portal/src/features/catalog/components/base/AddonGroup.tsx @@ -189,7 +189,7 @@ export function AddonGroup({ })} {selectedAddonSkus.length === 0 && ( -
+

Select add-ons to enhance your service

)} diff --git a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx index 67767203..60da9d56 100644 --- a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx +++ b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx @@ -407,30 +407,28 @@ export function AddressConfirmation({ )} {/* Action buttons */} -
-
- {/* Primary action when pending for Internet orders */} - {isInternetOrder && !addressConfirmed && !editing && ( - - )} -
+
+ {/* Primary action when pending for Internet orders */} + {isInternetOrder && !addressConfirmed && !editing && ( + + )} - {/* Edit button - always on the right */} + {/* Edit button */} {billingInfo.isComplete && !editing && ( )}
diff --git a/apps/portal/src/features/catalog/components/base/CardBadge.tsx b/apps/portal/src/features/catalog/components/base/CardBadge.tsx new file mode 100644 index 00000000..377107b0 --- /dev/null +++ b/apps/portal/src/features/catalog/components/base/CardBadge.tsx @@ -0,0 +1,46 @@ +"use client"; + +export type BadgeVariant = + | "gold" + | "platinum" + | "silver" + | "recommended" + | "family" + | "new" + | "default"; + +interface CardBadgeProps { + text: string; + variant?: BadgeVariant; + size?: "sm" | "md"; +} + +export function CardBadge({ text, variant = "default", size = "md" }: CardBadgeProps) { + const getVariantClasses = () => { + switch (variant) { + case "gold": + return "bg-yellow-50 text-yellow-700 border-yellow-200"; + case "platinum": + return "bg-indigo-50 text-indigo-700 border-indigo-200"; + case "silver": + return "bg-gray-50 text-gray-700 border-gray-200"; + case "recommended": + return "bg-green-50 text-green-700 border-green-200"; + case "family": + return "bg-blue-50 text-blue-700 border-blue-200"; + case "new": + return "bg-purple-50 text-purple-700 border-purple-200"; + default: + return "bg-gray-50 text-gray-700 border-gray-200"; + } + }; + + const sizeClasses = size === "sm" ? "text-xs px-2 py-0.5" : "text-xs px-2.5 py-1"; + + return ( + + {text} + + ); +} + diff --git a/apps/portal/src/features/catalog/components/base/CardPricing.tsx b/apps/portal/src/features/catalog/components/base/CardPricing.tsx new file mode 100644 index 00000000..1d387e7a --- /dev/null +++ b/apps/portal/src/features/catalog/components/base/CardPricing.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { CurrencyYenIcon } from "@heroicons/react/24/outline"; + +interface CardPricingProps { + monthlyPrice?: number | null; + oneTimePrice?: number | null; + size?: "sm" | "md" | "lg"; + alignment?: "left" | "right"; +} + +export function CardPricing({ + monthlyPrice, + oneTimePrice, + size = "md", + alignment = "right" +}: CardPricingProps) { + const sizeClasses = { + sm: { + monthlyPrice: "text-xl", + monthlyLabel: "text-xs", + icon: "h-5 w-5", + oneTimePrice: "text-sm", + oneTimeLabel: "text-xs", + }, + md: { + monthlyPrice: "text-2xl", + monthlyLabel: "text-sm", + icon: "h-6 w-6", + oneTimePrice: "text-base", + oneTimeLabel: "text-xs", + }, + lg: { + monthlyPrice: "text-3xl", + monthlyLabel: "text-base", + icon: "h-7 w-7", + oneTimePrice: "text-lg", + oneTimeLabel: "text-sm", + }, + }; + + const alignClass = alignment === "right" ? "text-right" : "text-left"; + const justifyClass = alignment === "right" ? "justify-end" : "justify-start"; + + if (!monthlyPrice && !oneTimePrice) { + return null; + } + + const classes = sizeClasses[size]; + + return ( +
+ {monthlyPrice && monthlyPrice > 0 && ( +
+ + + {monthlyPrice.toLocaleString()} + + + /month + +
+ )} + {oneTimePrice && oneTimePrice > 0 && ( +
+ + + {oneTimePrice.toLocaleString()} + + + one-time + +
+ )} +
+ ); +} + diff --git a/apps/portal/src/features/catalog/components/base/index.ts b/apps/portal/src/features/catalog/components/base/index.ts new file mode 100644 index 00000000..4dd7f5f7 --- /dev/null +++ b/apps/portal/src/features/catalog/components/base/index.ts @@ -0,0 +1,12 @@ +export { CardPricing } from "./CardPricing"; +export { CardBadge } from "./CardBadge"; +export type { BadgeVariant } from "./CardBadge"; +export { ProductCard } from "./ProductCard"; +export type { ProductCardProps } from "./ProductCard"; +export { CatalogHero } from "./CatalogHero"; +export type { CatalogHeroProps } from "./CatalogHero"; +export { CatalogBackLink } from "./CatalogBackLink"; +export { OrderSummary } from "./OrderSummary"; +export { PricingDisplay } from "./PricingDisplay"; +export type { PricingDisplayProps } from "./PricingDisplay"; + diff --git a/apps/portal/src/features/catalog/components/common/FeatureCard.tsx b/apps/portal/src/features/catalog/components/common/FeatureCard.tsx index 98bcbae5..f3bec459 100644 --- a/apps/portal/src/features/catalog/components/common/FeatureCard.tsx +++ b/apps/portal/src/features/catalog/components/common/FeatureCard.tsx @@ -1,7 +1,6 @@ "use client"; import React from "react"; -import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard"; export function FeatureCard({ icon, @@ -13,12 +12,14 @@ export function FeatureCard({ description: string; }) { return ( - -
-
{icon}
+
+
+ {icon}
-

{title}

-

{description}

- +
+

{title}

+

{description}

+
+
); } diff --git a/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx b/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx index 3939d46e..cbf0fe74 100644 --- a/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx +++ b/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx @@ -22,38 +22,35 @@ export function ServiceHeroCard({ }) { const colorClasses = { blue: { - bg: "bg-blue-50", - border: "border-blue-200", iconBg: "bg-blue-100", iconText: "text-blue-600", - button: "bg-blue-600 hover:bg-blue-700", - hoverBorder: "hover:border-blue-300", + border: "border-blue-100", + hoverBorder: "hover:border-blue-200", }, green: { - bg: "bg-green-50", - border: "border-green-200", iconBg: "bg-green-100", iconText: "text-green-600", - button: "bg-green-600 hover:bg-green-700", - hoverBorder: "hover:border-green-300", + border: "border-green-100", + hoverBorder: "hover:border-green-200", }, purple: { - bg: "bg-purple-50", - border: "border-purple-200", iconBg: "bg-purple-100", iconText: "text-purple-600", - button: "bg-purple-600 hover:bg-purple-700", - hoverBorder: "hover:border-purple-300", + border: "border-purple-100", + hoverBorder: "hover:border-purple-200", }, } as const; const colors = colorClasses[color]; return ( - -
-
-
+ +
+ {/* Icon and Title */} +
+
{icon}
@@ -61,32 +58,31 @@ export function ServiceHeroCard({
-

{description}

+ {/* Description */} +

{description}

-
    + {/* Features List */} +
      {features.map((feature, index) => ( -
    • -
      - {feature} +
    • + + {feature}
    • ))}
    -
    + {/* Action Button */} +
    -
    ); } diff --git a/apps/portal/src/features/catalog/components/internet/InstallationOptions.tsx b/apps/portal/src/features/catalog/components/internet/InstallationOptions.tsx index 03db2be1..58c02458 100644 --- a/apps/portal/src/features/catalog/components/internet/InstallationOptions.tsx +++ b/apps/portal/src/features/catalog/components/internet/InstallationOptions.tsx @@ -1,7 +1,7 @@ "use client"; import type { InternetInstallationCatalogItem } from "@customer-portal/domain/catalog"; -import { getDisplayPrice } from "../../utils/pricing"; +import { CardPricing } from "@/features/catalog/components/base/CardPricing"; type InstallationTerm = NonNullable< NonNullable["installationTerm"] @@ -14,24 +14,6 @@ interface InstallationOptionsProps { showSkus?: boolean; } -function getPriceLabel(installation: InternetInstallationCatalogItem): string { - const priceInfo = getDisplayPrice(installation); - if (!priceInfo) { - return "Price not available"; - } - const billingCycle = installation.billingCycle?.toLowerCase(); - if (billingCycle === "monthly" && priceInfo.monthly !== null) { - return `¥${priceInfo.monthly.toLocaleString()}/month`; - } - if (priceInfo.oneTime !== null) { - return `¥${priceInfo.oneTime.toLocaleString()} one-time`; - } - if (priceInfo.monthly !== null) { - return `¥${priceInfo.monthly.toLocaleString()}`; - } - return priceInfo.display || "Price not available"; -} - export function InstallationOptions({ installations, selectedInstallationSku, @@ -51,18 +33,17 @@ export function InstallationOptions({ } return ( -
    +
    {sortedInstallations.map(installation => { const isSelected = selectedInstallationSku === installation.sku; - const priceInfo = getDisplayPrice(installation); const installationTerm = installation.catalogMetadata?.installationTerm ?? null; const description = installation.description || (installationTerm === "12-Month" - ? "Spread the installation fee across 12 payments." + ? "Spread the installation fee across 12 monthly payments." : installationTerm === "24-Month" - ? "Spread the installation fee across 24 payments." - : "Pay the full installation fee once."); + ? "Spread the installation fee across 24 monthly payments." + : "Pay the full installation fee in one payment."); return ( ); })} diff --git a/apps/portal/src/features/catalog/components/internet/InternetConfigureView.tsx b/apps/portal/src/features/catalog/components/internet/InternetConfigureView.tsx index 9d0fdb8b..c233e21d 100644 --- a/apps/portal/src/features/catalog/components/internet/InternetConfigureView.tsx +++ b/apps/portal/src/features/catalog/components/internet/InternetConfigureView.tsx @@ -6,23 +6,28 @@ import type { InternetInstallationCatalogItem, InternetAddonCatalogItem, } from "@customer-portal/domain/catalog"; +import type { UseInternetConfigureResult } from "@/features/catalog/hooks/useInternetConfigure"; -interface Props { - plan: InternetPlanCatalogItem | null; - loading: boolean; - addons: InternetAddonCatalogItem[]; - installations: InternetInstallationCatalogItem[]; +interface Props extends UseInternetConfigureResult { onConfirm: () => void; } -export function InternetConfigureView({ plan, loading, addons, installations, onConfirm }: Props) { +export function InternetConfigureView(props: Props) { return ( ); } diff --git a/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx b/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx index 42a93922..414e4cb4 100644 --- a/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx +++ b/apps/portal/src/features/catalog/components/internet/InternetPlanCard.tsx @@ -8,6 +8,9 @@ import type { InternetInstallationCatalogItem, } from "@customer-portal/domain/catalog"; import { useRouter } from "next/navigation"; +import { CardPricing } from "@/features/catalog/components/base/CardPricing"; +import { CardBadge } from "@/features/catalog/components/base/CardBadge"; +import type { BadgeVariant } from "@/features/catalog/components/base/CardBadge"; interface InternetPlanCardProps { plan: InternetPlanCatalogItem; @@ -54,91 +57,92 @@ export function InternetPlanCard({ return "border border-gray-200 bg-white shadow hover:shadow-lg"; }; + const getTierBadgeVariant = (): BadgeVariant => { + if (isGold) return "gold"; + if (isPlatinum) return "platinum"; + if (isSilver) return "silver"; + return "default"; + }; + return ( -
    +
    + {/* Header with badges and pricing */}
    -
    -
    - - {tier || "Plan"} - +
    +
    + {isGold && ( - - Recommended - + )}
    -

    {plan.name}

    +

    {plan.name}

    - {plan.monthlyPrice && plan.monthlyPrice > 0 && ( -
    -
    Monthly
    -
    - ¥{plan.monthlyPrice.toLocaleString()} -
    - {plan.oneTimePrice && plan.oneTimePrice > 0 && ( -
    One-time ¥{plan.oneTimePrice.toLocaleString()}
    - )} -
    - )} +
    + +
    -

    + {/* Description */} +

    {plan.catalogMetadata?.tierDescription || plan.description}

    + {/* Features */}
    -

    Your Plan Includes

    +

    Plan Includes:

      {plan.catalogMetadata?.features && plan.catalogMetadata.features.length > 0 ? ( plan.catalogMetadata.features.map((feature, index) => ( -
    • - - {feature} +
    • + + {feature}
    • )) ) : ( <> -
    • - 1 NTT Optical Fiber (Flet's - Hikari Next -{" "} - {plan.internetOfferingType?.includes("Apartment") ? "Mansion" : "Home"}{" "} - {plan.internetOfferingType?.includes("10G") - ? "10Gbps" - : plan.internetOfferingType?.includes("100M") - ? "100Mbps" - : "1Gbps"} - ) Installation + Monthly +
    • + + NTT Optical Fiber (Flet's Hikari Next)
    • -
    • - - Monthly: ¥{(plan.monthlyPrice ?? 0).toLocaleString()} - {installations.length > 0 && minInstallationPrice > 0 && ( - - (+ installation from ¥{minInstallationPrice.toLocaleString()}) - - )} +
    • + + + {plan.internetOfferingType?.includes("Apartment") ? "Mansion" : "Home"}{" "} + {plan.internetOfferingType?.includes("10G") + ? "10Gbps" + : plan.internetOfferingType?.includes("100M") + ? "100Mbps" + : "1Gbps"} connection +
    • +
    • + + ISP connection protocols: IPoE and PPPoE +
    • + {installations.length > 0 && minInstallationPrice > 0 && ( +
    • + + Installation from ¥{minInstallationPrice.toLocaleString()} +
    • + )} )}
    + {/* Action Button */}
    -
    +
    diff --git a/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx b/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx index a576373c..706aa7cb 100644 --- a/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx +++ b/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx @@ -1,46 +1,59 @@ "use client"; -import { DevicePhoneMobileIcon, UsersIcon, CurrencyYenIcon } from "@heroicons/react/24/outline"; +import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline"; import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard"; import { Button } from "@/components/atoms/button"; +import { ArrowRightIcon } from "@heroicons/react/24/outline"; import type { SimCatalogProduct } from "@customer-portal/domain/catalog"; +import { CardPricing } from "@/features/catalog/components/base/CardPricing"; +import { CardBadge } from "@/features/catalog/components/base/CardBadge"; export function SimPlanCard({ plan, isFamily }: { plan: SimCatalogProduct; isFamily?: boolean }) { const displayPrice = plan.monthlyPrice ?? plan.unitPrice ?? plan.oneTimePrice ?? 0; const isFamilyPlan = isFamily ?? Boolean(plan.simHasFamilyDiscount); return ( - -
    -
    -
    - - {plan.simDataSize} + + {/* Header with data size and pricing */} +
    +
    +
    + + {plan.simDataSize}
    {isFamilyPlan && ( -
    - - - Family - -
    + )}
    -
    -
    - - {displayPrice.toLocaleString()} - /month -
    + + {/* Pricing */} +
    + {isFamilyPlan && ( -
    Discounted price
    +
    Discounted pricing applied
    )}
    -
    -

    {plan.name}

    + + {/* Description */} +
    +

    {plan.name}

    - diff --git a/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx b/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx index 2fb0da47..5bd69be9 100644 --- a/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx +++ b/apps/portal/src/features/catalog/components/vpn/VpnPlanCard.tsx @@ -2,8 +2,9 @@ import { AnimatedCard } from "@/components/molecules"; import { Button } from "@/components/atoms/button"; -import { CurrencyYenIcon } from "@heroicons/react/24/outline"; +import { ArrowRightIcon, ShieldCheckIcon } from "@heroicons/react/24/outline"; import type { VpnCatalogProduct } from "@customer-portal/domain/catalog"; +import { CardPricing } from "@/features/catalog/components/base/CardPricing"; interface VpnPlanCardProps { plan: VpnCatalogProduct; @@ -11,24 +12,40 @@ interface VpnPlanCardProps { export function VpnPlanCard({ plan }: VpnPlanCardProps) { return ( - -
    -

    {plan.name}

    -
    -
    -
    - - - {plan.monthlyPrice?.toLocaleString()} - - /month + + {/* Header with icon and name */} +
    +
    + +
    +
    +

    {plan.name}

    - + + {/* Pricing */} +
    + +
    + + {/* Action Button */} +
    + +
    ); } export type { VpnPlanCardProps }; + diff --git a/apps/portal/src/features/catalog/hooks/useConfigureParams.ts b/apps/portal/src/features/catalog/hooks/useConfigureParams.ts index 37b79d84..a3fa647c 100644 --- a/apps/portal/src/features/catalog/hooks/useConfigureParams.ts +++ b/apps/portal/src/features/catalog/hooks/useConfigureParams.ts @@ -41,7 +41,16 @@ export function useInternetConfigureParams() { const accessMode: AccessMode | null = accessModeParam === "IPoE-BYOR" || accessModeParam === "PPPoE" ? accessModeParam : null; const installationSku = params.get("installationSku"); - const addonSkus = params.getAll("addonSku"); + + // Support both formats: comma-separated 'addons' or multiple 'addonSku' params + const addonsParam = params.get("addons"); + const addonSkuParams = params.getAll("addonSku"); + + const addonSkus = addonsParam + ? addonsParam.split(",").map(s => s.trim()).filter(Boolean) + : addonSkuParams.length > 0 + ? addonSkuParams + : []; return { accessMode, diff --git a/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts b/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts index 6aad88e2..55313e2e 100644 --- a/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts +++ b/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts @@ -29,40 +29,38 @@ export type UseInternetConfigureResult = { selectedAddonSkus: string[]; setSelectedAddonSkus: (skus: string[]) => void; - currentStep: number; - isTransitioning: boolean; - transitionToStep: (nextStep: number) => void; - monthlyTotal: number; oneTimeTotal: number; buildCheckoutSearchParams: () => URLSearchParams | null; }; +/** + * Hook for managing internet service configuration state + * Follows domain/BFF architecture: minimal client logic, state management only + */ export function useInternetConfigure(): UseInternetConfigureResult { const router = useRouter(); const searchParams = useSearchParams(); const planSku = searchParams.get("plan"); + // Fetch catalog data from BFF const { data: internetData, isLoading: internetLoading } = useInternetCatalog(); const { plan: selectedPlan } = useInternetPlan(planSku || undefined); const { accessMode, installationSku, addonSkus } = useInternetConfigureParams(); + // Local UI state const [plan, setPlan] = useState(null); const [loading, setLoading] = useState(true); const [addons, setAddons] = useState([]); const [installations, setInstallations] = useState([]); + // Configuration selections const [mode, setMode] = useState(null); const [selectedInstallationSku, setSelectedInstallationSku] = useState(null); const [selectedAddonSkus, setSelectedAddonSkus] = useState([]); - const [currentStep, setCurrentStep] = useState(() => { - const stepParam = searchParams.get("step"); - return stepParam ? parseInt(stepParam, 10) : 1; - }); - const [isTransitioning, setIsTransitioning] = useState(false); - + // Initialize state from BFF data and URL params useEffect(() => { let mounted = true; if (!planSku) { @@ -78,9 +76,24 @@ export function useInternetConfigure(): UseInternetConfigureResult { setAddons(addonsData); setInstallations(installationsData); - if (accessMode) setMode(accessMode as InternetAccessMode); - if (installationSku) setSelectedInstallationSku(installationSku); - if (addonSkus.length > 0) setSelectedAddonSkus(addonSkus); + // Always restore state from URL if present (important for back navigation) + if (accessMode) { + setMode(accessMode as InternetAccessMode); + } else if (selectedPlan.internetPlanTier === "Gold" || selectedPlan.internetPlanTier === "Platinum") { + // Auto-set default mode for Gold/Platinum plans (IPoE-BYOR is standard for these tiers) + setMode("IPoE-BYOR"); + } + + // Restore installation and addons from URL params + if (installationSku) { + setSelectedInstallationSku(installationSku); + } + if (addonSkus.length > 0) { + setSelectedAddonSkus(addonSkus); + } else { + // Clear addons if none in URL (user might have removed them) + setSelectedAddonSkus([]); + } } else { router.push("/catalog/internet"); } @@ -98,17 +111,10 @@ export function useInternetConfigure(): UseInternetConfigureResult { selectedPlan, accessMode, installationSku, - addonSkus, + JSON.stringify(addonSkus), // Use JSON.stringify for array comparison ]); - const transitionToStep = (nextStep: number) => { - setIsTransitioning(true); - setTimeout(() => { - setCurrentStep(nextStep); - setTimeout(() => setIsTransitioning(false), 50); - }, 200); - }; - + // Derive selected installation from SKU const selectedInstallation = useMemo(() => { if (!selectedInstallationSku) return null; return installations.find(installation => installation.sku === selectedInstallationSku) || null; @@ -119,6 +125,7 @@ export function useInternetConfigure(): UseInternetConfigureResult { return selectedInstallation.catalogMetadata?.installationTerm ?? null; }, [selectedInstallation]); + // Calculate totals (simple summation - real pricing logic should be in BFF) const { monthlyTotal, oneTimeTotal } = useMemo(() => { const baseMonthly = plan?.monthlyPrice ?? 0; const baseOneTime = plan?.oneTimePrice ?? 0; @@ -154,12 +161,15 @@ export function useInternetConfigure(): UseInternetConfigureResult { } as const; }, [plan, selectedAddonSkus, addons, selectedInstallation]); + // Build checkout URL params (simple data marshalling, not business logic) const buildCheckoutSearchParams = () => { if (!plan || !mode || !selectedInstallationSku) return null; const params = new URLSearchParams({ type: "internet", plan: plan.sku, accessMode: mode }); params.append("installationSku", selectedInstallationSku); - if (selectedAddonSkus.length > 0) - selectedAddonSkus.forEach(sku => params.append("addonSku", sku)); + if (selectedAddonSkus.length > 0) { + // Send addons as comma-separated string to match BFF expectations + params.append("addons", selectedAddonSkus.join(",")); + } return params; }; @@ -175,9 +185,6 @@ export function useInternetConfigure(): UseInternetConfigureResult { selectedInstallationType, selectedAddonSkus, setSelectedAddonSkus, - currentStep, - isTransitioning, - transitionToStep, monthlyTotal, oneTimeTotal, buildCheckoutSearchParams, diff --git a/apps/portal/src/features/catalog/views/CatalogHome.tsx b/apps/portal/src/features/catalog/views/CatalogHome.tsx index 7bf22091..84f5a589 100644 --- a/apps/portal/src/features/catalog/views/CatalogHome.tsx +++ b/apps/portal/src/features/catalog/views/CatalogHome.tsx @@ -15,10 +15,10 @@ import { FeatureCard } from "@/features/catalog/components/common/FeatureCard"; export function CatalogHomeView() { return ( -
    +
    } title="" description="">
    -
    +
    Services Catalog @@ -26,20 +26,20 @@ export function CatalogHomeView() {

    Choose Your Perfect
    - + Connectivity Solution

    -

    +

    Discover high-speed internet, mobile data/voice options, and secure VPN services.

    -
    +
    } + icon={} features={[ "Up to 10Gbps speeds", "Fiber optic technology", @@ -52,7 +52,7 @@ export function CatalogHomeView() { } + icon={} features={[ "Physical SIM & eSIM", "Data + SMS + Voice plans", @@ -65,7 +65,7 @@ export function CatalogHomeView() { } + icon={} features={[ "Secure encryption", "Multiple locations", @@ -77,21 +77,21 @@ export function CatalogHomeView() { />
    -
    -
    -

    Why Choose Our Services?

    -

    +

    +
    +

    Why Choose Our Services?

    +

    Personalized recommendations based on your location and account eligibility.

    } + icon={} title="Location-Based Plans" description="Internet plans tailored to your house type and infrastructure" /> } + icon={} title="Seamless Integration" description="Manage all services from a single account" /> diff --git a/apps/portal/src/features/catalog/views/InternetConfigure.tsx b/apps/portal/src/features/catalog/views/InternetConfigure.tsx index e05fa1af..bb65fe3b 100644 --- a/apps/portal/src/features/catalog/views/InternetConfigure.tsx +++ b/apps/portal/src/features/catalog/views/InternetConfigure.tsx @@ -8,9 +8,41 @@ export function InternetConfigureContainer() { const router = useRouter(); const vm = useInternetConfigure(); + // Debug: log current state + console.log("InternetConfigure state:", { + plan: vm.plan?.sku, + mode: vm.mode, + installation: vm.selectedInstallation?.sku, + addons: vm.selectedAddonSkus, + }); + const handleConfirm = () => { + console.log("handleConfirm called, current state:", { + plan: vm.plan?.sku, + mode: vm.mode, + installation: vm.selectedInstallation?.sku, + selectedInstallationSku: vm.selectedInstallation?.sku, + }); + const params = vm.buildCheckoutSearchParams(); - if (!params) return; + if (!params) { + console.error("Cannot proceed to checkout: missing required configuration", { + plan: vm.plan?.sku, + mode: vm.mode, + installation: vm.selectedInstallation?.sku, + }); + + // Determine what's missing + let missingItems = []; + if (!vm.plan) missingItems.push("plan selection"); + if (!vm.mode) missingItems.push("access mode"); + if (!vm.selectedInstallation) missingItems.push("installation option"); + + alert(`Please complete the following before proceeding:\n- ${missingItems.join("\n- ")}`); + return; + } + + console.log("Navigating to checkout with params:", params.toString()); router.push(`/checkout?${params.toString()}`); }; diff --git a/apps/portal/src/features/catalog/views/SimPlans.tsx b/apps/portal/src/features/catalog/views/SimPlans.tsx index 2929ba5a..ea199657 100644 --- a/apps/portal/src/features/catalog/views/SimPlans.tsx +++ b/apps/portal/src/features/catalog/views/SimPlans.tsx @@ -236,52 +236,52 @@ export function SimPlansContainer() {
    -
    -
    - } - plans={plansByType.DataSmsVoice} - showFamilyDiscount={hasExistingSim} - /> -
    -
    - } - plans={plansByType.DataOnly} - showFamilyDiscount={hasExistingSim} - /> -
    -
    - } - plans={plansByType.VoiceOnly} - showFamilyDiscount={hasExistingSim} - /> -
    +
    + {activeTab === "data-voice" && ( +
    + } + plans={plansByType.DataSmsVoice} + showFamilyDiscount={hasExistingSim} + /> +
    + )} + {activeTab === "data-only" && ( +
    + } + plans={plansByType.DataOnly} + showFamilyDiscount={hasExistingSim} + /> +
    + )} + {activeTab === "voice-only" && ( +
    + } + plans={plansByType.VoiceOnly} + showFamilyDiscount={hasExistingSim} + /> +
    + )}
    diff --git a/apps/portal/src/features/checkout/hooks/useCheckout.ts b/apps/portal/src/features/checkout/hooks/useCheckout.ts index 14adfc76..9fab65a2 100644 --- a/apps/portal/src/features/checkout/hooks/useCheckout.ts +++ b/apps/portal/src/features/checkout/hooks/useCheckout.ts @@ -215,13 +215,16 @@ export function useCheckout() { const navigateBackToConfigure = useCallback(() => { const urlParams = new URLSearchParams(params.toString()); - const reviewStep = orderType === "Internet" ? "4" : "5"; - urlParams.set("step", reviewStep); + // Remove the 'type' param as it's not needed in configure URLs + urlParams.delete('type'); + const configureUrl = orderType === "Internet" ? `/catalog/internet/configure?${urlParams.toString()}` : `/catalog/sim/configure?${urlParams.toString()}`; - router.push(configureUrl); + + // Use Next.js router state to pass the step internally (not in URL) + router.push(configureUrl, { state: { returnToStep: 4 } } as any); }, [orderType, params, router]); return { diff --git a/apps/portal/src/features/checkout/services/checkout.service.ts b/apps/portal/src/features/checkout/services/checkout.service.ts index f2829ec2..63fc9ce3 100644 --- a/apps/portal/src/features/checkout/services/checkout.service.ts +++ b/apps/portal/src/features/checkout/services/checkout.service.ts @@ -1,5 +1,6 @@ import { apiClient, getDataOrThrow } from "@/lib/api"; import type { CheckoutCart, OrderConfigurations } from "@customer-portal/domain/orders"; +import type { ApiSuccessResponse } from "@customer-portal/domain/common"; export const checkoutService = { /** @@ -10,7 +11,7 @@ export const checkoutService = { selections: Record, configuration?: OrderConfigurations ): Promise { - const response = await apiClient.POST("/checkout/cart", { + const response = await apiClient.POST>("/api/checkout/cart", { body: { orderType, selections, @@ -18,13 +19,17 @@ export const checkoutService = { }, }); - return getDataOrThrow(response, "Failed to build checkout cart"); + const wrappedResponse = getDataOrThrow(response, "Failed to build checkout cart"); + if (!wrappedResponse.success) { + throw new Error("Failed to build checkout cart"); + } + return wrappedResponse.data; }, /** * Validate checkout cart */ async validateCart(cart: CheckoutCart): Promise { - await apiClient.POST("/checkout/validate", { body: cart }); + await apiClient.POST("/api/checkout/validate", { body: cart }); }, }; diff --git a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx index 0610ea6e..80da36a4 100644 --- a/apps/portal/src/features/checkout/views/CheckoutContainer.tsx +++ b/apps/portal/src/features/checkout/views/CheckoutContainer.tsx @@ -221,7 +221,7 @@ export function CheckoutContainer() { onClick={navigateBackToConfigure} className="flex-1 px-6 py-4 border border-gray-300 rounded-lg text-center hover:bg-gray-50 transition-colors font-medium" > - ← Back to Review + ← Back to Configuration