Files
blackcanyontickets/reactrebuild0825/src/components/territory/TerritoryKPITile.tsx
dzinesco aa81eb5adb feat: add advanced analytics and territory management system
- Add comprehensive analytics components with export functionality
- Implement territory management with manager performance tracking
- Add seatmap components for venue layout management
- Create customer management features with modal interface
- Add advanced hooks for dashboard flags and territory data
- Implement seat selection and venue management utilities
- Add type definitions for ticketing and seatmap systems

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 09:25:10 -06:00

189 lines
5.1 KiB
TypeScript

import React from 'react';
import { clsx } from 'clsx';
import { LucideIcon, TrendingUp, TrendingDown, Minus } from 'lucide-react';
import { Card, CardBody } from '@/components/ui/Card';
export interface TerritoryKPITileProps {
title: string;
value: string | number;
icon: LucideIcon;
trend?: {
value: number; // Percentage change
label: string; // e.g., "vs last period"
};
format?: 'currency' | 'number' | 'percentage';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
/**
* TerritoryKPITile - Reusable metric display tile for territory manager dashboard
*
* Features:
* - Glassmorphism design following project tokens
* - Trend indicators with color-coded arrows
* - Multiple format options (currency, number, percentage)
* - Responsive sizing
* - Proper accessibility with ARIA labels
*/
export const TerritoryKPITile: React.FC<TerritoryKPITileProps> = ({
title,
value,
icon: Icon,
trend,
format = 'number',
size = 'md',
className
}) => {
// Format the value based on type
const formatValue = (val: string | number): string => {
const numVal = typeof val === 'string' ? parseFloat(val) : val;
switch (format) {
case 'currency':
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numVal / 100); // Assuming value is in cents
case 'percentage':
return `${numVal.toFixed(1)}%`;
case 'number':
default:
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numVal);
}
};
// Determine trend styling
const getTrendColor = (change: number) => {
if (change > 0) return 'text-success';
if (change < 0) return 'text-error';
return 'text-text-secondary';
};
const getTrendIcon = (change: number) => {
if (change > 0) return TrendingUp;
if (change < 0) return TrendingDown;
return Minus;
};
// Size variants
const sizeClasses = {
sm: {
card: 'h-24',
icon: 'h-4 w-4',
value: 'text-lg font-bold',
title: 'text-xs',
trend: 'text-xs',
},
md: {
card: 'h-32',
icon: 'h-5 w-5',
value: 'text-2xl font-bold',
title: 'text-sm',
trend: 'text-xs',
},
lg: {
card: 'h-40',
icon: 'h-6 w-6',
value: 'text-3xl font-bold',
title: 'text-base',
trend: 'text-sm',
},
};
const sizeConfig = sizeClasses[size];
const TrendIcon = trend ? getTrendIcon(trend.value) : null;
return (
<Card
className={clsx(
'surface-card transition-all duration-200',
'hover:scale-[1.02] hover:shadow-elevation-lg',
sizeConfig.card,
className
)}
>
<CardBody className="p-4 h-full">
<div className="flex flex-col h-full justify-between">
{/* Header with icon and trend */}
<div className="flex items-center justify-between mb-2">
{/* Icon container */}
<div className="p-2 bg-glass-bg border border-glass-border rounded-lg">
<Icon
className={clsx(sizeConfig.icon, 'text-accent')}
aria-hidden="true"
/>
</div>
{/* Trend indicator */}
{trend && TrendIcon && (
<div
className={clsx(
'flex items-center space-x-1 px-2 py-1 rounded-full',
'bg-glass-bg border border-glass-border',
sizeConfig.trend
)}
aria-label={`Trend: ${trend.value > 0 ? 'up' : trend.value < 0 ? 'down' : 'flat'} ${Math.abs(trend.value)}%`}
>
<TrendIcon
className={clsx(
'h-3 w-3',
getTrendColor(trend.value)
)}
aria-hidden="true"
/>
<span className={getTrendColor(trend.value)}>
{Math.abs(trend.value).toFixed(1)}%
</span>
</div>
)}
</div>
{/* Value and label */}
<div className="space-y-1">
{/* Large value */}
<div
className={clsx(
sizeConfig.value,
'text-text-primary leading-none'
)}
aria-label={`${title}: ${formatValue(value)}`}
>
{formatValue(value)}
</div>
{/* Title label */}
<div className={clsx(
sizeConfig.title,
'text-text-secondary font-medium truncate'
)}>
{title}
</div>
{/* Trend label */}
{trend && (
<div className={clsx(
sizeConfig.trend,
'text-text-tertiary truncate'
)}>
{trend.label}
</div>
)}
</div>
</div>
</CardBody>
</Card>
);
};
export default TerritoryKPITile;