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>
184 lines
4.9 KiB
TypeScript
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;
|