- Deleted loading and page components for invoices and payment methods to streamline the billing section. - Updated AnimatedContainer, InlineToast, and other components to utilize framer-motion for improved animations. - Refactored AppShell and Sidebar components to enhance layout and integrate new animation features. - Adjusted various sections across the portal to ensure consistent animation behavior and visual appeal.
271 lines
8.8 KiB
TypeScript
271 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useParams, useSearchParams } from "next/navigation";
|
|
import Link from "next/link";
|
|
import {
|
|
ServerIcon,
|
|
CalendarIcon,
|
|
DocumentTextIcon,
|
|
XCircleIcon,
|
|
CreditCardIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { useSubscription } from "@/features/subscriptions/hooks";
|
|
import { Formatting } from "@customer-portal/domain/toolkit";
|
|
import { PageLayout } from "@/components/templates/PageLayout";
|
|
import { SectionCard } from "@/components/molecules/SectionCard";
|
|
import { StatusPill } from "@/components/atoms/status-pill";
|
|
import { SubscriptionDetailStatsSkeleton } from "@/components/atoms/loading-skeleton";
|
|
import { formatIsoDate, cn } from "@/shared/utils";
|
|
import { SimManagementSection } from "@/features/subscriptions/components/sim";
|
|
import type { SubscriptionStatus, SubscriptionCycle } from "@customer-portal/domain/subscriptions";
|
|
import {
|
|
getBillingCycleLabel,
|
|
getSubscriptionStatusVariant,
|
|
} from "@/features/subscriptions/utils/status-presenters";
|
|
|
|
const { formatCurrency: sharedFormatCurrency } = Formatting;
|
|
|
|
function SubscriptionStatsCard({
|
|
subscription,
|
|
}: {
|
|
subscription: {
|
|
status: SubscriptionStatus;
|
|
amount: number;
|
|
cycle: SubscriptionCycle;
|
|
nextDue?: string | undefined;
|
|
registrationDate?: string | undefined;
|
|
};
|
|
}) {
|
|
return (
|
|
<div className="bg-card border border-border rounded-xl shadow-[var(--cp-shadow-1)] 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">
|
|
{sharedFormatCurrency(subscription.amount || 0)}
|
|
</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">
|
|
{formatIsoDate(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">
|
|
{formatIsoDate(subscription.registrationDate)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SimTabNavigation({
|
|
subscriptionId,
|
|
activeTab,
|
|
}: {
|
|
subscriptionId: number;
|
|
activeTab: string;
|
|
}) {
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
|
|
function detectServiceType(productName: string | undefined): {
|
|
isSim: boolean;
|
|
isInternet: boolean;
|
|
} {
|
|
const lower = productName?.toLowerCase() ?? "";
|
|
const isNttFiber = lower.includes("ntt") && lower.includes("fiber");
|
|
return {
|
|
isSim: lower.includes("sim"),
|
|
isInternet: lower.includes("internet") || lower.includes("sonixnet") || isNttFiber,
|
|
};
|
|
}
|
|
|
|
function getPageError(error: unknown): string | null {
|
|
if (!error) return null;
|
|
if (process.env.NODE_ENV === "development" && error instanceof Error) return error.message;
|
|
return "Unable to load subscription details. Please try again.";
|
|
}
|
|
|
|
function useActiveTab(searchParams: ReturnType<typeof useSearchParams>) {
|
|
const [activeTab, setActiveTab] = useState<"overview" | "sim">("overview");
|
|
useEffect(() => {
|
|
const updateTab = () => {
|
|
const hash = typeof window === "undefined" ? "" : window.location.hash;
|
|
const service = (searchParams.get("service") || "").toLowerCase();
|
|
setActiveTab(hash.includes("sim-management") || service === "sim" ? "sim" : "overview");
|
|
};
|
|
updateTab();
|
|
if (typeof window === "undefined") return;
|
|
window.addEventListener("hashchange", updateTab);
|
|
return () => window.removeEventListener("hashchange", updateTab);
|
|
}, [searchParams]);
|
|
return activeTab;
|
|
}
|
|
|
|
function CancelServiceAction({ subscriptionId }: { subscriptionId: number }) {
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
|
|
function SubscriptionDetailContent({
|
|
subscription,
|
|
subscriptionId,
|
|
activeTab,
|
|
}: {
|
|
subscription: {
|
|
productName: string;
|
|
status: SubscriptionStatus;
|
|
amount: number;
|
|
cycle: SubscriptionCycle;
|
|
nextDue?: string | undefined;
|
|
registrationDate?: string | undefined;
|
|
};
|
|
subscriptionId: number;
|
|
activeTab: string;
|
|
}) {
|
|
const { isSim } = detectServiceType(subscription.productName);
|
|
return (
|
|
<div className="space-y-6">
|
|
<SubscriptionStatsCard subscription={subscription} />
|
|
{isSim && <SimTabNavigation subscriptionId={subscriptionId} activeTab={activeTab} />}
|
|
{activeTab === "sim" && isSim && <SimManagementSection subscriptionId={subscriptionId} />}
|
|
{activeTab === "overview" && (
|
|
<SectionCard
|
|
icon={<CreditCardIcon className="h-5 w-5" />}
|
|
title="Billing Information"
|
|
subtitle="Payment and invoices"
|
|
tone="primary"
|
|
actions={
|
|
<Link
|
|
href="/account/billing"
|
|
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
|
|
>
|
|
View Invoices
|
|
</Link>
|
|
}
|
|
>
|
|
<p className="text-sm text-muted-foreground">
|
|
Invoices and payment history are available on the billing page.
|
|
</p>
|
|
</SectionCard>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SubscriptionDetailContainer() {
|
|
const params = useParams();
|
|
const searchParams = useSearchParams();
|
|
const subscriptionId = Number.parseInt(params["id"] as string);
|
|
const { data: subscription, error } = useSubscription(subscriptionId);
|
|
const activeTab = useActiveTab(searchParams);
|
|
|
|
if (!subscription && !error) {
|
|
return (
|
|
<PageLayout
|
|
icon={<ServerIcon className="h-6 w-6" />}
|
|
title="Subscription"
|
|
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}
|
|
>
|
|
<div className="space-y-6">
|
|
<SubscriptionDetailStatsSkeleton />
|
|
</div>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
const { isInternet } = detectServiceType(subscription?.productName);
|
|
const headerActions =
|
|
isInternet && subscription?.status === "Active" ? (
|
|
<CancelServiceAction subscriptionId={subscriptionId} />
|
|
) : undefined;
|
|
|
|
return (
|
|
<PageLayout
|
|
icon={<ServerIcon className="h-6 w-6" />}
|
|
title={subscription?.productName ?? "Subscription"}
|
|
actions={headerActions}
|
|
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}
|
|
error={getPageError(error)}
|
|
>
|
|
{subscription ? (
|
|
<SubscriptionDetailContent
|
|
subscription={subscription}
|
|
subscriptionId={subscriptionId}
|
|
activeTab={activeTab}
|
|
/>
|
|
) : null}
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
export default SubscriptionDetailContainer;
|