- 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>
189 lines
5.1 KiB
TypeScript
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; |