Add new page designs
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, forwardRef } from 'react';
|
||||
import React, { useState, useCallback, forwardRef, useMemo } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuthUser, useIsAuthenticated } from '../../../stores';
|
||||
@@ -127,53 +127,55 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
const sidebarRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get navigation routes from config
|
||||
const navigationRoutes = getNavigationRoutes();
|
||||
|
||||
// Convert route config to navigation items
|
||||
const convertRoutesToItems = (routes: typeof navigationRoutes): NavigationItem[] => {
|
||||
return routes.map(route => ({
|
||||
id: route.path,
|
||||
label: route.title,
|
||||
path: route.path,
|
||||
icon: route.icon ? iconMap[route.icon] : undefined,
|
||||
requiredPermissions: route.requiredPermissions,
|
||||
requiredRoles: route.requiredRoles,
|
||||
children: route.children ? convertRoutesToItems(route.children) : undefined,
|
||||
}));
|
||||
};
|
||||
// Get navigation routes from config and convert to navigation items - memoized
|
||||
const navigationItems = useMemo(() => {
|
||||
const navigationRoutes = getNavigationRoutes();
|
||||
|
||||
const convertRoutesToItems = (routes: typeof navigationRoutes): NavigationItem[] => {
|
||||
return routes.map(route => ({
|
||||
id: route.path,
|
||||
label: route.title,
|
||||
path: route.path,
|
||||
icon: route.icon ? iconMap[route.icon] : undefined,
|
||||
requiredPermissions: route.requiredPermissions,
|
||||
requiredRoles: route.requiredRoles,
|
||||
children: route.children ? convertRoutesToItems(route.children) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
const navigationItems = customItems || convertRoutesToItems(navigationRoutes);
|
||||
return customItems || convertRoutesToItems(navigationRoutes);
|
||||
}, [customItems]);
|
||||
|
||||
// Filter items based on user permissions
|
||||
const filterItemsByPermissions = (items: NavigationItem[]): NavigationItem[] => {
|
||||
if (!isAuthenticated || !user) return [];
|
||||
// Filter items based on user permissions - memoized to prevent infinite re-renders
|
||||
const visibleItems = useMemo(() => {
|
||||
const filterItemsByPermissions = (items: NavigationItem[]): NavigationItem[] => {
|
||||
if (!isAuthenticated || !user) return [];
|
||||
|
||||
return items.filter(item => {
|
||||
const userRoles = user.role ? [user.role] : [];
|
||||
const userPermissions: string[] = user?.permissions || [];
|
||||
return items.map(item => ({
|
||||
...item, // Create a shallow copy to avoid mutation
|
||||
children: item.children ? filterItemsByPermissions(item.children) : item.children
|
||||
})).filter(item => {
|
||||
const userRoles = user.role ? [user.role] : [];
|
||||
const userPermissions: string[] = user?.permissions || [];
|
||||
|
||||
const hasAccess = !item.requiredPermissions && !item.requiredRoles ||
|
||||
canAccessRoute(
|
||||
{
|
||||
path: item.path,
|
||||
requiredRoles: item.requiredRoles,
|
||||
requiredPermissions: item.requiredPermissions
|
||||
} as any,
|
||||
isAuthenticated,
|
||||
userRoles,
|
||||
userPermissions
|
||||
);
|
||||
const hasAccess = !item.requiredPermissions && !item.requiredRoles ||
|
||||
canAccessRoute(
|
||||
{
|
||||
path: item.path,
|
||||
requiredRoles: item.requiredRoles,
|
||||
requiredPermissions: item.requiredPermissions
|
||||
} as any,
|
||||
isAuthenticated,
|
||||
userRoles,
|
||||
userPermissions
|
||||
);
|
||||
|
||||
if (hasAccess && item.children) {
|
||||
item.children = filterItemsByPermissions(item.children);
|
||||
}
|
||||
return hasAccess;
|
||||
});
|
||||
};
|
||||
|
||||
return hasAccess;
|
||||
});
|
||||
};
|
||||
|
||||
const visibleItems = filterItemsByPermissions(navigationItems);
|
||||
return filterItemsByPermissions(navigationItems);
|
||||
}, [navigationItems, isAuthenticated, user]);
|
||||
|
||||
// Handle item click
|
||||
const handleItemClick = useCallback((item: NavigationItem) => {
|
||||
@@ -224,30 +226,30 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
}, []);
|
||||
|
||||
// Auto-expand parent items for active path
|
||||
React.useEffect(() => {
|
||||
const findParentPaths = (items: NavigationItem[], targetPath: string, parents: string[] = []): string[] => {
|
||||
for (const item of items) {
|
||||
const currentPath = [...parents, item.id];
|
||||
|
||||
if (item.path === targetPath) {
|
||||
return parents;
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
const found = findParentPaths(item.children, targetPath, currentPath);
|
||||
if (found.length > 0) {
|
||||
return found;
|
||||
}
|
||||
const findParentPaths = useCallback((items: NavigationItem[], targetPath: string, parents: string[] = []): string[] => {
|
||||
for (const item of items) {
|
||||
const currentPath = [...parents, item.id];
|
||||
|
||||
if (item.path === targetPath) {
|
||||
return parents;
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
const found = findParentPaths(item.children, targetPath, currentPath);
|
||||
if (found.length > 0) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
}
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const parentPaths = findParentPaths(visibleItems, location.pathname);
|
||||
if (parentPaths.length > 0) {
|
||||
setExpandedItems(prev => new Set([...prev, ...parentPaths]));
|
||||
}
|
||||
}, [location.pathname, visibleItems]);
|
||||
}, [location.pathname, findParentPaths, visibleItems]);
|
||||
|
||||
// Expose ref methods
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
|
||||
215
frontend/src/components/ui/Stats/StatsCard.tsx
Normal file
215
frontend/src/components/ui/Stats/StatsCard.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { Card } from '../Card';
|
||||
|
||||
export type StatsCardVariant = 'default' | 'success' | 'info' | 'warning' | 'error' | 'purple';
|
||||
export type StatsCardSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
export interface StatsCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon?: LucideIcon;
|
||||
variant?: StatsCardVariant;
|
||||
size?: StatsCardSize;
|
||||
trend?: {
|
||||
value: number;
|
||||
label?: string;
|
||||
direction: 'up' | 'down' | 'neutral';
|
||||
};
|
||||
subtitle?: string;
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
formatValue?: (value: string | number) => string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const StatsCard = forwardRef<HTMLDivElement, StatsCardProps>(({
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
trend,
|
||||
subtitle,
|
||||
loading = false,
|
||||
className,
|
||||
formatValue,
|
||||
onClick,
|
||||
...props
|
||||
}, ref) => {
|
||||
const formattedValue = formatValue ? formatValue(value) : value;
|
||||
|
||||
const variantStyles = {
|
||||
default: {
|
||||
iconColor: 'var(--text-tertiary)',
|
||||
valueColor: 'var(--text-primary)',
|
||||
iconBg: 'var(--bg-tertiary)',
|
||||
},
|
||||
success: {
|
||||
iconColor: 'var(--color-success)',
|
||||
valueColor: 'var(--color-success)',
|
||||
iconBg: 'var(--color-success-50)',
|
||||
},
|
||||
info: {
|
||||
iconColor: 'var(--color-info)',
|
||||
valueColor: 'var(--color-info)',
|
||||
iconBg: 'var(--color-info-50)',
|
||||
},
|
||||
warning: {
|
||||
iconColor: 'var(--color-warning)',
|
||||
valueColor: 'var(--color-warning)',
|
||||
iconBg: 'var(--color-warning-50)',
|
||||
},
|
||||
error: {
|
||||
iconColor: 'var(--color-error)',
|
||||
valueColor: 'var(--color-error)',
|
||||
iconBg: 'var(--color-error-50)',
|
||||
},
|
||||
purple: {
|
||||
iconColor: '#a78bfa',
|
||||
valueColor: '#a78bfa',
|
||||
iconBg: 'rgba(167, 139, 250, 0.1)',
|
||||
},
|
||||
};
|
||||
|
||||
const sizeStyles = {
|
||||
sm: {
|
||||
padding: 'p-4',
|
||||
iconSize: 20,
|
||||
valueSize: 'text-2xl',
|
||||
titleSize: 'text-sm',
|
||||
iconPadding: 'p-2',
|
||||
},
|
||||
md: {
|
||||
padding: 'p-6',
|
||||
iconSize: 24,
|
||||
valueSize: 'text-3xl',
|
||||
titleSize: 'text-sm',
|
||||
iconPadding: 'p-2.5',
|
||||
},
|
||||
lg: {
|
||||
padding: 'p-8',
|
||||
iconSize: 28,
|
||||
valueSize: 'text-4xl',
|
||||
titleSize: 'text-base',
|
||||
iconPadding: 'p-3',
|
||||
},
|
||||
};
|
||||
|
||||
const currentVariant = variantStyles[variant];
|
||||
const currentSize = sizeStyles[size];
|
||||
const isInteractive = !!onClick;
|
||||
|
||||
const cardClasses = clsx(
|
||||
{
|
||||
'animate-pulse': loading,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
const iconContainerClasses = clsx(
|
||||
'rounded-xl flex items-center justify-center',
|
||||
currentSize.iconPadding
|
||||
);
|
||||
|
||||
const iconContainerStyle = {
|
||||
backgroundColor: currentVariant.iconBg,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card
|
||||
padding={size}
|
||||
interactive={isInteractive}
|
||||
className={cardClasses}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20"></div>
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16"></div>
|
||||
{subtitle && <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-24"></div>}
|
||||
</div>
|
||||
<div
|
||||
className="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-xl"
|
||||
></div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
padding={size}
|
||||
interactive={isInteractive}
|
||||
onClick={onClick}
|
||||
className={cardClasses}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3
|
||||
className={clsx('font-medium mb-2 leading-tight', currentSize.titleSize)}
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<span
|
||||
className={clsx('font-bold leading-none', currentSize.valueSize)}
|
||||
style={{ color: currentVariant.valueColor }}
|
||||
>
|
||||
{formattedValue}
|
||||
</span>
|
||||
|
||||
{trend && (
|
||||
<span
|
||||
className={clsx(
|
||||
'text-xs font-medium px-1.5 py-0.5 rounded-full',
|
||||
{
|
||||
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400': trend.direction === 'up',
|
||||
'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400': trend.direction === 'down',
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300': trend.direction === 'neutral',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{trend.direction === 'up' && '+'}
|
||||
{trend.value}
|
||||
{trend.label && ` ${trend.label}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{subtitle && (
|
||||
<p
|
||||
className="text-xs leading-relaxed"
|
||||
style={{ color: 'var(--text-tertiary)' }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Icon && (
|
||||
<div
|
||||
className={iconContainerClasses}
|
||||
style={iconContainerStyle}
|
||||
>
|
||||
<Icon
|
||||
size={currentSize.iconSize}
|
||||
style={{ color: currentVariant.iconColor }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
StatsCard.displayName = 'StatsCard';
|
||||
|
||||
export default StatsCard;
|
||||
201
frontend/src/components/ui/Stats/StatsExample.tsx
Normal file
201
frontend/src/components/ui/Stats/StatsExample.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import { FC } from 'react';
|
||||
import { StatsGrid, StatsCard } from './index';
|
||||
import { pagePresets, businessMetrics, formatters } from './StatsPresets';
|
||||
import {
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
Zap,
|
||||
Shield
|
||||
} from 'lucide-react';
|
||||
|
||||
// Example: Production Management Stats (matching your screenshot)
|
||||
export const ProductionStatsExample: FC = () => {
|
||||
// Sample data
|
||||
const productionData = {
|
||||
dailyTarget: 150,
|
||||
completed: 85,
|
||||
inProgress: 12,
|
||||
pending: 53,
|
||||
efficiency: 78,
|
||||
quality: 94,
|
||||
};
|
||||
|
||||
return (
|
||||
<StatsGrid
|
||||
stats={pagePresets.production(productionData)}
|
||||
columns={6}
|
||||
title="Gestión de Producción"
|
||||
description="Planifica y controla la producción diaria de tu panadería"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Example: Custom Stats Configuration
|
||||
export const CustomStatsExample = () => {
|
||||
return (
|
||||
<StatsGrid
|
||||
stats={[
|
||||
{
|
||||
title: 'Meta Diaria',
|
||||
value: 150,
|
||||
icon: Calendar,
|
||||
variant: 'default',
|
||||
},
|
||||
{
|
||||
title: 'Completado',
|
||||
value: 85,
|
||||
icon: CheckCircle,
|
||||
variant: 'success',
|
||||
},
|
||||
{
|
||||
title: 'En Proceso',
|
||||
value: 12,
|
||||
icon: Clock,
|
||||
variant: 'info',
|
||||
},
|
||||
{
|
||||
title: 'Pendiente',
|
||||
value: 53,
|
||||
icon: AlertTriangle,
|
||||
variant: 'warning',
|
||||
},
|
||||
{
|
||||
title: 'Eficiencia',
|
||||
value: 78,
|
||||
icon: Zap,
|
||||
variant: 'purple',
|
||||
formatValue: formatters.percentage,
|
||||
},
|
||||
{
|
||||
title: 'Calidad',
|
||||
value: 94,
|
||||
icon: Shield,
|
||||
variant: 'success',
|
||||
formatValue: formatters.percentage,
|
||||
},
|
||||
]}
|
||||
columns={6}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Example: Sales Dashboard
|
||||
export const SalesStatsExample = () => {
|
||||
const salesData = {
|
||||
revenue: 15420,
|
||||
revenueGrowth: 12.5,
|
||||
orders: 142,
|
||||
ordersGrowth: -3.2,
|
||||
customers: 89,
|
||||
};
|
||||
|
||||
return (
|
||||
<StatsGrid
|
||||
stats={pagePresets.sales(salesData)}
|
||||
columns={3}
|
||||
title="Ventas del Día"
|
||||
description="Resumen de rendimiento de ventas"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Example: Individual StatsCard Usage
|
||||
export const IndividualStatsExample = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatsCard
|
||||
title="Pedidos Hoy"
|
||||
value={45}
|
||||
icon={CheckCircle}
|
||||
variant="success"
|
||||
trend={{
|
||||
value: 15,
|
||||
direction: 'up',
|
||||
label: '%',
|
||||
}}
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Ingresos"
|
||||
value={2840}
|
||||
icon={Calendar}
|
||||
variant="info"
|
||||
formatValue={formatters.currency}
|
||||
subtitle="Últimas 24 horas"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Productos Vendidos"
|
||||
value={1250}
|
||||
icon={Zap}
|
||||
variant="default"
|
||||
formatValue={formatters.number}
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Satisfacción"
|
||||
value={96}
|
||||
icon={Shield}
|
||||
variant="success"
|
||||
formatValue={formatters.percentage}
|
||||
onClick={() => console.log('Navigate to satisfaction details')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example: Loading State
|
||||
export const LoadingStatsExample = () => {
|
||||
return (
|
||||
<StatsGrid
|
||||
stats={[
|
||||
{ title: '', value: '' },
|
||||
{ title: '', value: '' },
|
||||
{ title: '', value: '' },
|
||||
{ title: '', value: '' },
|
||||
]}
|
||||
loading
|
||||
columns={4}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Example: Using Business Metrics Directly
|
||||
export const BusinessMetricsExample = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<StatsGrid
|
||||
stats={[
|
||||
businessMetrics.production.dailyTarget(150),
|
||||
businessMetrics.production.completed(85),
|
||||
businessMetrics.production.efficiency(78),
|
||||
]}
|
||||
columns={3}
|
||||
title="Producción"
|
||||
/>
|
||||
|
||||
<StatsGrid
|
||||
stats={[
|
||||
businessMetrics.sales.revenue(15420, { value: 12.5, direction: 'up' }),
|
||||
businessMetrics.sales.orders(142, { value: 3.2, direction: 'down' }),
|
||||
businessMetrics.sales.customers(89),
|
||||
]}
|
||||
columns={3}
|
||||
title="Ventas"
|
||||
/>
|
||||
|
||||
<StatsGrid
|
||||
stats={[
|
||||
businessMetrics.inventory.totalItems(450),
|
||||
businessMetrics.inventory.lowStock(12),
|
||||
businessMetrics.inventory.outOfStock(3),
|
||||
]}
|
||||
columns={3}
|
||||
title="Inventario"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
94
frontend/src/components/ui/Stats/StatsGrid.tsx
Normal file
94
frontend/src/components/ui/Stats/StatsGrid.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import StatsCard, { StatsCardProps } from './StatsCard';
|
||||
|
||||
export interface StatsGridProps {
|
||||
stats: StatsCardProps[];
|
||||
columns?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
gap?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const StatsGrid: React.FC<StatsGridProps> = ({
|
||||
stats,
|
||||
columns = 3,
|
||||
gap = 'md',
|
||||
className,
|
||||
loading = false,
|
||||
title,
|
||||
description,
|
||||
}) => {
|
||||
const gridClasses = {
|
||||
1: 'grid-cols-1',
|
||||
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',
|
||||
5: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5',
|
||||
6: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6',
|
||||
};
|
||||
|
||||
const gapClasses = {
|
||||
sm: 'gap-3',
|
||||
md: 'gap-4',
|
||||
lg: 'gap-6',
|
||||
};
|
||||
|
||||
const containerClasses = clsx(
|
||||
'grid',
|
||||
gridClasses[columns],
|
||||
gapClasses[gap],
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
{(title || description) && (
|
||||
<div className="space-y-2">
|
||||
{title && (
|
||||
<h2
|
||||
className="text-2xl font-bold leading-tight"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p
|
||||
className="text-base leading-relaxed"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className={containerClasses}>
|
||||
{loading
|
||||
? Array.from({ length: stats.length || 6 }).map((_, index) => (
|
||||
<StatsCard
|
||||
key={index}
|
||||
title=""
|
||||
value=""
|
||||
loading
|
||||
/>
|
||||
))
|
||||
: stats.map((stat, index) => (
|
||||
<StatsCard
|
||||
key={stat.title || index}
|
||||
{...stat}
|
||||
loading={loading}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsGrid;
|
||||
237
frontend/src/components/ui/Stats/StatsPresets.ts
Normal file
237
frontend/src/components/ui/Stats/StatsPresets.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import {
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
Zap,
|
||||
Shield,
|
||||
TrendingUp,
|
||||
Package,
|
||||
Users,
|
||||
DollarSign,
|
||||
BarChart3,
|
||||
Target,
|
||||
Activity,
|
||||
Award
|
||||
} from 'lucide-react';
|
||||
import { StatsCardProps, StatsCardVariant } from './StatsCard';
|
||||
|
||||
// Common formatting functions
|
||||
export const formatters = {
|
||||
percentage: (value: string | number): string => `${value}%`,
|
||||
currency: (value: string | number): string => `€${parseFloat(String(value)).toFixed(2)}`,
|
||||
number: (value: string | number): string => parseFloat(String(value)).toLocaleString('es-ES'),
|
||||
compact: (value: string | number): string => {
|
||||
const num = parseFloat(String(value));
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
|
||||
return num.toString();
|
||||
},
|
||||
};
|
||||
|
||||
// Icon mappings for common stat types
|
||||
export const statIcons = {
|
||||
target: Calendar,
|
||||
completed: CheckCircle,
|
||||
inProgress: Clock,
|
||||
pending: AlertTriangle,
|
||||
efficiency: Zap,
|
||||
quality: Shield,
|
||||
growth: TrendingUp,
|
||||
inventory: Package,
|
||||
users: Users,
|
||||
revenue: DollarSign,
|
||||
analytics: BarChart3,
|
||||
goals: Target,
|
||||
activity: Activity,
|
||||
achievement: Award,
|
||||
};
|
||||
|
||||
// Variant mappings for common stat types
|
||||
export const statVariants: Record<string, StatsCardVariant> = {
|
||||
target: 'default',
|
||||
completed: 'success',
|
||||
inProgress: 'info',
|
||||
pending: 'warning',
|
||||
efficiency: 'purple',
|
||||
quality: 'success',
|
||||
error: 'error',
|
||||
revenue: 'success',
|
||||
growth: 'success',
|
||||
decline: 'error',
|
||||
};
|
||||
|
||||
// Predefined stat configurations for common business metrics
|
||||
export const businessMetrics = {
|
||||
production: {
|
||||
dailyTarget: (value: number): StatsCardProps => ({
|
||||
title: 'Meta Diaria',
|
||||
value,
|
||||
icon: statIcons.target,
|
||||
variant: statVariants.target,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
completed: (value: number): StatsCardProps => ({
|
||||
title: 'Completado',
|
||||
value,
|
||||
icon: statIcons.completed,
|
||||
variant: statVariants.completed,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
inProgress: (value: number): StatsCardProps => ({
|
||||
title: 'En Proceso',
|
||||
value,
|
||||
icon: statIcons.inProgress,
|
||||
variant: statVariants.inProgress,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
pending: (value: number): StatsCardProps => ({
|
||||
title: 'Pendiente',
|
||||
value,
|
||||
icon: statIcons.pending,
|
||||
variant: statVariants.pending,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
efficiency: (value: number): StatsCardProps => ({
|
||||
title: 'Eficiencia',
|
||||
value,
|
||||
icon: statIcons.efficiency,
|
||||
variant: statVariants.efficiency,
|
||||
formatValue: formatters.percentage,
|
||||
}),
|
||||
quality: (value: number): StatsCardProps => ({
|
||||
title: 'Calidad',
|
||||
value,
|
||||
icon: statIcons.quality,
|
||||
variant: statVariants.quality,
|
||||
formatValue: formatters.percentage,
|
||||
}),
|
||||
},
|
||||
|
||||
sales: {
|
||||
revenue: (value: number, trend?: { value: number; direction: 'up' | 'down' | 'neutral'; label?: string }): StatsCardProps => ({
|
||||
title: 'Ingresos',
|
||||
value,
|
||||
icon: statIcons.revenue,
|
||||
variant: statVariants.revenue,
|
||||
formatValue: formatters.currency,
|
||||
trend,
|
||||
}),
|
||||
orders: (value: number, trend?: { value: number; direction: 'up' | 'down' | 'neutral'; label?: string }): StatsCardProps => ({
|
||||
title: 'Pedidos',
|
||||
value,
|
||||
icon: statIcons.analytics,
|
||||
variant: statVariants.target,
|
||||
formatValue: formatters.number,
|
||||
trend,
|
||||
}),
|
||||
customers: (value: number): StatsCardProps => ({
|
||||
title: 'Clientes',
|
||||
value,
|
||||
icon: statIcons.users,
|
||||
variant: statVariants.target,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
},
|
||||
|
||||
inventory: {
|
||||
totalItems: (value: number): StatsCardProps => ({
|
||||
title: 'Total Items',
|
||||
value,
|
||||
icon: statIcons.inventory,
|
||||
variant: statVariants.target,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
lowStock: (value: number): StatsCardProps => ({
|
||||
title: 'Stock Bajo',
|
||||
value,
|
||||
icon: statIcons.pending,
|
||||
variant: value > 0 ? statVariants.pending : statVariants.completed,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
outOfStock: (value: number): StatsCardProps => ({
|
||||
title: 'Sin Stock',
|
||||
value,
|
||||
icon: statIcons.pending,
|
||||
variant: value > 0 ? statVariants.error : statVariants.completed,
|
||||
formatValue: formatters.number,
|
||||
}),
|
||||
},
|
||||
|
||||
performance: {
|
||||
growth: (value: number): StatsCardProps => ({
|
||||
title: 'Crecimiento',
|
||||
value,
|
||||
icon: statIcons.growth,
|
||||
variant: value >= 0 ? statVariants.growth : statVariants.decline,
|
||||
formatValue: formatters.percentage,
|
||||
trend: {
|
||||
value: Math.abs(value),
|
||||
direction: value >= 0 ? 'up' : 'down',
|
||||
label: 'vs mes anterior',
|
||||
},
|
||||
}),
|
||||
satisfaction: (value: number): StatsCardProps => ({
|
||||
title: 'Satisfacción',
|
||||
value,
|
||||
icon: statIcons.achievement,
|
||||
variant: value >= 80 ? statVariants.quality : value >= 60 ? statVariants.pending : statVariants.error,
|
||||
formatValue: formatters.percentage,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Quick preset configurations for common page layouts
|
||||
export const pagePresets = {
|
||||
production: (data: {
|
||||
dailyTarget: number;
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
pending: number;
|
||||
efficiency: number;
|
||||
quality: number;
|
||||
}): StatsCardProps[] => [
|
||||
businessMetrics.production.dailyTarget(data.dailyTarget),
|
||||
businessMetrics.production.completed(data.completed),
|
||||
businessMetrics.production.inProgress(data.inProgress),
|
||||
businessMetrics.production.pending(data.pending),
|
||||
businessMetrics.production.efficiency(data.efficiency),
|
||||
businessMetrics.production.quality(data.quality),
|
||||
],
|
||||
|
||||
sales: (data: {
|
||||
revenue: number;
|
||||
revenueGrowth?: number;
|
||||
orders: number;
|
||||
ordersGrowth?: number;
|
||||
customers: number;
|
||||
}): StatsCardProps[] => [
|
||||
businessMetrics.sales.revenue(
|
||||
data.revenue,
|
||||
data.revenueGrowth ? {
|
||||
value: data.revenueGrowth,
|
||||
direction: data.revenueGrowth >= 0 ? 'up' : 'down',
|
||||
label: '%',
|
||||
} : undefined
|
||||
),
|
||||
businessMetrics.sales.orders(
|
||||
data.orders,
|
||||
data.ordersGrowth ? {
|
||||
value: data.ordersGrowth,
|
||||
direction: data.ordersGrowth >= 0 ? 'up' : 'down',
|
||||
label: '%',
|
||||
} : undefined
|
||||
),
|
||||
businessMetrics.sales.customers(data.customers),
|
||||
],
|
||||
|
||||
inventory: (data: {
|
||||
totalItems: number;
|
||||
lowStock: number;
|
||||
outOfStock: number;
|
||||
}): StatsCardProps[] => [
|
||||
businessMetrics.inventory.totalItems(data.totalItems),
|
||||
businessMetrics.inventory.lowStock(data.lowStock),
|
||||
businessMetrics.inventory.outOfStock(data.outOfStock),
|
||||
],
|
||||
};
|
||||
4
frontend/src/components/ui/Stats/index.ts
Normal file
4
frontend/src/components/ui/Stats/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as StatsCard } from './StatsCard';
|
||||
export { default as StatsGrid } from './StatsGrid';
|
||||
export type { StatsCardProps, StatsCardVariant, StatsCardSize } from './StatsCard';
|
||||
export type { StatsGridProps } from './StatsGrid';
|
||||
@@ -13,6 +13,7 @@ export { ThemeToggle } from './ThemeToggle';
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { StatusIndicator } from './StatusIndicator';
|
||||
export { ListItem } from './ListItem';
|
||||
export { StatsCard, StatsGrid } from './Stats';
|
||||
|
||||
// Export types
|
||||
export type { ButtonProps } from './Button';
|
||||
@@ -28,4 +29,5 @@ export type { DatePickerProps } from './DatePicker';
|
||||
export type { ThemeToggleProps } from './ThemeToggle';
|
||||
export type { ProgressBarProps } from './ProgressBar';
|
||||
export type { StatusIndicatorProps } from './StatusIndicator';
|
||||
export type { ListItemProps } from './ListItem';
|
||||
export type { ListItemProps } from './ListItem';
|
||||
export type { StatsCardProps, StatsCardVariant, StatsCardSize, StatsGridProps } from './Stats';
|
||||
Reference in New Issue
Block a user