- Updated export statements in user and mapping mappers for consistency. - Enhanced FreebitAuthService to explicitly define response types for better type inference. - Refactored various services to improve error handling and response structure. - Cleaned up unused code and comments across multiple files to enhance readability. - Improved type annotations in invoice and subscription services for better validation and consistency.
267 lines
9.9 KiB
TypeScript
267 lines
9.9 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 { Formatting } from "@customer-portal/domain/toolkit";
|
|
|
|
const { formatCurrency: sharedFormatCurrency } = Formatting;
|
|
import { SimManagementSection } from "@/features/sim-management";
|
|
|
|
export function SubscriptionDetailContainer() {
|
|
const params = useParams();
|
|
const searchParams = useSearchParams();
|
|
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 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);
|
|
|
|
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;
|