316 lines
11 KiB
TypeScript

"use client";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { DetailHeader } from "@/components/molecules/DetailHeader/DetailHeader";
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "next/navigation";
import Link from "next/link";
import {
ArrowLeftIcon,
ServerIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
ClockIcon,
XCircleIcon,
CalendarIcon,
DocumentTextIcon,
ArrowTopRightOnSquareIcon,
} from "@heroicons/react/24/outline";
import { format } from "date-fns";
import { useSubscription } from "@/features/subscriptions/hooks";
import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList";
import { formatCurrency as sharedFormatCurrency, getCurrencyLocale } from "@customer-portal/domain";
import { SimManagementSection } from "@/features/sim-management";
export function SubscriptionDetailContainer() {
const params = useParams();
const searchParams = useSearchParams();
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const [showInvoices, setShowInvoices] = useState(true);
const [showSimManagement, setShowSimManagement] = useState(false);
const subscriptionId = parseInt(params.id as string);
const { data: subscription, isLoading, error } = useSubscription(subscriptionId);
useEffect(() => {
const updateVisibility = () => {
const hash = typeof window !== "undefined" ? window.location.hash : "";
const service = (searchParams.get("service") || "").toLowerCase();
const isSimContext = hash.includes("sim-management") || service === "sim";
if (isSimContext) {
setShowInvoices(false);
setShowSimManagement(true);
} else {
setShowInvoices(true);
setShowSimManagement(false);
}
};
updateVisibility();
if (typeof window !== "undefined") {
window.addEventListener("hashchange", updateVisibility);
return () => window.removeEventListener("hashchange", updateVisibility);
}
return;
}, [searchParams]);
const getStatusIcon = (status: string) => {
switch (status) {
case "Active":
return <CheckCircleIcon className="h-6 w-6 text-green-500" />;
case "Suspended":
return <ExclamationTriangleIcon className="h-6 w-6 text-yellow-500" />;
case "Terminated":
return <XCircleIcon className="h-6 w-6 text-red-500" />;
case "Cancelled":
return <XCircleIcon className="h-6 w-6 text-gray-500" />;
case "Pending":
return <ClockIcon className="h-6 w-6 text-blue-500" />;
default:
return <ServerIcon className="h-6 w-6 text-gray-500" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "Active":
return "bg-green-100 text-green-800";
case "Suspended":
return "bg-yellow-100 text-yellow-800";
case "Terminated":
return "bg-red-100 text-red-800";
case "Cancelled":
return "bg-gray-100 text-gray-800";
case "Pending":
return "bg-blue-100 text-blue-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const getInvoiceStatusIcon = (status: string) => {
switch (status) {
case "Paid":
return <CheckCircleIcon className="h-5 w-5 text-green-500" />;
case "Overdue":
return <ExclamationTriangleIcon className="h-5 w-5 text-red-500" />;
case "Unpaid":
return <ClockIcon className="h-5 w-5 text-yellow-500" />;
default:
return <DocumentTextIcon className="h-5 w-5 text-gray-500" />;
}
};
const getInvoiceStatusColor = (status: string) => {
switch (status) {
case "Paid":
return "bg-green-100 text-green-800";
case "Overdue":
return "bg-red-100 text-red-800";
case "Unpaid":
return "bg-yellow-100 text-yellow-800";
case "Cancelled":
return "bg-gray-100 text-gray-800";
default:
return "bg-gray-100 text-gray-800";
}
};
const formatDate = (dateString: string | undefined) => {
if (!dateString) return "N/A";
try {
return format(new Date(dateString), "MMM d, yyyy");
} catch {
return "Invalid date";
}
};
const formatCurrency = (amount: number) =>
sharedFormatCurrency(amount || 0, {
currency: subscription.currency,
locale: getCurrencyLocale(subscription.currency),
});
const formatBillingLabel = (cycle: string) => {
switch (cycle) {
case "Monthly":
return "Monthly Billing";
case "Annually":
return "Annual Billing";
case "Quarterly":
return "Quarterly Billing";
case "Semi-Annually":
return "Semi-Annual Billing";
case "Biennially":
return "Biennial Billing";
case "Triennially":
return "Triennial Billing";
default:
return "One-time Payment";
}
};
if (isLoading) {
return (
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 space-y-6">
<LoadingCard />
<div className="bg-white rounded-xl border p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-6 w-24" />
<Skeleton className="h-3 w-20" />
</div>
))}
</div>
</div>
<LoadingCard />
</div>
</div>
);
}
if (error || !subscription) {
return (
<div className="space-y-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading subscription</h3>
<div className="mt-2 text-sm text-red-700">
{error instanceof Error ? error.message : "Subscription not found"}
</div>
<div className="mt-4">
<Link href="/subscriptions" className="text-red-700 hover:text-red-600 font-medium">
Back to subscriptions
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<div className="mb-8">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Link href="/subscriptions" className="mr-4 text-gray-600 hover:text-gray-900">
<ArrowLeftIcon className="h-6 w-6" />
</Link>
<div className="flex items-center">
<ServerIcon className="h-8 w-8 text-blue-600 mr-3" />
<div>
<h1 className="text-2xl font-bold text-gray-900">{subscription.productName}</h1>
<p className="text-gray-600">Service ID: {subscription.serviceId}</p>
</div>
</div>
</div>
</div>
</div>
<SubCard className="mb-6">
<DetailHeader
title="Subscription Details"
subtitle="Service subscription information"
leftIcon={getStatusIcon(subscription.status)}
status={{
label: subscription.status,
variant:
subscription.status === "Active"
? "success"
: subscription.status === "Suspended"
? "warning"
: ["Cancelled", "Terminated"].includes(subscription.status)
? "neutral"
: "info",
}}
/>
<div className="pt-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-500 uppercase tracking-wider">
Billing Amount
</h4>
<p className="mt-2 text-2xl font-bold text-gray-900">
{formatCurrency(subscription.amount)}
</p>
<p className="text-sm text-gray-500">{formatBillingLabel(subscription.cycle)}</p>
</div>
<div>
<h4 className="text-sm font-medium text-gray-500 uppercase tracking-wider">
Next Due Date
</h4>
<p className="mt-2 text-lg text-gray-900">{formatDate(subscription.nextDue)}</p>
<div className="flex items-center mt-1">
<CalendarIcon className="h-4 w-4 text-gray-400 mr-1" />
<span className="text-sm text-gray-500">Due date</span>
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-500 uppercase tracking-wider">
Registration Date
</h4>
<p className="mt-2 text-lg text-gray-900">
{formatDate(subscription.registrationDate)}
</p>
<span className="text-sm text-gray-500">Service created</span>
</div>
</div>
</div>
</SubCard>
{subscription.productName.toLowerCase().includes("sim") && (
<div className="mb-8">
<SubCard>
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
<div>
<h3 className="text-xl font-semibold text-gray-900">Service Management</h3>
<p className="text-sm text-gray-600 mt-1">
Switch between billing and SIM management views
</p>
</div>
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 bg-gray-100 rounded-xl p-2">
<Link
href={`/subscriptions/${subscriptionId}#sim-management`}
className={`px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 min-w-[140px] text-center ${showSimManagement ? "bg-white text-blue-600 shadow-md hover:shadow-lg" : "text-gray-600 hover:text-gray-900 hover:bg-gray-200"}`}
>
<ServerIcon className="h-4 w-4 inline mr-2" />
SIM Management
</Link>
<Link
href={`/subscriptions/${subscriptionId}`}
className={`px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 min-w-[120px] text-center ${showInvoices ? "bg-white text-blue-600 shadow-md hover:shadow-lg" : "text-gray-600 hover:text-gray-900 hover:bg-gray-200"}`}
>
<DocumentTextIcon className="h-4 w-4 inline mr-2" />
Invoices
</Link>
</div>
</div>
</SubCard>
</div>
)}
{showSimManagement && (
<div className="mb-10">
<SimManagementSection subscriptionId={subscriptionId} />
</div>
)}
{showInvoices && <InvoicesList subscriptionId={subscriptionId} />}
</div>
</div>
);
}
export default SubscriptionDetailContainer;