207 lines
7.0 KiB
TypeScript
207 lines
7.0 KiB
TypeScript
"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 { Formatting } from "@customer-portal/domain/toolkit";
|
|
import type { Subscription } from "@customer-portal/domain/subscriptions";
|
|
|
|
const { formatCurrency, getCurrencyLocale } = Formatting;
|
|
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, 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, "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";
|