barsa 7502068ea9 refactor: remove unused billing and payment components, enhance animation capabilities
- 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.
2026-03-06 14:48:34 +09:00

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;