226 lines
8.6 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "next/navigation";
import Link from "next/link";
import {
ServerIcon,
CalendarIcon,
DocumentTextIcon,
XCircleIcon,
} from "@heroicons/react/24/outline";
import { useSubscription } from "@/features/subscriptions/hooks";
import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList";
import { Formatting } from "@customer-portal/domain/toolkit";
import { PageLayout } from "@/components/templates/PageLayout";
import { StatusPill } from "@/components/atoms/status-pill";
import {
SubscriptionDetailStatsSkeleton,
InvoiceListSkeleton,
} from "@/components/atoms/loading-skeleton";
import { formatIsoDate } from "@/shared/utils";
const { formatCurrency: sharedFormatCurrency } = Formatting;
import { SimManagementSection } from "@/features/subscriptions/components/sim";
import {
getBillingCycleLabel,
getSubscriptionStatusVariant,
} from "@/features/subscriptions/utils/status-presenters";
import { cn } from "@/shared/utils";
export function SubscriptionDetailContainer() {
const params = useParams();
const searchParams = useSearchParams();
const [activeTab, setActiveTab] = useState<"overview" | "sim">("overview");
const subscriptionId = parseInt(params.id as string);
const { data: subscription, error } = useSubscription(subscriptionId);
// Simple loading check: show skeleton until we have data or an error
const showLoading = !subscription && !error;
useEffect(() => {
const updateTab = () => {
const hash = typeof window !== "undefined" ? window.location.hash : "";
const service = (searchParams.get("service") || "").toLowerCase();
const isSimContext = hash.includes("sim-management") || service === "sim";
setActiveTab(isSimContext ? "sim" : "overview");
};
updateTab();
if (typeof window !== "undefined") {
window.addEventListener("hashchange", updateTab);
return () => window.removeEventListener("hashchange", updateTab);
}
return;
}, [searchParams]);
const formatDate = (dateString: string | undefined) => formatIsoDate(dateString);
const formatCurrency = (amount: number) => sharedFormatCurrency(amount || 0);
// Show error message (only when we have an error, not during loading)
const pageError = error
? process.env.NODE_ENV === "development" && error instanceof Error
? error.message
: "Unable to load subscription details. Please try again."
: null;
const productNameLower = subscription?.productName?.toLowerCase() ?? "";
const isSimService = productNameLower.includes("sim");
// Match: "Internet", "SonixNet via NTT Optical Fiber", or any NTT-based fiber service
const isInternetService =
productNameLower.includes("internet") ||
productNameLower.includes("sonixnet") ||
(productNameLower.includes("ntt") && productNameLower.includes("fiber"));
const canCancel = subscription?.status === "Active";
// Header action: cancel button (for Internet services)
const headerActions =
isInternetService && canCancel ? (
<Link
href={`/account/subscriptions/${subscriptionId}/cancel`}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-danger-foreground bg-danger hover:bg-danger/90 rounded-lg transition-colors"
>
<XCircleIcon className="h-4 w-4" />
Cancel Service
</Link>
) : undefined;
// Render skeleton matching loading.tsx structure when loading
if (showLoading) {
return (
<PageLayout
icon={<ServerIcon className="h-6 w-6" />}
title="Subscription"
description="Loading subscription details..."
breadcrumbs={[
{ label: "Subscriptions", href: "/account/subscriptions" },
{ label: "Subscription" },
]}
>
<div className="space-y-6">
<SubscriptionDetailStatsSkeleton />
<InvoiceListSkeleton rows={5} />
</div>
</PageLayout>
);
}
return (
<PageLayout
icon={<ServerIcon className="h-6 w-6" />}
title={subscription?.productName ?? "Subscription"}
actions={headerActions}
breadcrumbs={[
{ label: "Subscriptions", href: "/account/subscriptions" },
{ label: subscription?.productName ?? "Subscription" },
]}
error={pageError}
>
{subscription ? (
<div className="space-y-6">
{/* Subscription Stats */}
<div className="bg-card border border-border rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-5 grid grid-cols-2 md:grid-cols-4 gap-6">
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Service Status
</h4>
<div className="mt-2">
<StatusPill
label={subscription.status}
variant={getSubscriptionStatusVariant(subscription.status)}
size="lg"
/>
</div>
</div>
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Billing Amount
</h4>
<div className="mt-2 flex items-baseline gap-2">
<p className="text-2xl font-bold text-foreground">
{formatCurrency(subscription.amount)}
</p>
<span className="text-sm text-muted-foreground">
{getBillingCycleLabel(subscription.cycle)}
</span>
</div>
</div>
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Next Due Date
</h4>
<div className="flex items-center mt-2 gap-2">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<p className="text-lg font-medium text-foreground">
{formatDate(subscription.nextDue)}
</p>
</div>
</div>
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Registration Date
</h4>
<div className="flex items-center mt-2 gap-2">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<p className="text-lg font-medium text-foreground">
{formatDate(subscription.registrationDate)}
</p>
</div>
</div>
</div>
</div>
{/* Tab Navigation for SIM Services */}
{isSimService && (
<div className="flex items-center gap-1 p-1 bg-muted rounded-lg w-fit">
<Link
href={`/account/subscriptions/${subscriptionId}`}
className={cn(
"px-4 py-2 text-sm font-medium rounded-md transition-all flex items-center gap-2",
activeTab === "overview"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
<DocumentTextIcon className="h-4 w-4" />
Overview & Billing
</Link>
<Link
href={`/account/subscriptions/${subscriptionId}#sim-management`}
className={cn(
"px-4 py-2 text-sm font-medium rounded-md transition-all flex items-center gap-2",
activeTab === "sim"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
<ServerIcon className="h-4 w-4" />
SIM Management
</Link>
</div>
)}
{/* SIM Management Section */}
{activeTab === "sim" && isSimService && (
<SimManagementSection subscriptionId={subscriptionId} />
)}
{/* Billing History Section */}
{activeTab === "overview" && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground">Billing History</h3>
</div>
<InvoicesList subscriptionId={subscriptionId} pageSize={5} showFilters={false} />
</div>
)}
</div>
) : null}
</PageLayout>
);
}
export default SubscriptionDetailContainer;