Improve the frontend 5

This commit is contained in:
Urtzi Alfaro
2025-11-02 20:24:44 +01:00
parent 0220da1725
commit 5adb0e39c0
90 changed files with 10658 additions and 2548 deletions

View File

@@ -0,0 +1,120 @@
import React from 'react';
import { clsx } from 'clsx';
import { Card } from '../ui';
export interface AnalyticsCardProps {
/**
* Card title
*/
title?: string;
/**
* Card subtitle/description
*/
subtitle?: string;
/**
* Card content
*/
children: React.ReactNode;
/**
* Custom className
*/
className?: string;
/**
* Action buttons for card header
*/
actions?: React.ReactNode;
/**
* Loading state
*/
loading?: boolean;
/**
* Empty state message
*/
emptyMessage?: string;
/**
* Whether the card has data
*/
isEmpty?: boolean;
}
/**
* AnalyticsCard - Preset Card component for analytics pages
*
* Provides consistent styling and structure for analytics content cards:
* - Standard padding (p-6)
* - Rounded corners (rounded-lg)
* - Title with consistent styling (text-lg font-semibold mb-4)
* - Optional subtitle
* - Optional header actions
* - Loading state support
* - Empty state support
*/
export const AnalyticsCard: React.FC<AnalyticsCardProps> = ({
title,
subtitle,
children,
className,
actions,
loading = false,
emptyMessage,
isEmpty = false,
}) => {
return (
<Card className={clsx('p-6', className)}>
{/* Card Header */}
{(title || subtitle || actions) && (
<div className="mb-4">
{/* Title Row */}
{(title || actions) && (
<div className="flex items-center justify-between mb-2">
{title && (
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
{title}
</h3>
)}
{actions && <div className="flex items-center gap-2">{actions}</div>}
</div>
)}
{/* Subtitle */}
{subtitle && (
<p className="text-sm text-[var(--text-secondary)]">{subtitle}</p>
)}
</div>
)}
{/* Loading State */}
{loading && (
<div className="flex flex-col items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)] mb-3"></div>
<p className="text-sm text-[var(--text-secondary)]">Cargando datos...</p>
</div>
)}
{/* Empty State */}
{!loading && isEmpty && (
<div className="flex flex-col items-center justify-center py-12">
<svg
className="w-12 h-12 text-[var(--text-tertiary)] mb-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<p className="text-sm text-[var(--text-secondary)]">
{emptyMessage || 'No hay datos disponibles'}
</p>
</div>
)}
{/* Card Content */}
{!loading && !isEmpty && children}
</Card>
);
};

View File

