211 lines
7.2 KiB
TypeScript
Raw Normal View History

"use client";
import { forwardRef } from "react";
import Link from "next/link";
import { format } from "date-fns";
import {
ServerIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
ClockIcon,
XCircleIcon,
CalendarIcon,
ArrowTopRightOnSquareIcon,
} from "@heroicons/react/24/outline";
import { StatusPill } from "@/components/atoms/status-pill";
import { Button } from "@/components/atoms/button";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain/billing";
import type { Subscription } from "@customer-portal/domain/billing";
import { cn } from "@/lib/utils";
interface SubscriptionCardProps {
subscription: Subscription;
variant?: "list" | "grid";
showActions?: boolean;
onViewClick?: (subscription: Subscription) => void;
className?: string;
}
const getStatusIcon = (status: string) => {
switch (status) {
case "Active":
return <CheckCircleIcon className="h-5 w-5 text-green-500" />;
case "Suspended":
return <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />;
case "Pending":
return <ClockIcon className="h-5 w-5 text-blue-500" />;
case "Cancelled":
case "Terminated":
return <XCircleIcon className="h-5 w-5 text-gray-500" />;
default:
return <ClockIcon className="h-5 w-5 text-gray-500" />;
}
};
const getStatusVariant = (status: string) => {
switch (status) {
case "Active":
return "success" as const;
case "Suspended":
return "warning" as const;
case "Pending":
return "info" as const;
case "Cancelled":
case "Terminated":
return "neutral" as const;
default:
return "neutral" as const;
}
};
const formatDate = (dateString: string | undefined) => {
if (!dateString) return "N/A";
try {
return format(new Date(dateString), "MMM d, yyyy");
} catch {
return "Invalid date";
}
};
const getBillingCycleLabel = (cycle: string) => {
const name = cycle.toLowerCase();
const looksLikeActivation = name.includes("activation") || name.includes("setup");
return looksLikeActivation ? "One-time" : cycle;
};
export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps>(
({ subscription, variant = "list", showActions = true, onViewClick, className }, ref) => {
const handleViewClick = () => {
if (onViewClick) {
onViewClick(subscription);
}
};
if (variant === "grid") {
return (
<SubCard ref={ref} className={cn("hover:shadow-lg transition-all duration-200", className)}>
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
{getStatusIcon(subscription.status)}
<div className="min-w-0 flex-1">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{subscription.productName}
</h3>
<p className="text-sm text-gray-500">Service ID: {subscription.serviceId}</p>
</div>
</div>
<StatusPill
label={subscription.status}
variant={getStatusVariant(subscription.status)}
size="sm"
/>
</div>
{/* Details */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-500">Price</p>
<p className="font-semibold text-gray-900">
{formatCurrency(subscription.amount, {
currency: subscription.currency,
locale: getCurrencyLocale(subscription.currency),
})}
</p>
<p className="text-xs text-gray-500">{getBillingCycleLabel(subscription.cycle)}</p>
</div>
<div>
<p className="text-gray-500">Next Due</p>
<div className="flex items-center space-x-1">
<CalendarIcon className="h-4 w-4 text-gray-400" />
<p className="font-medium text-gray-900">{formatDate(subscription.nextDue)}</p>
</div>
</div>
</div>
{/* Actions */}
{showActions && (
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<p className="text-xs text-gray-500">
Created {formatDate(subscription.registrationDate)}
</p>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={handleViewClick}
rightIcon={<ArrowTopRightOnSquareIcon className="h-4 w-4" />}
>
View
</Button>
</div>
</div>
)}
</div>
</SubCard>
);
}
// List variant (default)
return (
<SubCard ref={ref} className={cn("hover:shadow-md transition-all duration-200", className)}>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4 min-w-0 flex-1">
{getStatusIcon(subscription.status)}
<div className="min-w-0 flex-1">
<div className="flex items-center space-x-3">
<h3 className="text-base font-semibold text-gray-900 truncate">
{subscription.productName}
</h3>
<StatusPill
label={subscription.status}
variant={getStatusVariant(subscription.status)}
size="sm"
/>
</div>
<p className="text-sm text-gray-500 mt-1">Service ID: {subscription.serviceId}</p>
</div>
</div>
<div className="flex items-center space-x-6 text-sm">
<div className="text-right">
<p className="font-semibold text-gray-900">
{formatCurrency(subscription.amount, {
currency: "JPY",
locale: getCurrencyLocale("JPY"),
})}
</p>
<p className="text-gray-500">{getBillingCycleLabel(subscription.cycle)}</p>
</div>
<div className="text-right">
<div className="flex items-center space-x-1">
<CalendarIcon className="h-4 w-4 text-gray-400" />
<p className="text-gray-900">{formatDate(subscription.nextDue)}</p>
</div>
<p className="text-gray-500">Next due</p>
</div>
{showActions && (
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={handleViewClick}
rightIcon={<ArrowTopRightOnSquareIcon className="h-4 w-4" />}
>
View
</Button>
</div>
)}
</div>
</div>
</SubCard>
);
}
);
SubscriptionCard.displayName = "SubscriptionCard";