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>
This commit is contained in:
2025-08-16 11:54:25 -06:00
parent 6d879d0685
commit 02a5146533
13 changed files with 2257 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
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;