puffin-app/components/admin/OrderDetailsModal.tsx

360 lines
14 KiB
TypeScript
Raw Normal View History

'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Copy, Check, ExternalLink } from 'lucide-react';
import { useState } from 'react';
import { OrderRecord } from '@/src/types';
interface OrderDetailsModalProps {
order: OrderRecord | null;
isOpen: boolean;
onClose: () => void;
}
export function OrderDetailsModal({ order, isOpen, onClose }: OrderDetailsModalProps) {
const [copiedField, setCopiedField] = useState<string | null>(null);
if (!order) return null;
const copyToClipboard = async (text: string, fieldName: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedField(fieldName);
setTimeout(() => setCopiedField(null), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
const formatDate = (dateString: string | undefined) => {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const formatCurrency = (amount: string | undefined, currency: string = 'USD') => {
if (!amount) return 'N/A';
const numAmount = parseFloat(amount) / 100;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(numAmount);
};
const getStatusBadgeClass = (status: string) => {
switch (status) {
case 'pending':
return 'bg-gradient-to-r from-muted-gold/20 to-orange-400/20 text-muted-gold border border-muted-gold/30';
case 'paid':
return 'bg-gradient-to-r from-maritime-teal/20 to-teal-400/20 text-maritime-teal border border-maritime-teal/30';
case 'fulfilled':
return 'bg-gradient-to-r from-sea-green/20 to-green-400/20 text-sea-green border border-sea-green/30';
case 'cancelled':
return 'bg-gradient-to-r from-red-500/20 to-red-400/20 text-red-600 border border-red-500/30';
default:
return 'bg-gray-100 text-gray-700 border border-gray-300';
}
};
const CopyButton = ({ text, fieldName }: { text: string; fieldName: string }) => (
<button
type="button"
onClick={() => copyToClipboard(text, fieldName)}
className="ml-2 p-1 hover:bg-gray-100 rounded transition-colors"
title="Copy to clipboard"
>
{copiedField === fieldName ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<Copy className="w-4 h-4 text-deep-sea-blue/40" />
)}
</button>
);
const DetailField = ({
label,
value,
copyable = false,
fieldName = '',
}: {
label: string;
value: string | number | undefined | null;
copyable?: boolean;
fieldName?: string;
}) => {
const displayValue = value || 'N/A';
return (
<div className="py-3 border-b border-gray-100 last:border-0">
<div className="text-xs font-medium text-deep-sea-blue/60 mb-1">{label}</div>
<div className="flex items-center">
<div className="text-sm text-deep-sea-blue font-medium">{displayValue}</div>
{copyable && value && <CopyButton text={String(value)} fieldName={fieldName} />}
</div>
</div>
);
};
const SectionHeader = ({ title }: { title: string }) => (
<h3 className="text-lg font-bold text-deep-sea-blue mb-4 flex items-center">
<div className="w-1 h-6 bg-deep-sea-blue rounded-full mr-3"></div>
{title}
</h3>
);
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50 z-40"
/>
{/* Sliding Panel */}
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
className="fixed right-0 top-0 bottom-0 w-full max-w-2xl bg-white shadow-2xl z-50 overflow-hidden flex flex-col"
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 bg-gray-50">
<div>
<h2 className="text-2xl font-bold text-deep-sea-blue">Order Details</h2>
<p className="text-sm text-deep-sea-blue/60 mt-1">
ID: {order.orderId.substring(0, 12)}...
</p>
</div>
<button
type="button"
onClick={onClose}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
>
<X className="w-6 h-6 text-deep-sea-blue" />
</button>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-8">
{/* Order Information */}
<section>
<SectionHeader title="Order Information" />
<div className="bg-gray-50 rounded-xl p-4">
<DetailField label="Order ID" value={order.orderId} copyable fieldName="orderId" />
<div className="py-3 border-b border-gray-100">
<div className="text-xs font-medium text-deep-sea-blue/60 mb-1">Status</div>
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${getStatusBadgeClass(
order.status
)}`}
>
{order.status.charAt(0).toUpperCase() + order.status.slice(1)}
</span>
</div>
<DetailField label="Source" value={order.source || 'web'} />
<DetailField label="Created At" value={formatDate(order.CreatedAt)} />
<DetailField label="Last Updated" value={formatDate(order.UpdatedAt)} />
</div>
</section>
{/* Payment Details */}
<section>
<SectionHeader title="Payment Details" />
<div className="bg-gray-50 rounded-xl p-4">
<DetailField
label="Total Amount"
value={formatCurrency(order.totalAmount, order.currency)}
/>
<DetailField
label="Base Amount"
value={formatCurrency(order.baseAmount, order.currency)}
/>
<DetailField
label="Processing Fee"
value={formatCurrency(order.processingFee, order.currency)}
/>
<DetailField label="Currency" value={order.currency} />
{order.amountUSD && (
<DetailField label="Amount (USD)" value={formatCurrency(order.amountUSD, 'USD')} />
)}
<DetailField label="Payment Method" value={order.paymentMethod} />
<DetailField
label="Stripe Session ID"
value={order.stripeSessionId}
copyable
fieldName="stripeSessionId"
/>
<DetailField
label="Stripe Payment Intent"
value={order.stripePaymentIntent}
copyable
fieldName="stripePaymentIntent"
/>
<DetailField
label="Stripe Customer ID"
value={order.stripeCustomerId}
copyable
fieldName="stripeCustomerId"
/>
</div>
</section>
{/* Customer Information */}
<section>
<SectionHeader title="Customer Information" />
<div className="bg-gray-50 rounded-xl p-4">
<DetailField label="Name" value={order.customerName} />
<DetailField label="Email" value={order.customerEmail} copyable fieldName="email" />
<DetailField label="Phone" value={order.customerPhone} />
{order.businessName && (
<>
<DetailField label="Business Name" value={order.businessName} />
<DetailField label="Tax ID Type" value={order.taxIdType} />
<DetailField
label="Tax ID Value"
value={order.taxIdValue}
copyable
fieldName="taxId"
/>
</>
)}
</div>
</section>
{/* Billing Address */}
<section>
<SectionHeader title="Billing Address" />
<div className="bg-gray-50 rounded-xl p-4">
<DetailField label="Address Line 1" value={order.billingLine1} />
<DetailField label="Address Line 2" value={order.billingLine2} />
<DetailField label="City" value={order.billingCity} />
<DetailField label="State/Province" value={order.billingState} />
<DetailField label="Postal Code" value={order.billingPostalCode} />
<DetailField label="Country" value={order.billingCountry} />
</div>
</section>
{/* Carbon Offset Details */}
<section>
<SectionHeader title="Carbon Offset Details" />
<div className="bg-gray-50 rounded-xl p-4">
<div className="py-3 border-b border-gray-100">
<div className="text-xs font-medium text-deep-sea-blue/60 mb-1">CO Offset</div>
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold bg-sea-green/20 text-sea-green border border-sea-green/30">
{parseFloat(order.co2Tons).toFixed(2)} tons
</span>
</div>
<DetailField label="Portfolio ID" value={order.portfolioId} />
<DetailField label="Portfolio Name" value={order.portfolioName} />
<DetailField
label="Wren Order ID"
value={order.wrenOrderId}
copyable
fieldName="wrenOrderId"
/>
{order.certificateUrl && (
<div className="py-3">
<div className="text-xs font-medium text-deep-sea-blue/60 mb-1">
Certificate URL
</div>
<a
href={order.certificateUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-maritime-teal hover:text-maritime-teal/80 flex items-center font-medium"
>
View Certificate
<ExternalLink className="w-4 h-4 ml-1" />
</a>
</div>
)}
<DetailField label="Fulfilled At" value={formatDate(order.fulfilledAt)} />
</div>
</section>
{/* Vessel Information (if applicable) */}
{(order.vesselName || order.imoNumber || order.vesselType || order.vesselLength) && (
<section>
<SectionHeader title="Vessel Information" />
<div className="bg-gray-50 rounded-xl p-4">
<DetailField label="Vessel Name" value={order.vesselName} />
<DetailField label="IMO Number" value={order.imoNumber} />
<DetailField label="Vessel Type" value={order.vesselType} />
<DetailField
label="Vessel Length"
value={order.vesselLength ? `${order.vesselLength}m` : undefined}
/>
</div>
</section>
)}
{/* Trip Details (if applicable) */}
{(order.departurePort ||
order.arrivalPort ||
order.distance ||
order.avgSpeed ||
order.duration ||
order.enginePower) && (
<section>
<SectionHeader title="Trip Details" />
<div className="bg-gray-50 rounded-xl p-4">
<DetailField label="Departure Port" value={order.departurePort} />
<DetailField label="Arrival Port" value={order.arrivalPort} />
<DetailField
label="Distance"
value={order.distance ? `${order.distance} nm` : undefined}
/>
<DetailField
label="Average Speed"
value={order.avgSpeed ? `${order.avgSpeed} knots` : undefined}
/>
<DetailField
label="Duration"
value={order.duration ? `${order.duration} hours` : undefined}
/>
<DetailField
label="Engine Power"
value={order.enginePower ? `${order.enginePower} HP` : undefined}
/>
</div>
</section>
)}
{/* Admin Notes */}
{order.notes && (
<section>
<SectionHeader title="Admin Notes" />
<div className="bg-gray-50 rounded-xl p-4">
<p className="text-sm text-deep-sea-blue whitespace-pre-wrap">{order.notes}</p>
</div>
</section>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50">
<button
type="button"
onClick={onClose}
className="px-6 py-2.5 text-sm font-medium text-deep-sea-blue bg-white border border-light-gray-border rounded-lg hover:bg-gray-50 transition-colors"
>
Close
</button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}