Files
blackcanyontickets/reactrebuild0825/src/components/tickets/InventoryAndRulesEditor.tsx
dzinesco 8ed7ae95d1 feat: comprehensive project completion and documentation
- Enhanced event creation wizard with multi-step validation
- Added advanced QR scanning system with offline support
- Implemented comprehensive territory management features
- Expanded analytics with export functionality and KPIs
- Created complete design token system with theme switching
- Added 25+ Playwright test files for comprehensive coverage
- Implemented enterprise-grade permission system
- Enhanced component library with 80+ React components
- Added Firebase integration for deployment
- Completed Phase 3 development goals substantially

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 15:04:37 -06:00

353 lines
15 KiB
TypeScript

import React from 'react';
import { Input } from '../ui/Input';
import { Card, CardBody, CardHeader } from '../ui/Card';
import { Badge } from '../ui/Badge';
import type { EnhancedTicketType, SharedPool, TicketRules } from '../../types/ticketing';
interface InventoryAndRulesEditorProps {
ticketType: EnhancedTicketType;
sharedPools: SharedPool[];
onUpdate: (updates: Partial<EnhancedTicketType>) => void;
}
export const InventoryAndRulesEditor: React.FC<InventoryAndRulesEditorProps> = ({
ticketType,
sharedPools,
onUpdate
}) => {
const formatDateForInput = (dateString?: string) => {
if (!dateString) return '';
try {
return new Date(dateString).toISOString().slice(0, 16);
} catch {
return '';
}
};
const handleRulesUpdate = (updates: Partial<TicketRules>) => {
const currentRules = ticketType.rules || {
idCheck: false,
reentry: 'none' as const,
waiverRequired: false
};
onUpdate({
rules: {
...currentRules,
...updates
}
});
};
return (
<div className="space-y-6">
{/* Inventory Management */}
<Card variant="surface" className="border-border-subtle">
<CardHeader>
<h4 className="text-lg font-semibold text-text-primary">Inventory & Capacity</h4>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input
label="Total Capacity *"
type="number"
min="0"
value={ticketType.capacity?.toString() || ''}
onChange={(e) => onUpdate({ capacity: parseInt(e.target.value) || 0 })}
placeholder="100"
helperText="Total tickets available"
required
/>
<div>
<label className="block text-sm font-medium text-text-secondary mb-2">
Sold
</label>
<div className="px-3 py-2 bg-background-secondary rounded-lg border border-border-subtle">
<span className="text-text-primary font-medium">{ticketType.sold}</span>
</div>
<p className="text-xs text-text-muted mt-1">Tickets sold (read-only)</p>
</div>
<div>
<label className="block text-sm font-medium text-text-secondary mb-2">
Available
</label>
<div className="px-3 py-2 bg-background-secondary rounded-lg border border-border-subtle">
<span className="text-text-primary font-medium">
{(ticketType.capacity || 0) - ticketType.sold - ticketType.reserved}
</span>
</div>
<p className="text-xs text-text-muted mt-1">Available for purchase</p>
</div>
</div>
{/* Sales Window */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
On Sale Start
</label>
<input
type="datetime-local"
value={formatDateForInput(ticketType.onSaleStart)}
onChange={(e) => {
if (e.target.value) {
onUpdate({ onSaleStart: new Date(e.target.value).toISOString() });
} else {
onUpdate({}); // Send empty update to clear the field
}
}}
className="w-full px-3 py-2 border border-border-subtle rounded-lg bg-background-elevated text-text-primary focus:ring-2 focus:ring-accent-primary-500 focus:border-accent-primary-500 transition-colors"
/>
<p className="text-xs text-text-muted mt-1">When sales begin (optional)</p>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
On Sale End
</label>
<input
type="datetime-local"
value={formatDateForInput(ticketType.onSaleEnd)}
onChange={(e) => {
if (e.target.value) {
onUpdate({ onSaleEnd: new Date(e.target.value).toISOString() });
} else {
onUpdate({}); // Send empty update to clear the field
}
}}
className="w-full px-3 py-2 border border-border-subtle rounded-lg bg-background-elevated text-text-primary focus:ring-2 focus:ring-accent-primary-500 focus:border-accent-primary-500 transition-colors"
/>
<p className="text-xs text-text-muted mt-1">When sales end (optional)</p>
</div>
</div>
{/* Shared Pools Section (Simplified) */}
{sharedPools.length > 0 && (
<div className="p-4 bg-background-secondary rounded-lg border border-border-subtle">
<h5 className="text-sm font-medium text-text-primary mb-2">Shared Inventory Pools</h5>
<p className="text-xs text-text-muted mb-3">
Advanced feature: Share capacity with other ticket types
</p>
<div className="space-y-2">
{sharedPools.map((pool) => (
<label key={pool.id} className="flex items-center gap-3">
<input
type="checkbox"
checked={ticketType.sharedPools?.some(ref => ref.poolId === pool.id) || false}
onChange={(e) => {
if (e.target.checked) {
const newRef = { poolId: pool.id, quantity: Math.min(50, pool.totalCapacity) };
onUpdate({
sharedPools: [...(ticketType.sharedPools || []), newRef]
});
} else {
const filtered = ticketType.sharedPools?.filter(ref => ref.poolId !== pool.id);
if (filtered && filtered.length > 0) {
onUpdate({ sharedPools: filtered });
} else {
onUpdate({}); // Send empty update to clear the field
}
}
}}
className="h-4 w-4 text-accent-primary-500 border-border-subtle rounded focus:ring-accent-primary-500 bg-background-elevated"
/>
<div className="flex-1">
<span className="text-sm text-text-primary">{pool.name}</span>
<span className="text-xs text-text-muted ml-2">
({pool.totalCapacity - pool.allocated} available)
</span>
</div>
</label>
))}
</div>
</div>
)}
</CardBody>
</Card>
{/* Gate Rules & Restrictions */}
<Card variant="surface" className="border-border-subtle">
<CardHeader>
<h4 className="text-lg font-semibold text-text-primary">Gate Rules & Restrictions</h4>
</CardHeader>
<CardBody className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Minimum Age"
type="number"
min="0"
max="120"
value={ticketType.rules?.ageMin?.toString() || ''}
onChange={(e) => {
if (e.target.value) {
const value = parseInt(e.target.value);
handleRulesUpdate({ ageMin: value });
} else {
handleRulesUpdate({}); // Send empty update to clear the field
}
}}
placeholder="18"
helperText="Optional age restriction"
/>
<Input
label="Maximum Age"
type="number"
min="0"
max="120"
value={ticketType.rules?.ageMax?.toString() || ''}
onChange={(e) => {
if (e.target.value) {
const value = parseInt(e.target.value);
handleRulesUpdate({ ageMax: value });
} else {
handleRulesUpdate({}); // Send empty update to clear the field
}
}}
placeholder="65"
helperText="For youth/senior tickets"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
Re-entry Policy
</label>
<select
value={ticketType.rules?.reentry || 'none'}
onChange={(e) => handleRulesUpdate({
reentry: e.target.value as 'none' | 'same_day' | 'unlimited'
})}
className="w-full px-3 py-2 border border-border-subtle rounded-lg bg-background-elevated text-text-primary focus:ring-2 focus:ring-accent-primary-500 focus:border-accent-primary-500 transition-colors"
>
<option value="none">No Re-entry</option>
<option value="same_day">Same Day Re-entry</option>
<option value="unlimited">Unlimited Re-entry</option>
</select>
</div>
<Input
label="Scan Limit"
type="number"
min="1"
value={ticketType.rules?.scanLimit?.toString() || ''}
onChange={(e) => {
if (e.target.value) {
const value = parseInt(e.target.value);
handleRulesUpdate({ scanLimit: value });
} else {
handleRulesUpdate({}); // Send empty update to clear the field
}
}}
placeholder="1"
helperText="Max times ticket can be scanned"
/>
</div>
<div className="space-y-3">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={ticketType.rules?.idCheck || false}
onChange={(e) => handleRulesUpdate({ idCheck: e.target.checked })}
className="h-4 w-4 text-accent-primary-500 border-border-subtle rounded focus:ring-accent-primary-500 bg-background-elevated"
/>
<span className="text-sm text-text-primary">Require ID Verification</span>
</label>
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={ticketType.rules?.waiverRequired || false}
onChange={(e) => handleRulesUpdate({ waiverRequired: e.target.checked })}
className="h-4 w-4 text-accent-primary-500 border-border-subtle rounded focus:ring-accent-primary-500 bg-background-elevated"
/>
<span className="text-sm text-text-primary">Require Signed Waiver</span>
</label>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
Special Instructions (for gate staff)
</label>
<textarea
value={ticketType.rules?.specialInstructions || ''}
onChange={(e) => {
if (e.target.value.trim()) {
handleRulesUpdate({ specialInstructions: e.target.value });
} else {
handleRulesUpdate({}); // Send empty update to clear the field
}
}}
placeholder="Special handling instructions for gate staff..."
rows={3}
maxLength={500}
className="w-full px-3 py-2 border border-border-subtle rounded-lg bg-background-elevated text-text-primary placeholder-text-muted focus:ring-2 focus:ring-accent-primary-500 focus:border-accent-primary-500 transition-colors resize-none"
/>
<p className="text-xs text-text-muted mt-1">
Visible to scanning staff only
</p>
</div>
{/* Rules Summary */}
<div className="p-4 bg-background-secondary rounded-lg border border-border-subtle">
<h5 className="text-sm font-medium text-text-primary mb-2">Gate Rules Summary</h5>
<div className="flex flex-wrap gap-2">
{ticketType.rules?.ageMin && (
<Badge variant="neutral" size="sm">
{ticketType.rules.ageMax
? `Ages ${ticketType.rules.ageMin}-${ticketType.rules.ageMax}`
: `Min Age ${ticketType.rules.ageMin}`
}
</Badge>
)}
{ticketType.rules?.idCheck && (
<Badge variant="warning" size="sm">ID Required</Badge>
)}
{ticketType.rules?.waiverRequired && (
<Badge variant="warning" size="sm">Waiver Required</Badge>
)}
{ticketType.rules?.reentry && ticketType.rules.reentry !== 'none' && (
<Badge variant="success" size="sm">
{ticketType.rules.reentry === 'same_day' ? 'Same Day Re-entry' : 'Unlimited Re-entry'}
</Badge>
)}
{ticketType.rules?.scanLimit && ticketType.rules.scanLimit > 1 && (
<Badge variant="neutral" size="sm">
{ticketType.rules.scanLimit}x Scan Limit
</Badge>
)}
</div>
{!ticketType.rules || Object.keys(ticketType.rules).length === 0 && (
<p className="text-xs text-text-muted italic">No special rules configured</p>
)}
</div>
</CardBody>
</Card>
{/* Tips */}
<div className="p-4 bg-background-secondary rounded-lg border border-border-subtle">
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-accent-primary-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
</div>
<div>
<h6 className="text-sm font-medium text-accent-primary-700 mb-2">Inventory & Rules Tips</h6>
<ul className="text-xs text-accent-primary-600 space-y-1">
<li> Set capacity slightly below venue max to account for staff, comps</li>
<li> Use sales windows to control when different tiers go on sale</li>
<li> ID checking helps prevent resale and ensures age compliance</li>
<li> Re-entry policies should match your event type and venue security</li>
<li> Shared pools work great for general admission with VIP upgrade options</li>
</ul>
</div>
</div>
</div>
</div>
);
};