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>
This commit is contained in:
189
reactrebuild0825/src/components/territory/TerritoryKPITile.tsx
Normal file
189
reactrebuild0825/src/components/territory/TerritoryKPITile.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user