2025-09-17 18:43:43 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { forwardRef } from "react";
|
|
|
|
|
import { format } from "date-fns";
|
2025-11-18 14:06:27 +09:00
|
|
|
import { CalendarIcon, ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
|
2025-09-20 11:35:40 +09:00
|
|
|
import { StatusPill } from "@/components/atoms/status-pill";
|
|
|
|
|
import { Button } from "@/components/atoms/button";
|
2025-09-25 15:14:36 +09:00
|
|
|
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
2025-10-09 10:49:03 +09:00
|
|
|
import type { Subscription } from "@customer-portal/domain/subscriptions";
|
|
|
|
|
|
2025-10-20 14:01:29 +09:00
|
|
|
import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
|
2025-09-20 11:35:40 +09:00
|
|
|
import { cn } from "@/lib/utils";
|
2025-11-18 14:06:27 +09:00
|
|
|
import {
|
|
|
|
|
getBillingCycleLabel,
|
|
|
|
|
getSubscriptionStatusIcon,
|
|
|
|
|
getSubscriptionStatusVariant,
|
|
|
|
|
} from "@/features/subscriptions/utils/status-presenters";
|
2025-09-17 18:43:43 +09:00
|
|
|
|
|
|
|
|
interface SubscriptionCardProps {
|
|
|
|
|
subscription: Subscription;
|
|
|
|
|
variant?: "list" | "grid";
|
|
|
|
|
showActions?: boolean;
|
|
|
|
|
onViewClick?: (subscription: Subscription) => void;
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateString: string | undefined) => {
|
|
|
|
|
if (!dateString) return "N/A";
|
|
|
|
|
try {
|
|
|
|
|
return format(new Date(dateString), "MMM d, yyyy");
|
|
|
|
|
} catch {
|
|
|
|
|
return "Invalid date";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps>(
|
|
|
|
|
({ subscription, variant = "list", showActions = true, onViewClick, className }, ref) => {
|
2025-10-20 14:01:29 +09:00
|
|
|
const { formatCurrency } = useFormatCurrency();
|
2025-10-22 10:58:16 +09:00
|
|
|
|
2025-09-17 18:43:43 +09:00
|
|
|
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">
|
2025-11-18 14:06:27 +09:00
|
|
|
{getSubscriptionStatusIcon(subscription.status)}
|
2025-09-17 18:43:43 +09:00
|
|
|
<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}
|
2025-11-18 14:06:27 +09:00
|
|
|
variant={getSubscriptionStatusVariant(subscription.status)}
|
2025-09-17 18:43:43 +09:00
|
|
|
size="sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Details */}
|
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-gray-500">Price</p>
|
2025-10-22 10:58:16 +09:00
|
|
|
<p className="font-semibold text-gray-900">{formatCurrency(subscription.amount)}</p>
|
2025-09-17 18:43:43 +09:00
|
|
|
<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">
|
2025-11-18 14:06:27 +09:00
|
|
|
{getSubscriptionStatusIcon(subscription.status)}
|
2025-09-17 18:43:43 +09:00
|
|
|
<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}
|
2025-11-18 14:06:27 +09:00
|
|
|
variant={getSubscriptionStatusVariant(subscription.status)}
|
2025-09-17 18:43:43 +09:00
|
|
|
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">
|
2025-10-22 10:58:16 +09:00
|
|
|
<p className="font-semibold text-gray-900">{formatCurrency(subscription.amount)}</p>
|
2025-09-17 18:43:43 +09:00
|
|
|
<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";
|