Files
blackcanyontickets/reactrebuild0825/src/components/checkout/OrderSummary.tsx
dzinesco 02a5146533 feat(domain): create comprehensive business components for ticketing platform
- Add EventCard with glassmorphism styling and role-based actions
- Add TicketTypeRow with inline editing and inventory tracking
- Add OrderSummary with promo codes and fee breakdown integration
- Add ScanStatusBadge with real-time updates and accessibility
- Add FeeBreakdown with transparent pricing and regulatory compliance
- Create business logic types for events, tickets, orders, scanning
- Implement responsive layouts (card/table) for all screen sizes
- Ensure WCAG AA compliance with proper ARIA labels and screen reader support
- Use design tokens exclusively for consistent theming
- Build comprehensive showcase component demonstrating all features

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-16 11:54:25 -06:00

308 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import { Order, FeeStructure, PromoCode, DEFAULT_FEE_STRUCTURE } from '../../types/business';
import { Card, CardHeader, CardBody, CardFooter } from '../ui/Card';
import { Button } from '../ui/Button';
import { Input } from '../ui/Input';
import { Badge } from '../ui/Badge';
import { Alert } from '../ui/Alert';
export interface OrderSummaryProps {
order: Order;
feeStructure?: FeeStructure;
onPromoCodeApply?: (code: string) => Promise<{ success: boolean; promoCode?: PromoCode; error?: string }>;
onPromoCodeRemove?: () => void;
layout?: 'compact' | 'detailed';
showPromoCode?: boolean;
className?: string;
}
const OrderSummary: React.FC<OrderSummaryProps> = ({
order,
feeStructure = DEFAULT_FEE_STRUCTURE,
onPromoCodeApply,
onPromoCodeRemove,
layout = 'detailed',
showPromoCode = true,
className = ''
}) => {
const [promoCodeInput, setPromoCodeInput] = useState('');
const [isApplyingPromo, setIsApplyingPromo] = useState(false);
const [promoError, setPromoError] = useState<string | null>(null);
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
// Format currency helper
const formatCurrency = (amountInCents: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amountInCents / 100);
};
// Handle promo code application
const handleApplyPromo = async () => {
if (!promoCodeInput.trim() || !onPromoCodeApply) return;
setIsApplyingPromo(true);
setPromoError(null);
try {
const result = await onPromoCodeApply(promoCodeInput.trim().toUpperCase());
if (result.success) {
setPromoCodeInput('');
} else {
setPromoError(result.error || 'Invalid promo code');
}
} catch (error) {
setPromoError('Failed to apply promo code. Please try again.');
} finally {
setIsApplyingPromo(false);
}
};
// Handle promo code removal
const handleRemovePromo = () => {
onPromoCodeRemove?.();
setPromoError(null);
};
// Fee breakdown calculation is handled by FeeBreakdown component
if (layout === 'compact') {
return (
<Card className={`${className}`} variant="elevated">
<CardBody className="space-y-3">
{/* Order Items Summary */}
<div className="space-y-2">
{order.items.map((item, index) => (
<div key={index} className="flex justify-between text-sm">
<span className="text-fg-secondary">
{item.quantity}x {item.ticketTypeName}
</span>
<span className="font-medium text-fg-primary">
{formatCurrency(item.subtotal)}
</span>
</div>
))}
</div>
{/* Promo Code */}
{order.promoCode && (
<div className="flex justify-between text-sm border-t border-border-subtle pt-2">
<span className="text-success">Promo: {order.promoCode}</span>
<span className="font-medium text-success">
-{formatCurrency(order.discount || 0)}
</span>
</div>
)}
{/* Total */}
<div className="flex justify-between items-center pt-2 border-t border-border-subtle">
<span className="text-lg font-semibold text-fg-primary">Total</span>
<span className="text-xl font-bold text-accent-primary">
{formatCurrency(order.total)}
</span>
</div>
{/* Fee Breakdown Toggle */}
<button
onClick={() => setShowFeeBreakdown(!showFeeBreakdown)}
className="text-xs text-fg-secondary hover:text-fg-primary transition-colors underline"
>
{showFeeBreakdown ? 'Hide' : 'Show'} fee breakdown
</button>
{showFeeBreakdown && (
<div className="space-y-1 text-xs text-fg-secondary border-t border-border-subtle pt-2">
<div className="flex justify-between">
<span>Subtotal</span>
<span>{formatCurrency(order.subtotal)}</span>
</div>
<div className="flex justify-between">
<span>Platform fee</span>
<span>{formatCurrency(order.platformFee)}</span>
</div>
<div className="flex justify-between">
<span>Processing fee</span>
<span>{formatCurrency(order.processingFee)}</span>
</div>
<div className="flex justify-between">
<span>Tax</span>
<span>{formatCurrency(order.tax)}</span>
</div>
</div>
)}
</CardBody>
</Card>
);
}
return (
<Card className={className} variant="elevated">
<CardHeader>
<h3 className="text-lg font-semibold text-fg-primary">Order Summary</h3>
</CardHeader>
<CardBody className="space-y-4">
{/* Order Items */}
<div className="space-y-3">
{order.items.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex-1">
<div className="font-medium text-fg-primary">{item.ticketTypeName}</div>
<div className="text-sm text-fg-secondary">
{formatCurrency(item.price)} × {item.quantity}
</div>
</div>
<div className="font-semibold text-fg-primary">
{formatCurrency(item.subtotal)}
</div>
</div>
))}
</div>
{/* Subtotal */}
<div className="flex justify-between items-center pt-3 border-t border-border-subtle">
<span className="font-medium text-fg-primary">Subtotal</span>
<span className="font-semibold text-fg-primary">
{formatCurrency(order.subtotal)}
</span>
</div>
{/* Promo Code Section */}
{showPromoCode && (
<div className="space-y-3">
{order.promoCode ? (
<div className="flex items-center justify-between p-3 bg-success/10 border border-success/20 rounded-lg">
<div className="flex items-center gap-2">
<Badge variant="success" size="sm">PROMO</Badge>
<span className="font-medium text-success">{order.promoCode}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-semibold text-success">
-{formatCurrency(order.discount || 0)}
</span>
<Button
variant="ghost"
size="sm"
onClick={handleRemovePromo}
className="p-1 h-6 w-6 text-success hover:text-success"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</Button>
</div>
</div>
) : (
<div className="space-y-2">
<div className="flex gap-2">
<Input
placeholder="Enter promo code"
value={promoCodeInput}
onChange={(e) => setPromoCodeInput(e.target.value.toUpperCase())}
className="flex-1"
onKeyDown={(e) => e.key === 'Enter' && handleApplyPromo()}
/>
<Button
variant="secondary"
onClick={handleApplyPromo}
disabled={!promoCodeInput.trim() || isApplyingPromo}
loading={isApplyingPromo}
>
Apply
</Button>
</div>
{promoError && (
<Alert variant="error" className="text-sm">
{promoError}
</Alert>
)}
</div>
)}
</div>
)}
{/* Fee Breakdown */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<button
onClick={() => setShowFeeBreakdown(!showFeeBreakdown)}
className="flex items-center gap-1 text-sm text-fg-secondary hover:text-fg-primary transition-colors"
>
<span>Fees & Taxes</span>
<svg
className={`w-4 h-4 transition-transform ${showFeeBreakdown ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<span className="font-medium text-fg-primary">
{formatCurrency(order.platformFee + order.processingFee + order.tax)}
</span>
</div>
{showFeeBreakdown && (
<div className="space-y-2 pl-4 border-l-2 border-border-subtle">
<div className="flex justify-between text-sm">
<div className="flex items-center gap-1">
<span className="text-fg-secondary">Platform fee</span>
<button
title={`${(feeStructure.platformFeeRate * 100).toFixed(1)}% + $${(feeStructure.platformFeeFixed / 100).toFixed(2)}`}
className="text-fg-muted hover:text-fg-secondary"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
<span className="text-fg-secondary">{formatCurrency(order.platformFee)}</span>
</div>
<div className="flex justify-between text-sm">
<div className="flex items-center gap-1">
<span className="text-fg-secondary">Processing fee</span>
<button
title={`${(feeStructure.processingFeeRate * 100).toFixed(1)}% + $${(feeStructure.processingFeeFixed / 100).toFixed(2)}`}
className="text-fg-muted hover:text-fg-secondary"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
<span className="text-fg-secondary">{formatCurrency(order.processingFee)}</span>
</div>
<div className="flex justify-between text-sm">
<div className="flex items-center gap-1">
<span className="text-fg-secondary">Tax</span>
<button
title={`${(feeStructure.taxRate * 100).toFixed(2)}% tax rate`}
className="text-fg-muted hover:text-fg-secondary"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
<span className="text-fg-secondary">{formatCurrency(order.tax)}</span>
</div>
</div>
)}
</div>
</CardBody>
<CardFooter className="border-t border-border-subtle">
<div className="flex justify-between items-center w-full">
<span className="text-xl font-bold text-fg-primary">Total</span>
<span className="text-2xl font-bold text-accent-primary">
{formatCurrency(order.total)}
</span>
</div>
</CardFooter>
</Card>
);
};
export default OrderSummary;