@@ -0,0 +1,219 @@
import React from 'react';
import { Lock } from 'lucide-react';
import { clsx } from 'clsx';
import { PageHeader } from '../layout';
import { Card, Button, StatsGrid, Tabs } from '../ui';
import { ActionButton } from '../layout/PageHeader/PageHeader';
export interface AnalyticsPageLayoutProps {
/**
* Page title
*/
title: string;
/**
* Page description
*/
description: string;
/**
* Action buttons for the page header
*/
actions?: ActionButton[];
/**
* Key metrics to display in stats grid
*/
stats?: Array<{
title: string;
value: number | string;
variant?: 'success' | 'error' | 'warning' | 'info';
icon?: React.ComponentType<{ className?: string }>;
subtitle?: string;
formatter?: (value: any) => string;
}>;
/**
* Number of columns for stats grid (4 or 6)
*/
statsColumns?: 4 | 6;
/**
* Tab configuration
*/
tabs?: Array<{
id: string;
label: string;
icon?: React.ComponentType<{ className?: string }>;
}>;
/**
* Active tab ID
*/
activeTab?: string;
/**
* Tab change handler
*/
onTabChange?: (tabId: string) => void;
/**
* Optional filters/controls section
*/
filters?: React.ReactNode;
/**
* Loading state for subscription check
*/
subscriptionLoading?: boolean;
/**
* Whether user has access to advanced analytics
*/
hasAccess?: boolean;
/**
* Loading state for data
*/
dataLoading?: boolean;
/**
* Main content (tab content)
*/
children: React.ReactNode;
/**
* Custom className for container
*/
className?: string;
/**
* Show mobile optimization notice
*/
showMobileNotice?: boolean;
/**
* Custom mobile notice text
*/
mobileNoticeText?: string;
}
/**
* AnalyticsPageLayout - Standardized layout for analytics pages
*
* Provides consistent structure across all analytics pages:
* 1. Page header with title, description, and actions
* 2. Optional filters/controls section
* 3. Key metrics (StatsGrid with 4 or 6 metrics)
* 4. Tab navigation
* 5. Tab content area
* 6. Subscription checks and access control
* 7. Loading states
*/
export const AnalyticsPageLayout: React.FC<AnalyticsPageLayoutProps> = ({
title,
description,
actions,
stats,
statsColumns = 4,
tabs,
activeTab,
onTabChange,
filters,
subscriptionLoading = false,
hasAccess = true,
dataLoading = false,
children,
className,
showMobileNotice = false,
mobileNoticeText,
}) => {
// Show loading state while subscription data is being fetched
if (subscriptionLoading) {
return (
<div className="space-y-6">
<PageHeader title={title} description={description} />
<Card className="p-8 text-center">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--color-primary)]"></div>
<p className="text-[var(--text-secondary)]">
Cargando información de suscripción...
</p>
</div>
</Card>
</div>
);
}
// If user doesn't have access to advanced analytics, show upgrade message
if (!hasAccess) {
return (
<div className="space-y-6">
<PageHeader title={title} description={description} />
<Card className="p-8 text-center">
<Lock className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
Funcionalidad Exclusiva para Profesionales y Empresas
</h3>
<p className="text-[var(--text-secondary)] mb-4">
El análisis avanzado está disponible solo para planes Professional y Enterprise.
Actualiza tu plan para acceder a métricas avanzadas, análisis detallados y optimización operativa.
</p>
<Button
variant="primary"
size="md"
onClick={() => (window.location.hash = '#/app/settings/profile')}
>
Actualizar Plan
</Button>
</Card>
</div>
);
}
return (
<div className={clsx('space-y-6', className)}>
{/* Page Header */}
<PageHeader title={title} description={description} actions={actions} />
{/* Optional Filters/Controls */}
{filters && <Card className="p-6">{filters}</Card>}
{/* Key Metrics - StatsGrid */}
{stats && stats.length > 0 && (
<StatsGrid
stats={stats}
columns={statsColumns}
loading={dataLoading}
/>
)}
{/* Tabs Navigation */}
{tabs && tabs.length > 0 && activeTab && onTabChange && (
<Tabs
items={tabs.map((tab) => ({ id: tab.id, label: tab.label }))}
activeTab={activeTab}
onTabChange={onTabChange}
/>
)}
{/* Main Content (Tab Content) */}
<div className="space-y-6">{children}</div>
{/* Mobile Optimization Notice */}
{showMobileNotice && (
<div className="md:hidden p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="flex items-start space-x-3">
<svg
className="w-5 h-5 mt-0.5 text-blue-600 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<p className="text-sm font-medium text-blue-600">
Experiencia Optimizada para Móvil
</p>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{mobileNoticeText ||
'Desliza, desplázate e interactúa con los gráficos para explorar los datos.'}
</p>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { Badge } from '../../ui';
import { Plus, Edit, Trash2, Check, X, Eye, RefreshCw } from 'lucide-react';
interface ActionBadgeProps {
action: string;
showIcon?: boolean;
}
export const ActionBadge: React.FC<ActionBadgeProps> = ({ action, showIcon = true }) => {
const actionConfig: Record<string, { label: string; color: 'green' | 'blue' | 'red' | 'purple' | 'orange' | 'gray'; icon: any }> = {
create: {
label: 'Crear',
color: 'green',
icon: Plus,
},
update: {
label: 'Actualizar',
color: 'blue',
icon: Edit,
},
delete: {
label: 'Eliminar',
color: 'red',
icon: Trash2,
},
approve: {
label: 'Aprobar',
color: 'green',
icon: Check,
},
reject: {
label: 'Rechazar',
color: 'red',
icon: X,
},
view: {
label: 'Ver',
color: 'gray',
icon: Eye,
},
sync: {
label: 'Sincronizar',
color: 'purple',
icon: RefreshCw,
},
};
const config = actionConfig[action.toLowerCase()] || {
label: action,
color: 'gray' as const,
icon: RefreshCw,
};
const { label, color, icon: Icon } = config;
return (
<Badge color={color}>
{showIcon && <Icon className="mr-1 h-3 w-3" />}
{label}
</Badge>
);
};

View File

@@ -0,0 +1,194 @@
import React from 'react';
import { X, Copy, Download, User, Clock, Globe, Terminal } from 'lucide-react';
import { Button, Card, Badge } from '../../ui';
import { AggregatedAuditLog } from '../../../api/types/auditLogs';
import { SeverityBadge } from './SeverityBadge';
import { ServiceBadge } from './ServiceBadge';
import { ActionBadge } from './ActionBadge';
interface EventDetailModalProps {
event: AggregatedAuditLog;
onClose: () => void;
}
export const EventDetailModal: React.FC<EventDetailModalProps> = ({ event, onClose }) => {
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const exportEvent = () => {
const json = JSON.stringify(event, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `event-${event.id}.json`;
link.click();
URL.revokeObjectURL(url);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4">
<Card className="max-h-[90vh] w-full max-w-4xl overflow-y-auto">
{/* Header */}
<div className="border-b border-gray-200 p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="mb-2 flex items-center gap-2">
<h2 className="text-2xl font-bold text-gray-900">Detalle del Evento</h2>
<ServiceBadge service={event.service_name} />
<SeverityBadge severity={event.severity} />
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{new Date(event.created_at).toLocaleString()}
</span>
{event.user_id && (
<span className="flex items-center gap-1">
<User className="h-4 w-4" />
{event.user_id}
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(event.id)}
icon={Copy}
title="Copiar ID"
/>
<Button
variant="ghost"
size="sm"
onClick={exportEvent}
icon={Download}
title="Exportar evento"
/>
<Button
variant="ghost"
size="sm"
onClick={onClose}
icon={X}
/>
</div>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Event Information */}
<div>
<h3 className="mb-3 text-lg font-semibold text-gray-900">Información del Evento</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-600">Acción</label>
<div className="mt-1">
<ActionBadge action={event.action} />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Tipo de Recurso</label>
<p className="mt-1 text-sm text-gray-900">{event.resource_type}</p>
</div>
{event.resource_id && (
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-600">ID de Recurso</label>
<p className="mt-1 font-mono text-sm text-gray-900">{event.resource_id}</p>
</div>
)}
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-600">Descripción</label>
<p className="mt-1 text-sm text-gray-900">{event.description}</p>
</div>
</div>
</div>
{/* Changes */}
{event.changes && Object.keys(event.changes).length > 0 && (
<div>
<h3 className="mb-3 text-lg font-semibold text-gray-900">Cambios</h3>
<div className="rounded-lg bg-gray-50 p-4">
<pre className="overflow-x-auto text-sm text-gray-900">
{JSON.stringify(event.changes, null, 2)}
</pre>
</div>
</div>
)}
{/* Request Metadata */}
<div>
<h3 className="mb-3 text-lg font-semibold text-gray-900">Metadatos de Solicitud</h3>
<div className="grid grid-cols-2 gap-4">
{event.endpoint && (
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-600">Endpoint</label>
<p className="mt-1 font-mono text-sm text-gray-900">{event.endpoint}</p>
</div>
)}
{event.method && (
<div>
<label className="block text-sm font-medium text-gray-600">Método HTTP</label>
<Badge color="blue">{event.method}</Badge>
</div>
)}
{event.ip_address && (
<div>
<label className="block text-sm font-medium text-gray-600">Dirección IP</label>
<p className="mt-1 text-sm text-gray-900">{event.ip_address}</p>
</div>
)}
{event.user_agent && (
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-600">User Agent</label>
<p className="mt-1 text-sm text-gray-700 break-all">{event.user_agent}</p>
</div>
)}
</div>
</div>
{/* Additional Metadata */}
{event.audit_metadata && Object.keys(event.audit_metadata).length > 0 && (
<div>
<h3 className="mb-3 text-lg font-semibold text-gray-900">Metadatos Adicionales</h3>
<div className="rounded-lg bg-gray-50 p-4">
<pre className="overflow-x-auto text-sm text-gray-900">
{JSON.stringify(event.audit_metadata, null, 2)}
</pre>
</div>
</div>
)}
{/* Event ID */}
<div className="border-t border-gray-200 pt-4">
<label className="block text-sm font-medium text-gray-600">ID del Evento</label>
<div className="mt-1 flex items-center gap-2">
<code className="flex-1 rounded bg-gray-100 px-3 py-2 font-mono text-sm text-gray-900">
{event.id}
</code>
<Button
variant="secondary"
size="sm"
onClick={() => copyToClipboard(event.id)}
icon={Copy}
>
Copiar
</Button>
</div>
</div>
</div>
{/* Footer */}
<div className="border-t border-gray-200 p-6">
<div className="flex justify-end gap-3">
<Button variant="secondary" onClick={onClose}>
Cerrar
</Button>
</div>
</div>
</Card>
</div>
);
};

View File

@@ -0,0 +1,174 @@
import React, { useState } from 'react';
import { Card, Button } from '../../ui';
import { Calendar, User, Filter as FilterIcon, X } from 'lucide-react';
import { AuditLogFilters, AuditLogStatsResponse, AUDIT_LOG_SERVICES } from '../../../api/types/auditLogs';
interface EventFilterSidebarProps {
filters: AuditLogFilters;
onFiltersChange: (filters: Partial<AuditLogFilters>) => void;
stats?: AuditLogStatsResponse;
}
export const EventFilterSidebar: React.FC<EventFilterSidebarProps> = ({
filters,
onFiltersChange,
stats,
}) => {
const [localFilters, setLocalFilters] = useState<AuditLogFilters>(filters);
const handleApply = () => {
onFiltersChange(localFilters);
};
const handleClear = () => {
const clearedFilters: AuditLogFilters = { limit: 50, offset: 0 };
setLocalFilters(clearedFilters);
onFiltersChange(clearedFilters);
};
return (
<Card>
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Filtros</h3>
<Button
variant="ghost"
size="sm"
onClick={handleClear}
icon={X}
>
Limpiar
</Button>
</div>
<div className="space-y-6">
{/* Date Range */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">
Rango de Fechas
</label>
<div className="space-y-2">
<input
type="date"
value={localFilters.start_date?.split('T')[0] || ''}
onChange={(e) =>
setLocalFilters({ ...localFilters, start_date: e.target.value ? new Date(e.target.value).toISOString() : undefined })
}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
placeholder="Fecha inicio"
/>
<input
type="date"
value={localFilters.end_date?.split('T')[0] || ''}
onChange={(e) =>
setLocalFilters({ ...localFilters, end_date: e.target.value ? new Date(e.target.value).toISOString() : undefined })
}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
placeholder="Fecha fin"
/>
</div>
</div>
{/* Severity Filter */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">
Severidad
</label>
<select
value={localFilters.severity || ''}
onChange={(e) =>
setLocalFilters({ ...localFilters, severity: e.target.value as any || undefined })
}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
>
<option value="">Todas</option>
<option value="low">Bajo</option>
<option value="medium">Medio</option>
<option value="high">Alto</option>
<option value="critical">Crítico</option>
</select>
</div>
{/* Action Filter */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">
Acción
</label>
<input
type="text"
value={localFilters.action || ''}
onChange={(e) =>
setLocalFilters({ ...localFilters, action: e.target.value || undefined })
}
placeholder="create, update, delete..."
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
/>
</div>
{/* Resource Type Filter */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">
Tipo de Recurso
</label>
<input
type="text"
value={localFilters.resource_type || ''}
onChange={(e) =>
setLocalFilters({ ...localFilters, resource_type: e.target.value || undefined })
}
placeholder="user, recipe, order..."
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
/>
</div>
{/* Search */}
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">
Buscar en Descripción
</label>
<input
type="text"
value={localFilters.search || ''}
onChange={(e) =>
setLocalFilters({ ...localFilters, search: e.target.value || undefined })
}
placeholder="Buscar..."
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
/>
</div>
{/* Apply Button */}
<Button
variant="primary"
className="w-full"
onClick={handleApply}
icon={FilterIcon}
>
Aplicar Filtros
</Button>
</div>
{/* Stats Summary */}
{stats && (
<div className="mt-6 border-t border-gray-200 pt-4">
<h4 className="mb-3 text-sm font-medium text-gray-700">Resumen</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Total Eventos:</span>
<span className="font-semibold text-gray-900">{stats.total_events}</span>
</div>
{stats.events_by_severity && Object.keys(stats.events_by_severity).length > 0 && (
<div className="flex justify-between">
<span className="text-gray-600">Críticos:</span>
<span className="font-semibold text-red-600">
{stats.events_by_severity.critical || 0}
</span>
</div>
)}
</div>
</div>
)}
</div>
</Card>
);
};

View File

@@ -0,0 +1,104 @@
import React from 'react';
import { Card } from '../../ui';
import { Activity, AlertTriangle, TrendingUp, Clock } from 'lucide-react';
import { AuditLogStatsResponse } from '../../../api/types/auditLogs';
interface EventStatsWidgetProps {
stats: AuditLogStatsResponse;
}
export const EventStatsWidget: React.FC<EventStatsWidgetProps> = ({ stats }) => {
const criticalCount = stats.events_by_severity?.critical || 0;
const highCount = stats.events_by_severity?.high || 0;
const todayCount = stats.total_events; // Simplified - would need date filtering for actual "today"
// Find most common action
const mostCommonAction = Object.entries(stats.events_by_action || {})
.sort(([, a], [, b]) => b - a)[0]?.[0] || 'N/A';
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Total Events */}
<Card>
<div className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total de Eventos</p>
<p className="mt-2 text-3xl font-bold text-gray-900">{stats.total_events}</p>
</div>
<div className="rounded-full bg-blue-100 p-3">
<Activity className="h-6 w-6 text-blue-600" />
</div>
</div>
</div>
</Card>
{/* Critical Events */}
<Card>
<div className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Eventos Críticos</p>
<p className="mt-2 text-3xl font-bold text-red-600">{criticalCount}</p>
{highCount > 0 && (
<p className="mt-1 text-xs text-gray-500">+{highCount} de alta prioridad</p>
)}
</div>
<div className="rounded-full bg-red-100 p-3">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
</div>
</div>
</Card>
{/* Most Common Action */}
<Card>
<div className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Acción Más Común</p>
<p className="mt-2 text-2xl font-bold text-gray-900 capitalize">
{mostCommonAction}
</p>
{stats.events_by_action && stats.events_by_action[mostCommonAction] && (
<p className="mt-1 text-xs text-gray-500">
{stats.events_by_action[mostCommonAction]} veces
</p>
)}
</div>
<div className="rounded-full bg-green-100 p-3">
<TrendingUp className="h-6 w-6 text-green-600" />
</div>
</div>
</div>
</Card>
{/* Date Range */}
<Card>
<div className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Período</p>
{stats.date_range.min && stats.date_range.max ? (
<>
<p className="mt-2 text-sm font-semibold text-gray-900">
{new Date(stats.date_range.min).toLocaleDateString()}
</p>
<p className="text-sm text-gray-600">hasta</p>
<p className="text-sm font-semibold text-gray-900">
{new Date(stats.date_range.max).toLocaleDateString()}
</p>
</>
) : (
<p className="mt-2 text-sm text-gray-500">Sin datos</p>
)}
</div>
<div className="rounded-full bg-purple-100 p-3">
<Clock className="h-6 w-6 text-purple-600" />
</div>
</div>
</div>
</Card>
</div>
);
};

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { Badge } from '../../ui';
import {
ShoppingCart,
Package,
ClipboardList,
Factory,
ChefHat,
Truck,
CreditCard,
Brain,
Bell,
Cloud,
TrendingUp,
} from 'lucide-react';
interface ServiceBadgeProps {
service: string;
showIcon?: boolean;
}
export const ServiceBadge: React.FC<ServiceBadgeProps> = ({ service, showIcon = true }) => {
const serviceConfig: Record<string, { label: string; color: 'blue' | 'green' | 'purple' | 'orange' | 'pink' | 'indigo' | 'teal' | 'cyan' | 'amber'; icon: any }> = {
sales: {
label: 'Ventas',
color: 'blue',
icon: ShoppingCart,
},
inventory: {
label: 'Inventario',
color: 'green',
icon: Package,
},
orders: {
label: 'Pedidos',
color: 'purple',
icon: ClipboardList,
},
production: {
label: 'Producción',
color: 'orange',
icon: Factory,
},
recipes: {
label: 'Recetas',
color: 'pink',
icon: ChefHat,
},
suppliers: {
label: 'Proveedores',
color: 'indigo',
icon: Truck,
},
pos: {
label: 'POS',
color: 'teal',
icon: CreditCard,
},
training: {
label: 'Entrenamiento',
color: 'cyan',
icon: Brain,
},
notification: {
label: 'Notificaciones',
color: 'amber',
icon: Bell,
},
external: {
label: 'Externo',
color: 'blue',
icon: Cloud,
},
forecasting: {
label: 'Pronósticos',
color: 'purple',
icon: TrendingUp,
},
};
const config = serviceConfig[service] || {
label: service,
color: 'gray' as const,
icon: Package,
};
const { label, color, icon: Icon } = config;
return (
<Badge color={color}>
{showIcon && <Icon className="mr-1 h-3 w-3" />}
{label}
</Badge>
);
};

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Badge } from '../../ui';
import { AlertTriangle, Info, AlertCircle, XCircle } from 'lucide-react';
interface SeverityBadgeProps {
severity: 'low' | 'medium' | 'high' | 'critical';
showIcon?: boolean;
}
export const SeverityBadge: React.FC<SeverityBadgeProps> = ({ severity, showIcon = true }) => {
const config = {
low: {
label: 'Bajo',
color: 'gray' as const,
icon: Info,
},
medium: {
label: 'Medio',
color: 'blue' as const,
icon: AlertCircle,
},
high: {
label: 'Alto',
color: 'orange' as const,
icon: AlertTriangle,
},
critical: {
label: 'Crítico',
color: 'red' as const,
icon: XCircle,
},
};
const { label, color, icon: Icon } = config[severity];
return (
<Badge color={color}>
{showIcon && <Icon className="mr-1 h-3 w-3" />}
{label}
</Badge>
);
};

View File

@@ -0,0 +1,6 @@
export { EventFilterSidebar } from './EventFilterSidebar';
export { EventDetailModal } from './EventDetailModal';
export { EventStatsWidget } from './EventStatsWidget';
export { SeverityBadge } from './SeverityBadge';
export { ServiceBadge } from './ServiceBadge';
export { ActionBadge } from './ActionBadge';

View File

@@ -0,0 +1,11 @@
/**
* Analytics Components
*
* Reusable components for building consistent analytics pages
*/
export { AnalyticsPageLayout } from './AnalyticsPageLayout';
export { AnalyticsCard } from './AnalyticsCard';
export type { AnalyticsPageLayoutProps } from './AnalyticsPageLayout';
export type { AnalyticsCardProps } from './AnalyticsCard';