erp-construccion-frontend-v2/web/src/components/common/StatsCard.tsx
Adrian Flores Cortes 55261598a2 [FIX] fix: Resolve TypeScript errors for successful build
Fixes:
- Add teal, cyan, slate colors to StatusColor type and StatusBadge
- Create StatsCard component with color prop for backward compatibility
- Add label/required props to FormGroup component
- Fix Pagination to accept both currentPage and page props
- Fix unused imports in quality and contracts pages
- Add missing Plus, Trash2, User icon imports in contracts pages
- Remove duplicate formatDate function in ContratoDetailPage

New components:
- StatsCard, StatsCardGrid for statistics display

Build: Success (npm run build passes)
Dev: Success (npm run dev starts on port 3020)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:36:21 -06:00

184 lines
4.9 KiB
TypeScript

/**
* StatsCard - Statistics display card component
*/
import clsx from 'clsx';
import type { ReactNode } from 'react';
export interface StatsCardProps {
/** Card title/label */
title: string;
/** Main value to display */
value: string | number;
/** Optional description or subtitle */
description?: string;
/** Icon to display */
icon?: ReactNode;
/** Trend indicator */
trend?: {
value: number;
label?: string;
isPositive?: boolean;
};
/** Color variant */
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
/** Simple color prop (maps to variant for backward compatibility) */
color?: string;
/** Loading state */
loading?: boolean;
/** Additional CSS classes */
className?: string;
}
const variantClasses = {
default: {
icon: 'bg-background-muted dark:bg-background-muted text-foreground-muted',
accent: '',
},
primary: {
icon: 'bg-primary/10 dark:bg-primary/20 text-primary',
accent: 'border-l-4 border-l-primary',
},
success: {
icon: 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
accent: 'border-l-4 border-l-green-500',
},
warning: {
icon: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400',
accent: 'border-l-4 border-l-yellow-500',
},
danger: {
icon: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
accent: 'border-l-4 border-l-red-500',
},
info: {
icon: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
accent: 'border-l-4 border-l-blue-500',
},
};
export function StatsCard({
title,
value,
description,
icon,
trend,
variant = 'default',
color,
loading = false,
className,
}: StatsCardProps) {
// Map color prop to variant for backward compatibility
const colorToVariant: Record<string, keyof typeof variantClasses> = {
blue: 'info',
green: 'success',
yellow: 'warning',
red: 'danger',
orange: 'warning',
purple: 'primary',
indigo: 'primary',
};
const effectiveVariant = color ? (colorToVariant[color] || 'default') : variant;
const styles = variantClasses[effectiveVariant];
if (loading) {
return (
<div
className={clsx(
'bg-surface-card dark:bg-surface-card rounded-lg p-4 border border-border dark:border-border',
styles.accent,
className
)}
>
<div className="animate-pulse space-y-3">
<div className="h-4 bg-background-muted dark:bg-background-muted rounded w-1/2" />
<div className="h-8 bg-background-muted dark:bg-background-muted rounded w-3/4" />
<div className="h-3 bg-background-muted dark:bg-background-muted rounded w-1/3" />
</div>
</div>
);
}
return (
<div
className={clsx(
'bg-surface-card dark:bg-surface-card rounded-lg p-4 border border-border dark:border-border',
styles.accent,
className
)}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground-muted dark:text-foreground-muted truncate">
{title}
</p>
<p className="mt-1 text-2xl font-semibold text-foreground dark:text-foreground">
{value}
</p>
{description && (
<p className="mt-1 text-sm text-foreground-subtle dark:text-foreground-subtle">
{description}
</p>
)}
{trend && (
<div className="mt-2 flex items-center gap-1">
<span
className={clsx(
'text-sm font-medium',
trend.isPositive
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
)}
>
{trend.isPositive ? '↑' : '↓'} {Math.abs(trend.value)}%
</span>
{trend.label && (
<span className="text-sm text-foreground-muted dark:text-foreground-muted">
{trend.label}
</span>
)}
</div>
)}
</div>
{icon && (
<div
className={clsx(
'flex-shrink-0 p-3 rounded-lg',
styles.icon
)}
>
{icon}
</div>
)}
</div>
</div>
);
}
/**
* StatsCardGrid - Grid layout for multiple stats cards
*/
export function StatsCardGrid({
children,
cols = 4,
className,
}: {
children: ReactNode;
cols?: 2 | 3 | 4;
className?: string;
}) {
const colClasses = {
2: 'grid-cols-1 sm:grid-cols-2',
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
};
return (
<div className={clsx('grid gap-4', colClasses[cols], className)}>
{children}
</div>
);
}
export default StatsCard;