Add a ne model and card design across pages

This commit is contained in:
Urtzi Alfaro
2025-08-31 10:46:13 +02:00
parent ab21149acf
commit a8b73e22ea
14 changed files with 1865 additions and 820 deletions

View File

@@ -125,6 +125,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set()); const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const sidebarRef = React.useRef<HTMLDivElement>(null); const sidebarRef = React.useRef<HTMLDivElement>(null);
// Get navigation routes from config and convert to navigation items - memoized // Get navigation routes from config and convert to navigation items - memoized
@@ -302,11 +303,59 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
}; };
}, [isOpen, onClose]); }, [isOpen, onClose]);
// Render submenu overlay for collapsed sidebar
const renderSubmenuOverlay = (item: NavigationItem) => {
if (!item.children || item.children.length === 0) return null;
return (
<div className="fixed left-[var(--sidebar-collapsed-width)] top-0 z-[var(--z-popover)] min-w-[200px] bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-lg py-2">
<div className="px-3 py-2 border-b border-[var(--border-primary)]">
<span className="text-sm font-medium text-[var(--text-secondary)]">
{item.label}
</span>
</div>
<ul className="py-1">
{item.children.map(child => (
<li key={child.id}>
<button
onClick={() => handleItemClick(child)}
disabled={child.disabled}
className={clsx(
'w-full text-left px-3 py-2 text-sm transition-colors duration-200',
'hover:bg-[var(--bg-secondary)]',
location.pathname === child.path && 'bg-[var(--color-primary)]/10 text-[var(--color-primary)] border-r-2 border-[var(--color-primary)]',
child.disabled && 'opacity-50 cursor-not-allowed'
)}
>
<div className="flex items-center">
{child.icon && (
<child.icon className="w-4 h-4 mr-3 flex-shrink-0" />
)}
<span className="truncate">{child.label}</span>
{child.badge && (
<Badge
variant={child.badge.variant || 'default'}
size="sm"
className="ml-2 text-xs"
>
{child.badge.text}
</Badge>
)}
</div>
</button>
</li>
))}
</ul>
</div>
);
};
// Render navigation item // Render navigation item
const renderItem = (item: NavigationItem, level = 0) => { const renderItem = (item: NavigationItem, level = 0) => {
const isActive = location.pathname === item.path || location.pathname.startsWith(item.path + '/'); const isActive = location.pathname === item.path || location.pathname.startsWith(item.path + '/');
const isExpanded = expandedItems.has(item.id); const isExpanded = expandedItems.has(item.id);
const hasChildren = item.children && item.children.length > 0; const hasChildren = item.children && item.children.length > 0;
const isHovered = hoveredItem === item.id;
const ItemIcon = item.icon; const ItemIcon = item.icon;
const itemContent = ( const itemContent = (
@@ -317,17 +366,24 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
level > 0 && 'pl-6', level > 0 && 'pl-6',
)} )}
> >
{ItemIcon && ( <div className="relative">
<ItemIcon {ItemIcon && (
className={clsx( <ItemIcon
'flex-shrink-0 transition-colors duration-200', className={clsx(
isCollapsed ? 'w-5 h-5' : 'w-4 h-4 mr-3', 'flex-shrink-0 transition-colors duration-200',
isActive isCollapsed ? 'w-5 h-5' : 'w-4 h-4 mr-3',
? 'text-[var(--color-primary)]' isActive
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]' ? 'text-[var(--color-primary)]'
)} : 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
/> )}
)} />
)}
{/* Submenu indicator for collapsed sidebar */}
{isCollapsed && hasChildren && level === 0 && (
<div className="absolute -bottom-1 -right-1 w-2 h-2 bg-[var(--color-primary)] rounded-full opacity-75" />
)}
</div>
{!ItemIcon && level > 0 && ( {!ItemIcon && level > 0 && (
<Dot className={clsx( <Dot className={clsx(
@@ -374,24 +430,47 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
); );
const button = ( const button = (
<button <div className="relative">
onClick={() => handleItemClick(item)} <button
disabled={item.disabled} onClick={() => handleItemClick(item)}
data-path={item.path} disabled={item.disabled}
className={clsx( data-path={item.path}
'w-full rounded-lg transition-all duration-200', onMouseEnter={() => {
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20', if (isCollapsed && hasChildren && level === 0) {
isActive && 'bg-[var(--color-primary)]/10 border-l-2 border-[var(--color-primary)]', setHoveredItem(item.id);
!isActive && 'hover:bg-[var(--bg-secondary)]', }
item.disabled && 'opacity-50 cursor-not-allowed', }}
isCollapsed && !hasChildren ? 'flex justify-center items-center p-2 mx-1' : 'p-3' onMouseLeave={() => {
if (isCollapsed && hasChildren && level === 0) {
setHoveredItem(null);
}
}}
className={clsx(
'w-full rounded-lg transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20',
isActive && 'bg-[var(--color-primary)]/10 border-l-2 border-[var(--color-primary)]',
!isActive && 'hover:bg-[var(--bg-secondary)]',
item.disabled && 'opacity-50 cursor-not-allowed',
isCollapsed && !hasChildren ? 'flex justify-center items-center p-2 mx-1' : 'p-3'
)}
aria-expanded={hasChildren ? isExpanded : undefined}
aria-current={isActive ? 'page' : undefined}
title={isCollapsed ? item.label : undefined}
>
{itemContent}
</button>
{/* Submenu overlay for collapsed sidebar */}
{isCollapsed && hasChildren && level === 0 && isHovered && (
<div
className="absolute left-full top-0 ml-2 z-[var(--z-popover)]"
onMouseEnter={() => setHoveredItem(item.id)}
onMouseLeave={() => setHoveredItem(null)}
>
{renderSubmenuOverlay(item)}
</div>
)} )}
aria-expanded={hasChildren ? isExpanded : undefined} </div>
aria-current={isActive ? 'page' : undefined}
title={isCollapsed ? item.label : undefined}
>
{itemContent}
</button>
); );
return ( return (

View File

@@ -116,38 +116,38 @@ const Modal = forwardRef<HTMLDivElement, ModalProps>(({
if (!isOpen) return null; if (!isOpen) return null;
const sizeClasses = { const sizeClasses = {
xs: 'max-w-xs', xs: 'max-w-xs w-full',
sm: 'max-w-sm', sm: 'max-w-sm w-full',
md: 'max-w-md', md: 'max-w-md w-full',
lg: 'max-w-lg', lg: 'max-w-lg w-full max-h-[90vh] overflow-y-auto',
xl: 'max-w-xl', xl: 'max-w-xl w-full max-h-[90vh] overflow-y-auto',
'2xl': 'max-w-2xl', '2xl': 'max-w-2xl w-full max-h-[90vh] overflow-y-auto',
full: 'max-w-full', full: 'max-w-full w-full max-h-[90vh] overflow-y-auto',
}; };
const variantClasses = { const variantClasses = {
default: { default: {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex items-center justify-center p-4', overlay: 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4',
content: 'w-full transform transition-all duration-300 ease-out scale-100 opacity-100', content: 'w-full transform transition-all duration-300 ease-out scale-100 opacity-100',
}, },
centered: { centered: {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex items-center justify-center p-4', overlay: 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4',
content: 'w-full transform transition-all duration-300 ease-out scale-100 opacity-100', content: 'w-full transform transition-all duration-300 ease-out scale-100 opacity-100',
}, },
'drawer-left': { 'drawer-left': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex justify-start', overlay: 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex justify-start',
content: 'h-full w-full max-w-md transform transition-all duration-300 ease-out translate-x-0', content: 'h-full w-full max-w-md transform transition-all duration-300 ease-out translate-x-0',
}, },
'drawer-right': { 'drawer-right': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex justify-end', overlay: 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex justify-end',
content: 'h-full w-full max-w-md transform transition-all duration-300 ease-out translate-x-0', content: 'h-full w-full max-w-md transform transition-all duration-300 ease-out translate-x-0',
}, },
'drawer-top': { 'drawer-top': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex flex-col justify-start', overlay: 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex flex-col justify-start',
content: 'w-full transform transition-all duration-300 ease-out translate-y-0', content: 'w-full transform transition-all duration-300 ease-out translate-y-0',
}, },
'drawer-bottom': { 'drawer-bottom': {
overlay: 'fixed inset-0 bg-modal-backdrop backdrop-blur-sm z-50 flex flex-col justify-end', overlay: 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex flex-col justify-end',
content: 'w-full transform transition-all duration-300 ease-out translate-y-0', content: 'w-full transform transition-all duration-300 ease-out translate-y-0',
}, },
}; };
@@ -159,7 +159,7 @@ const Modal = forwardRef<HTMLDivElement, ModalProps>(({
); );
const contentClasses = clsx( const contentClasses = clsx(
'relative bg-modal-bg border border-modal-border rounded-lg shadow-xl', 'relative bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-xl',
'animate-in zoom-in-95 duration-200', 'animate-in zoom-in-95 duration-200',
variantClasses[variant].content, variantClasses[variant].content,
sizeClasses[size], sizeClasses[size],
@@ -192,7 +192,7 @@ const Modal = forwardRef<HTMLDivElement, ModalProps>(({
{showCloseButton && ( {showCloseButton && (
<button <button
type="button" type="button"
className="absolute top-4 right-4 text-text-tertiary hover:text-text-primary transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-color-primary/20 rounded-md p-1" className="absolute top-4 right-4 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 rounded-md p-1"
onClick={onClose} onClick={onClose}
aria-label="Cerrar modal" aria-label="Cerrar modal"
> >
@@ -225,7 +225,7 @@ const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(({
...props ...props
}, ref) => { }, ref) => {
const classes = clsx( const classes = clsx(
'px-6 py-4 border-b border-border-primary', 'px-6 py-4 border-b border-[var(--border-primary)]',
className className
); );
@@ -240,13 +240,13 @@ const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(({
{title && ( {title && (
<h2 <h2
id="modal-title" id="modal-title"
className="text-lg font-semibold text-text-primary" className="text-lg font-semibold text-[var(--text-primary)]"
> >
{title} {title}
</h2> </h2>
)} )}
{subtitle && ( {subtitle && (
<p className="mt-1 text-sm text-text-secondary"> <p className="mt-1 text-sm text-[var(--text-secondary)]">
{subtitle} {subtitle}
</p> </p>
)} )}
@@ -255,7 +255,7 @@ const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(({
{showCloseButton && onClose && ( {showCloseButton && onClose && (
<button <button
type="button" type="button"
className="ml-4 text-text-tertiary hover:text-text-primary transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-color-primary/20 rounded-md p-1" className="ml-4 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 rounded-md p-1"
onClick={onClose} onClick={onClose}
aria-label="Cerrar modal" aria-label="Cerrar modal"
> >
@@ -333,7 +333,7 @@ const ModalFooter = forwardRef<HTMLDivElement, ModalFooterProps>(({
}; };
const classes = clsx( const classes = clsx(
'flex items-center gap-3 border-t border-border-primary', 'flex items-center gap-3 border-t border-[var(--border-primary)]',
paddingClasses[padding], paddingClasses[padding],
justifyClasses[justify], justifyClasses[justify],
className className

View File

@@ -0,0 +1,226 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Card } from '../Card';
import { Button } from '../Button';
import { statusColors } from '../../../styles/colors';
export interface StatusIndicatorConfig {
color: string;
text: string;
icon?: LucideIcon;
isCritical?: boolean;
isHighlight?: boolean;
}
export interface StatusCardProps {
id: string;
statusIndicator: StatusIndicatorConfig;
title: string;
subtitle?: string;
primaryValue: string | number;
primaryValueLabel?: string;
secondaryInfo?: {
label: string;
value: string;
};
progress?: {
label: string;
percentage: number;
color?: string;
};
metadata?: string[];
actions?: Array<{
label: string;
icon?: LucideIcon;
variant?: 'primary' | 'outline';
onClick: () => void;
}>;
onClick?: () => void;
className?: string;
}
/**
* Get status color configuration from the global palette
*/
export const getStatusColor = (status: string): StatusIndicatorConfig['color'] => {
// Normalize status key (handle different formats)
const normalizedStatus = status.toLowerCase().replace(/[_-]/g, '').replace(/\s+/g, '');
// Map common status variations to our palette
const statusMap: { [key: string]: keyof typeof statusColors } = {
'pending': 'pending',
'inprogress': 'inProgress',
'completed': 'completed',
'cancelled': 'cancelled',
'normal': 'normal',
'low': 'low',
'out': 'out',
'expired': 'expired',
'approved': 'approved',
'intransit': 'inTransit',
'delivered': 'delivered',
'bread': 'bread',
'pastry': 'pastry',
'cake': 'cake',
'cookie': 'cookie',
'other': 'other',
};
const mappedStatus = statusMap[normalizedStatus];
return mappedStatus ? statusColors[mappedStatus].primary : statusColors.other.primary;
};
/**
* StatusCard - Reusable card component with consistent status styling
*/
export const StatusCard: React.FC<StatusCardProps> = ({
id,
statusIndicator,
title,
subtitle,
primaryValue,
primaryValueLabel,
secondaryInfo,
progress,
metadata = [],
actions = [],
onClick,
className = '',
}) => {
const StatusIcon = statusIndicator.icon;
const hasInteraction = onClick || actions.length > 0;
return (
<Card
className={`
p-5 transition-all duration-200 border-l-4
${hasInteraction ? 'hover:shadow-md cursor-pointer' : ''}
${className}
`}
style={{
borderLeftColor: statusIndicator.color,
backgroundColor: statusIndicator.isCritical
? `${statusIndicator.color}08`
: statusIndicator.isHighlight
? `${statusIndicator.color}05`
: undefined
}}
onClick={onClick}
>
<div className="space-y-4">
{/* Header with status indicator */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="flex-shrink-0 p-2 rounded-lg"
style={{ backgroundColor: `${statusIndicator.color}15` }}
>
{StatusIcon && (
<StatusIcon
className="w-4 h-4"
style={{ color: statusIndicator.color }}
/>
)}
</div>
<div>
<div className="font-semibold text-[var(--text-primary)] text-base">
{title}
</div>
<div
className="text-sm font-medium"
style={{ color: statusIndicator.color }}
>
{statusIndicator.text}
{statusIndicator.isCritical && (
<span className="ml-2 text-xs"></span>
)}
{statusIndicator.isHighlight && (
<span className="ml-2 text-xs"></span>
)}
</div>
{subtitle && (
<div className="text-xs text-[var(--text-secondary)]">
{subtitle}
</div>
)}
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-[var(--text-primary)]">
{primaryValue}
</div>
{primaryValueLabel && (
<div className="text-xs text-[var(--text-tertiary)] uppercase tracking-wide">
{primaryValueLabel}
</div>
)}
</div>
</div>
{/* Secondary info */}
{secondaryInfo && (
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">
{secondaryInfo.label}
</span>
<span className="font-medium text-[var(--text-primary)]">
{secondaryInfo.value}
</span>
</div>
)}
{/* Progress indicator */}
{progress && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-[var(--text-secondary)]">
{progress.label}
</span>
<span className="text-sm font-bold text-[var(--text-primary)]">
{progress.percentage}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className="h-2 rounded-full transition-all duration-300"
style={{
width: `${Math.min(progress.percentage, 100)}%`,
backgroundColor: progress.color || statusIndicator.color
}}
/>
</div>
</div>
)}
{/* Metadata */}
{metadata.length > 0 && (
<div className="text-xs text-[var(--text-secondary)] space-y-1">
{metadata.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
)}
{/* Actions */}
{actions.length > 0 && (
<div className="flex gap-2 pt-3 border-t border-[var(--border-primary)]">
{actions.map((action, index) => (
<Button
key={index}
variant={action.variant || 'outline'}
size="sm"
className="flex-1"
onClick={action.onClick}
>
{action.icon && <action.icon className="w-4 h-4 mr-2" />}
{action.label}
</Button>
))}
</div>
)}
</div>
</Card>
);
};
export default StatusCard;

View File

@@ -0,0 +1,2 @@
export { StatusCard, getStatusColor } from './StatusCard';
export type { StatusCardProps, StatusIndicatorConfig } from './StatusCard';

View File

@@ -0,0 +1,428 @@
import React from 'react';
import { LucideIcon, Edit, Eye, X } from 'lucide-react';
import Modal, { ModalHeader, ModalBody, ModalFooter } from '../Modal/Modal';
import { Button } from '../Button';
import { Input } from '../Input';
import { StatusIndicatorConfig, getStatusColor } from '../StatusCard';
import { formatters } from '../Stats/StatsPresets';
export interface StatusModalField {
label: string;
value: string | number | React.ReactNode;
type?: 'text' | 'currency' | 'date' | 'datetime' | 'percentage' | 'list' | 'status' | 'image' | 'email' | 'tel' | 'number';
highlight?: boolean;
span?: 1 | 2; // For grid layout
editable?: boolean; // Whether this field can be edited
required?: boolean; // Whether this field is required
placeholder?: string; // Placeholder text for inputs
options?: Array<{label: string; value: string | number}>; // For select fields
}
export interface StatusModalSection {
title: string;
icon?: LucideIcon;
fields: StatusModalField[];
}
export interface StatusModalAction {
label: string;
icon?: LucideIcon;
variant?: 'primary' | 'secondary' | 'outline' | 'danger';
onClick: () => void | Promise<void>;
disabled?: boolean;
loading?: boolean;
}
export interface StatusModalProps {
isOpen: boolean;
onClose: () => void;
mode: 'view' | 'edit';
onModeChange?: (mode: 'view' | 'edit') => void;
// Content
title: string;
subtitle?: string;
statusIndicator?: StatusIndicatorConfig;
image?: string;
sections: StatusModalSection[];
// Actions
actions?: StatusModalAction[];
showDefaultActions?: boolean;
onEdit?: () => void;
onSave?: () => Promise<void>;
onCancel?: () => void;
// Layout
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
loading?: boolean;
}
/**
* Format field value based on type
*/
const formatFieldValue = (value: string | number | React.ReactNode, type: StatusModalField['type'] = 'text'): React.ReactNode => {
if (React.isValidElement(value)) return value;
switch (type) {
case 'currency':
return formatters.currency(Number(value));
case 'date':
return new Date(String(value)).toLocaleDateString('es-ES');
case 'datetime':
return new Date(String(value)).toLocaleString('es-ES');
case 'percentage':
return `${value}%`;
case 'list':
if (Array.isArray(value)) {
return (
<ul className="list-disc list-inside space-y-1">
{value.map((item, index) => (
<li key={index} className="text-sm">{String(item)}</li>
))}
</ul>
);
}
return String(value);
case 'status':
return (
<span
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
style={{
backgroundColor: `${getStatusColor(String(value))}20`,
color: getStatusColor(String(value))
}}
>
{String(value)}
</span>
);
case 'image':
return (
<img
src={String(value)}
alt=""
className="w-16 h-16 rounded-lg object-cover bg-[var(--bg-secondary)]"
/>
);
default:
return String(value);
}
};
/**
* Render editable field based on type and mode
*/
const renderEditableField = (
field: StatusModalField,
isEditMode: boolean,
onChange?: (value: string | number) => void
): React.ReactNode => {
if (!isEditMode || !field.editable) {
return formatFieldValue(field.value, field.type);
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = field.type === 'number' || field.type === 'currency' ? Number(e.target.value) : e.target.value;
onChange?.(value);
};
const inputValue = field.type === 'currency' ? Number(String(field.value).replace(/[^0-9.-]+/g, '')) : field.value;
switch (field.type) {
case 'email':
return (
<Input
type="email"
value={String(inputValue)}
onChange={handleChange}
placeholder={field.placeholder}
required={field.required}
className="w-full"
/>
);
case 'tel':
return (
<Input
type="tel"
value={String(inputValue)}
onChange={handleChange}
placeholder={field.placeholder}
required={field.required}
className="w-full"
/>
);
case 'number':
case 'currency':
return (
<Input
type="number"
value={String(inputValue)}
onChange={handleChange}
placeholder={field.placeholder}
required={field.required}
step={field.type === 'currency' ? '0.01' : '1'}
className="w-full"
/>
);
case 'date':
const dateValue = field.value ? new Date(String(field.value)).toISOString().split('T')[0] : '';
return (
<Input
type="date"
value={dateValue}
onChange={handleChange}
required={field.required}
className="w-full"
/>
);
case 'list':
return (
<textarea
value={Array.isArray(field.value) ? field.value.join('\n') : String(field.value)}
onChange={(e) => onChange?.(e.target.value.split('\n'))}
placeholder={field.placeholder || 'Una opción por línea'}
required={field.required}
rows={4}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent"
/>
);
default:
return (
<Input
type="text"
value={String(inputValue)}
onChange={handleChange}
placeholder={field.placeholder}
required={field.required}
className="w-full"
/>
);
}
};
/**
* StatusModal - Unified modal component for viewing/editing card details
* Follows UX best practices for modal dialogs (2024)
*/
export const StatusModal: React.FC<StatusModalProps> = ({
isOpen,
onClose,
mode,
onModeChange,
title,
subtitle,
statusIndicator,
image,
sections,
actions = [],
showDefaultActions = true,
onEdit,
onSave,
onCancel,
size = 'lg',
loading = false,
}) => {
const StatusIcon = statusIndicator?.icon;
const handleEdit = () => {
if (onModeChange) {
onModeChange('edit');
} else if (onEdit) {
onEdit();
}
};
const handleSave = async () => {
if (onSave) {
await onSave();
}
if (onModeChange) {
onModeChange('view');
}
};
const handleCancel = () => {
if (onCancel) {
onCancel();
}
if (onModeChange) {
onModeChange('view');
}
};
// Default actions based on mode
const defaultActions: StatusModalAction[] = [];
if (showDefaultActions) {
if (mode === 'view') {
defaultActions.push({
label: 'Editar',
icon: Edit,
variant: 'primary',
onClick: handleEdit,
disabled: loading,
});
} else {
defaultActions.push(
{
label: 'Cancelar',
variant: 'outline',
onClick: handleCancel,
disabled: loading,
},
{
label: 'Guardar',
variant: 'primary',
onClick: handleSave,
disabled: loading,
loading: loading,
}
);
}
}
const allActions = [...actions, ...defaultActions];
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size={size}
closeOnOverlayClick={!loading}
closeOnEscape={!loading}
showCloseButton={false}
>
<ModalHeader
title={
<div className="flex items-center gap-3">
{/* Status indicator */}
{statusIndicator && (
<div
className="flex-shrink-0 p-2 rounded-lg"
style={{ backgroundColor: `${statusIndicator.color}15` }}
>
{StatusIcon && (
<StatusIcon
className="w-5 h-5"
style={{ color: statusIndicator.color }}
/>
)}
</div>
)}
{/* Title and status */}
<div>
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{title}
</h2>
{statusIndicator && (
<div
className="text-sm font-medium mt-1"
style={{ color: statusIndicator.color }}
>
{statusIndicator.text}
{statusIndicator.isCritical && (
<span className="ml-2 text-xs"></span>
)}
{statusIndicator.isHighlight && (
<span className="ml-2 text-xs"></span>
)}
</div>
)}
{subtitle && (
<p className="text-sm text-[var(--text-secondary)] mt-1">
{subtitle}
</p>
)}
</div>
</div>
}
showCloseButton={true}
onClose={onClose}
/>
<ModalBody>
{loading && (
<div className="absolute inset-0 bg-[var(--bg-primary)]/80 backdrop-blur-sm flex items-center justify-center z-10">
<div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[var(--color-primary)]"></div>
<span className="text-[var(--text-secondary)]">Cargando...</span>
</div>
</div>
)}
{image && (
<div className="mb-6">
<img
src={image}
alt={title}
className="w-full h-48 object-cover rounded-lg bg-[var(--bg-secondary)]"
/>
</div>
)}
<div className="space-y-6">
{sections.map((section, sectionIndex) => (
<div key={sectionIndex} className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-[var(--border-primary)]">
{section.icon && (
<section.icon className="w-4 h-4 text-[var(--text-tertiary)]" />
)}
<h3 className="font-medium text-[var(--text-primary)]">
{section.title}
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{section.fields.map((field, fieldIndex) => (
<div
key={fieldIndex}
className={`space-y-1 ${field.span === 2 ? 'md:col-span-2' : ''}`}
>
<dt className="text-sm font-medium text-[var(--text-secondary)]">
{field.label}
</dt>
<dd className={`text-sm ${
field.highlight
? 'font-semibold text-[var(--text-primary)]'
: 'text-[var(--text-primary)]'
}`}>
{renderEditableField(field, mode === 'edit')}
</dd>
</div>
))}
</div>
</div>
))}
</div>
</ModalBody>
{allActions.length > 0 && (
<ModalFooter justify="end">
<div className="flex gap-3">
{allActions.map((action, index) => (
<Button
key={index}
variant={action.variant || 'outline'}
onClick={action.onClick}
disabled={action.disabled || loading}
className="min-w-[80px]"
>
{action.loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
) : (
<>
{action.icon && <action.icon className="w-4 h-4 mr-2" />}
{action.label}
</>
)}
</Button>
))}
</div>
</ModalFooter>
)}
</Modal>
);
};
export default StatusModal;

View File

@@ -0,0 +1,7 @@
export { StatusModal } from './StatusModal';
export type {
StatusModalProps,
StatusModalField,
StatusModalSection,
StatusModalAction
} from './StatusModal';

View File

@@ -14,6 +14,8 @@ export { ProgressBar } from './ProgressBar';
export { StatusIndicator } from './StatusIndicator'; export { StatusIndicator } from './StatusIndicator';
export { ListItem } from './ListItem'; export { ListItem } from './ListItem';
export { StatsCard, StatsGrid } from './Stats'; export { StatsCard, StatsGrid } from './Stats';
export { StatusCard, getStatusColor } from './StatusCard';
export { StatusModal } from './StatusModal';
// Export types // Export types
export type { ButtonProps } from './Button'; export type { ButtonProps } from './Button';
@@ -30,4 +32,6 @@ export type { ThemeToggleProps } from './ThemeToggle';
export type { ProgressBarProps } from './ProgressBar'; export type { ProgressBarProps } from './ProgressBar';
export type { StatusIndicatorProps } from './StatusIndicator'; export type { StatusIndicatorProps } from './StatusIndicator';
export type { ListItemProps } from './ListItem'; export type { ListItemProps } from './ListItem';
export type { StatsCardProps, StatsCardVariant, StatsCardSize, StatsGridProps } from './Stats'; export type { StatsCardProps, StatsCardVariant, StatsCardSize, StatsGridProps } from './Stats';
export type { StatusCardProps, StatusIndicatorConfig } from './StatusCard';
export type { StatusModalProps, StatusModalField, StatusModalSection, StatusModalAction } from './StatusModal';

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Download, AlertTriangle, Package, Clock, CheckCircle, Eye, Edit, Calendar, DollarSign } from 'lucide-react'; import { Plus, Download, AlertTriangle, Package, Clock, CheckCircle, Eye, Edit, Calendar, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui'; import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
import { InventoryForm, LowStockAlert } from '../../../../components/domain/inventory'; import { InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
@@ -8,6 +8,7 @@ import { InventoryForm, LowStockAlert } from '../../../../components/domain/inve
const InventoryPage: React.FC = () => { const InventoryPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedItem, setSelectedItem] = useState<typeof mockInventoryItems[0] | null>(null); const [selectedItem, setSelectedItem] = useState<typeof mockInventoryItems[0] | null>(null);
const mockInventoryItems = [ const mockInventoryItems = [
@@ -83,65 +84,46 @@ const InventoryPage: React.FC = () => {
}, },
]; ];
const getStockStatusBadge = (item: typeof mockInventoryItems[0]) => { const getInventoryStatusConfig = (item: typeof mockInventoryItems[0]) => {
const { currentStock, minStock, status } = item; const { currentStock, minStock, status } = item;
if (status === 'expired') { if (status === 'expired') {
return ( return {
<Badge color: getStatusColor('expired'),
variant="error" text: 'Caducado',
icon={<AlertTriangle size={12} />} icon: AlertTriangle,
text="Caducado" isCritical: true,
/> isHighlight: false
); };
} }
if (currentStock === 0) { if (currentStock === 0) {
return ( return {
<Badge color: getStatusColor('out'),
variant="error" text: 'Sin Stock',
icon={<AlertTriangle size={12} />} icon: AlertTriangle,
text="Sin Stock" isCritical: true,
/> isHighlight: false
); };
} }
if (currentStock <= minStock) { if (currentStock <= minStock) {
return ( return {
<Badge color: getStatusColor('low'),
variant="warning" text: 'Stock Bajo',
icon={<AlertTriangle size={12} />} icon: AlertTriangle,
text="Stock Bajo" isCritical: false,
/> isHighlight: true
); };
} }
return ( return {
<Badge color: getStatusColor('normal'),
variant="success" text: 'Normal',
icon={<CheckCircle size={12} />} icon: CheckCircle,
text="Normal" isCritical: false,
/> isHighlight: false
);
};
const getCategoryBadge = (category: string) => {
const categoryConfig = {
'Harinas': { color: 'default' },
'Levaduras': { color: 'info' },
'Lácteos': { color: 'secondary' },
'Grasas': { color: 'warning' },
'Azúcares': { color: 'primary' },
'Especias': { color: 'success' },
}; };
const config = categoryConfig[category as keyof typeof categoryConfig] || { color: 'default' };
return (
<Badge
variant={config.color as any}
text={category}
/>
);
}; };
const filteredItems = mockInventoryItems.filter(item => { const filteredItems = mockInventoryItems.filter(item => {
@@ -258,132 +240,60 @@ const InventoryPage: React.FC = () => {
{/* Inventory Items Grid */} {/* Inventory Items Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredItems.map((item) => ( {filteredItems.map((item) => {
<Card key={item.id} className="p-4"> const statusConfig = getInventoryStatusConfig(item);
<div className="space-y-4"> const stockPercentage = Math.round((item.currentStock / item.maxStock) * 100);
{/* Header */} const isExpiringSoon = new Date(item.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
<div className="flex items-start justify-between"> const isExpired = new Date(item.expirationDate) < new Date();
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg"> return (
<Package className="w-4 h-4 text-[var(--text-tertiary)]" /> <StatusCard
</div> key={item.id}
<div> id={item.id}
<div className="font-medium text-[var(--text-primary)]"> statusIndicator={statusConfig}
{item.name} title={item.name}
</div> subtitle={`${item.category}${item.supplier}`}
<div className="text-sm text-[var(--text-secondary)]"> primaryValue={item.currentStock}
{item.supplier} primaryValueLabel={item.unit}
</div> secondaryInfo={{
</div> label: 'Valor total',
</div> value: `${formatters.currency(item.currentStock * item.cost)} (${formatters.currency(item.cost)}/${item.unit})`
{getStockStatusBadge(item)} }}
</div> progress={{
label: 'Nivel de stock',
{/* Category and Stock */} percentage: stockPercentage,
<div className="flex items-center justify-between"> color: statusConfig.color
<div> }}
{getCategoryBadge(item.category)} metadata={[
</div> `Rango: ${item.minStock} - ${item.maxStock} ${item.unit}`,
<div className="text-right"> `Caduca: ${new Date(item.expirationDate).toLocaleDateString('es-ES')}${isExpired ? ' (CADUCADO)' : isExpiringSoon ? ' (PRONTO)' : ''}`,
<div className="text-lg font-bold text-[var(--text-primary)]"> `Último restock: ${new Date(item.lastRestocked).toLocaleDateString('es-ES')}`
{item.currentStock} {item.unit} ]}
</div> actions={[
<div className="text-xs text-[var(--text-tertiary)]"> {
Mín: {item.minStock} | Máx: {item.maxStock} label: 'Ver',
</div> icon: Eye,
</div> variant: 'outline',
</div> onClick: () => {
{/* Value and Dates */}
<div className="flex items-center justify-between text-sm">
<div>
<div className="text-[var(--text-secondary)]">Costo unitario:</div>
<div className="font-medium text-[var(--text-primary)]">
{formatters.currency(item.cost)}
</div>
</div>
<div className="text-right">
<div className="text-[var(--text-secondary)]">Valor total:</div>
<div className="font-medium text-[var(--text-primary)]">
{formatters.currency(item.currentStock * item.cost)}
</div>
</div>
</div>
{/* Dates */}
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Último restock:</span>
<span className="text-[var(--text-primary)]">
{new Date(item.lastRestocked).toLocaleDateString('es-ES')}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Caducidad:</span>
<span className={`font-medium ${
new Date(item.expirationDate) < new Date()
? 'text-[var(--color-error)]'
: new Date(item.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
? 'text-[var(--color-warning)]'
: 'text-[var(--text-primary)]'
}`}>
{new Date(item.expirationDate).toLocaleDateString('es-ES')}
</span>
</div>
</div>
{/* Stock Level Progress */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Nivel de stock</span>
<span className="text-[var(--text-primary)]">
{Math.round((item.currentStock / item.maxStock) * 100)}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
item.currentStock <= item.minStock
? 'bg-[var(--color-error)]'
: item.currentStock <= item.minStock * 1.5
? 'bg-[var(--color-warning)]'
: 'bg-[var(--color-success)]'
}`}
style={{ width: `${Math.min((item.currentStock / item.maxStock) * 100, 100)}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedItem(item); setSelectedItem(item);
setModalMode('view');
setShowForm(true); setShowForm(true);
}} }
> },
<Eye className="w-4 h-4 mr-1" /> {
Ver label: 'Editar',
</Button> icon: Edit,
<Button variant: 'outline',
variant="outline" onClick: () => {
size="sm"
className="flex-1"
onClick={() => {
setSelectedItem(item); setSelectedItem(item);
setModalMode('edit');
setShowForm(true); setShowForm(true);
}} }
> }
<Edit className="w-4 h-4 mr-1" /> ]}
Editar />
</Button> );
</div> })}
</div>
</Card>
))}
</div> </div>
{/* Empty State */} {/* Empty State */}
@@ -403,19 +313,107 @@ const InventoryPage: React.FC = () => {
</div> </div>
)} )}
{/* Inventory Form Modal */} {/* Inventory Item Modal */}
{showForm && ( {showForm && selectedItem && (
<InventoryForm <StatusModal
item={selectedItem} isOpen={showForm}
onClose={() => { onClose={() => {
setShowForm(false); setShowForm(false);
setSelectedItem(null); setSelectedItem(null);
setModalMode('view');
}} }}
onSave={(item) => { mode={modalMode}
// Handle save logic onModeChange={setModalMode}
console.log('Saving item:', item); title={selectedItem.name}
setShowForm(false); subtitle={`${selectedItem.category} - ${selectedItem.supplier}`}
setSelectedItem(null); statusIndicator={getInventoryStatusConfig(selectedItem)}
size="lg"
sections={[
{
title: 'Información Básica',
icon: Package,
fields: [
{
label: 'Nombre',
value: selectedItem.name,
highlight: true
},
{
label: 'Categoría',
value: selectedItem.category
},
{
label: 'Proveedor',
value: selectedItem.supplier
},
{
label: 'Unidad de medida',
value: selectedItem.unit
}
]
},
{
title: 'Stock y Niveles',
icon: Package,
fields: [
{
label: 'Stock actual',
value: `${selectedItem.currentStock} ${selectedItem.unit}`,
highlight: true
},
{
label: 'Stock mínimo',
value: `${selectedItem.minStock} ${selectedItem.unit}`
},
{
label: 'Stock máximo',
value: `${selectedItem.maxStock} ${selectedItem.unit}`
},
{
label: 'Porcentaje de stock',
value: Math.round((selectedItem.currentStock / selectedItem.maxStock) * 100),
type: 'percentage',
highlight: selectedItem.currentStock <= selectedItem.minStock
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Costo por unidad',
value: selectedItem.cost,
type: 'currency'
},
{
label: 'Valor total en stock',
value: selectedItem.currentStock * selectedItem.cost,
type: 'currency',
highlight: true
}
]
},
{
title: 'Fechas Importantes',
icon: Calendar,
fields: [
{
label: 'Último restock',
value: selectedItem.lastRestocked,
type: 'date'
},
{
label: 'Fecha de caducidad',
value: selectedItem.expirationDate,
type: 'date',
highlight: new Date(selectedItem.expirationDate) < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
]
}
]}
onEdit={() => {
console.log('Editing inventory item:', selectedItem.id);
}} }}
/> />
)} )}

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Download, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer } from 'lucide-react'; import { Plus, Download, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui'; import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
import { OrderForm } from '../../../../components/domain/sales'; import { OrderForm } from '../../../../components/domain/sales';
@@ -9,6 +9,7 @@ const OrdersPage: React.FC = () => {
const [activeTab] = useState('all'); const [activeTab] = useState('all');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedOrder, setSelectedOrder] = useState<typeof mockOrders[0] | null>(null); const [selectedOrder, setSelectedOrder] = useState<typeof mockOrders[0] | null>(null);
const mockOrders = [ const mockOrders = [
@@ -80,24 +81,26 @@ const OrdersPage: React.FC = () => {
}, },
]; ];
const getStatusBadge = (status: string) => { const getOrderStatusConfig = (status: string, paymentStatus: string) => {
const statusConfig = { const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock }, pending: { text: 'Pendiente', icon: Clock },
in_progress: { color: 'info', text: 'En Proceso', icon: Timer }, in_progress: { text: 'En Proceso', icon: Timer },
ready: { color: 'success', text: 'Listo', icon: CheckCircle }, ready: { text: 'Listo', icon: CheckCircle },
completed: { color: 'success', text: 'Completado', icon: CheckCircle }, completed: { text: 'Completado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle }, cancelled: { text: 'Cancelado', icon: AlertCircle },
}; };
const config = statusConfig[status as keyof typeof statusConfig]; const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon; const Icon = config?.icon;
return ( const isPaymentPending = paymentStatus === 'pending';
<Badge
variant={config?.color as any} return {
icon={Icon && <Icon size={12} />} color: getStatusColor(status),
text={config?.text || status} text: config?.text || status,
/> icon: Icon,
); isCritical: false,
isHighlight: isPaymentPending
};
}; };
const filteredOrders = mockOrders.filter(order => { const filteredOrders = mockOrders.filter(order => {
@@ -211,77 +214,53 @@ const OrdersPage: React.FC = () => {
{/* Orders Grid */} {/* Orders Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => ( {filteredOrders.map((order) => {
<Card key={order.id} className="p-4"> const statusConfig = getOrderStatusConfig(order.status, order.paymentStatus);
<div className="space-y-4"> const paymentNote = order.paymentStatus === 'pending' ? 'Pago pendiente' : '';
{/* Header */}
<div className="flex items-start justify-between"> return (
<div className="flex items-start gap-3"> <StatusCard
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg"> key={order.id}
<Package className="w-4 h-4 text-[var(--text-tertiary)]" /> id={order.id}
</div> statusIndicator={statusConfig}
<div> title={order.customerName}
<div className="font-mono text-sm font-semibold text-[var(--color-primary)]"> subtitle={order.id}
{order.id} primaryValue={formatters.currency(order.total)}
</div> primaryValueLabel={`${order.items?.length} artículos`}
<span className="font-medium text-[var(--text-primary)]"> secondaryInfo={{
{order.customerName} label: 'Entrega',
</span> value: `${new Date(order.deliveryDate).toLocaleDateString('es-ES')} - ${new Date(order.deliveryDate).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}`
</div> }}
</div> metadata={[
{getStatusBadge(order.status)} order.customerEmail,
</div> order.customerPhone,
...(paymentNote ? [paymentNote] : [])
{/* Key Info */} ]}
<div className="flex items-center justify-between"> actions={[
<div className="text-right"> {
<div className="text-lg font-bold text-[var(--text-primary)]"> label: 'Ver',
{formatters.currency(order.total)} icon: Eye,
</div> variant: 'outline',
<div className="text-xs text-[var(--text-tertiary)]"> onClick: () => {
{order.items?.length} artículos
</div>
</div>
<div className="text-right">
<div className="text-sm text-[var(--text-primary)]">
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
Entrega: {new Date(order.deliveryDate).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedOrder(order); setSelectedOrder(order);
setModalMode('view');
setShowForm(true); setShowForm(true);
}} }
> },
<Eye className="w-4 h-4 mr-1" /> {
Ver label: 'Editar',
</Button> icon: Edit,
<Button variant: 'outline',
variant="outline" onClick: () => {
size="sm"
className="flex-1"
onClick={() => {
setSelectedOrder(order); setSelectedOrder(order);
setModalMode('edit');
setShowForm(true); setShowForm(true);
}} }
> }
<Edit className="w-4 h-4 mr-1" /> ]}
Editar />
</Button> );
</div> })}
</div>
</Card>
))}
</div> </div>
{/* Empty State */} {/* Empty State */}
@@ -301,20 +280,117 @@ const OrdersPage: React.FC = () => {
</div> </div>
)} )}
{/* Order Form Modal */} {/* Order Details Modal */}
{showForm && ( {showForm && selectedOrder && (
<OrderForm <StatusModal
orderId={selectedOrder?.id} isOpen={showForm}
onOrderCancel={() => { onClose={() => {
setShowForm(false); setShowForm(false);
setSelectedOrder(null); setSelectedOrder(null);
setModalMode('view');
}} }}
onOrderSave={async (order: any) => { mode={modalMode}
// Handle save logic onModeChange={setModalMode}
console.log('Saving order:', order); title={selectedOrder.customerName}
setShowForm(false); subtitle={`Pedido ${selectedOrder.id}`}
setSelectedOrder(null); statusIndicator={getOrderStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)}
return true; size="lg"
sections={[
{
title: 'Información del Cliente',
icon: Users,
fields: [
{
label: 'Nombre',
value: selectedOrder.customerName,
highlight: true
},
{
label: 'Email',
value: selectedOrder.customerEmail
},
{
label: 'Teléfono',
value: selectedOrder.customerPhone
},
{
label: 'Método de entrega',
value: selectedOrder.deliveryMethod === 'pickup' ? 'Recogida' : 'Entrega a domicilio'
}
]
},
{
title: 'Detalles del Pedido',
icon: Package,
fields: [
{
label: 'Fecha del pedido',
value: selectedOrder.orderDate,
type: 'datetime'
},
{
label: 'Fecha de entrega',
value: selectedOrder.deliveryDate,
type: 'datetime',
highlight: true
},
{
label: 'Artículos',
value: selectedOrder.items?.map(item => `${item.name} (${item.quantity})`),
type: 'list',
span: 2
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Subtotal',
value: selectedOrder.subtotal,
type: 'currency'
},
{
label: 'Impuestos',
value: selectedOrder.tax,
type: 'currency'
},
{
label: 'Descuento',
value: selectedOrder.discount,
type: 'currency'
},
{
label: 'Total',
value: selectedOrder.total,
type: 'currency',
highlight: true
},
{
label: 'Método de pago',
value: selectedOrder.paymentMethod === 'card' ? 'Tarjeta' : selectedOrder.paymentMethod === 'cash' ? 'Efectivo' : 'Transferencia'
},
{
label: 'Estado del pago',
value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : 'Pendiente',
type: 'status'
}
]
},
...(selectedOrder.notes ? [{
title: 'Notas',
fields: [
{
label: 'Observaciones',
value: selectedOrder.notes,
span: 2 as const
}
]
}] : [])
]}
onEdit={() => {
console.log('Editing order:', selectedOrder.id);
}} }}
/> />
)} )}

View File

@@ -1,12 +1,15 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Edit } from 'lucide-react'; import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Edit } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui'; import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
const ProcurementPage: React.FC = () => { const ProcurementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('orders'); const [activeTab, setActiveTab] = useState('orders');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedOrder, setSelectedOrder] = useState<typeof mockPurchaseOrders[0] | null>(null);
const mockPurchaseOrders = [ const mockPurchaseOrders = [
{ {
@@ -101,42 +104,27 @@ const ProcurementPage: React.FC = () => {
}, },
]; ];
const getStatusBadge = (status: string) => { const getPurchaseStatusConfig = (status: string, paymentStatus: string) => {
const statusConfig = { const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock }, pending: { text: 'Pendiente', icon: Clock },
approved: { color: 'info', text: 'Aprobado', icon: CheckCircle }, approved: { text: 'Aprobado', icon: CheckCircle },
in_transit: { color: 'secondary', text: 'En Tránsito', icon: Truck }, in_transit: { text: 'En Tránsito', icon: Truck },
delivered: { color: 'success', text: 'Entregado', icon: CheckCircle }, delivered: { text: 'Entregado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle }, cancelled: { text: 'Cancelado', icon: AlertCircle },
}; };
const config = statusConfig[status as keyof typeof statusConfig]; const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon; const Icon = config?.icon;
return ( const isPaymentPending = paymentStatus === 'pending';
<Badge const isOverdue = paymentStatus === 'overdue';
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
};
const getPaymentStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
paid: { color: 'success', text: 'Pagado', icon: CheckCircle },
overdue: { color: 'error', text: 'Vencido', icon: AlertCircle },
};
const config = statusConfig[status as keyof typeof statusConfig]; return {
const Icon = config?.icon; color: getStatusColor(status === 'in_transit' ? 'inTransit' : status),
return ( text: config?.text || status,
<Badge icon: Icon,
variant={config?.color as any} isCritical: isOverdue,
icon={Icon && <Icon size={12} />} isHighlight: isPaymentPending
text={config?.text || status} };
/>
);
}; };
const filteredOrders = mockPurchaseOrders.filter(order => { const filteredOrders = mockPurchaseOrders.filter(order => {
@@ -282,86 +270,52 @@ const ProcurementPage: React.FC = () => {
{/* Purchase Orders Grid */} {/* Purchase Orders Grid */}
{activeTab === 'orders' && ( {activeTab === 'orders' && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => ( {filteredOrders.map((order) => {
<Card key={order.id} className="p-4"> const statusConfig = getPurchaseStatusConfig(order.status, order.paymentStatus);
<div className="space-y-4"> const paymentNote = order.paymentStatus === 'pending' ? 'Pago pendiente' : order.paymentStatus === 'overdue' ? 'Pago vencido' : '';
{/* Header */}
<div className="flex items-start justify-between"> return (
<div className="flex items-start gap-3"> <StatusCard
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg"> key={order.id}
<ShoppingCart className="w-4 h-4 text-[var(--text-tertiary)]" /> id={order.id}
</div> statusIndicator={statusConfig}
<div> title={order.supplier}
<div className="font-mono text-sm font-semibold text-[var(--color-primary)]"> subtitle={order.id}
{order.id} primaryValue={formatters.currency(order.totalAmount)}
</div> primaryValueLabel={`${order.items?.length} artículos`}
<span className="font-medium text-[var(--text-primary)]"> secondaryInfo={{
{order.supplier} label: 'Entrega',
</span> value: `${new Date(order.deliveryDate).toLocaleDateString('es-ES')} (pedido: ${new Date(order.orderDate).toLocaleDateString('es-ES')})`
</div> }}
</div> metadata={[
{getStatusBadge(order.status)} ...(order.notes ? [`"${order.notes}"`] : []),
</div> ...(paymentNote ? [paymentNote] : [])
]}
{/* Key Info */} actions={[
<div className="flex items-center justify-between"> {
<div className="text-right"> label: 'Ver',
<div className="text-lg font-bold text-[var(--text-primary)]"> icon: Eye,
{formatters.currency(order.totalAmount)} variant: 'outline',
</div> onClick: () => {
<div className="text-xs text-[var(--text-tertiary)]"> setSelectedOrder(order);
{order.items?.length} artículos setModalMode('view');
</div> setShowForm(true);
</div> }
<div className="text-right"> },
<div className="text-sm text-[var(--text-primary)]"> {
{new Date(order.deliveryDate).toLocaleDateString('es-ES')} label: 'Editar',
</div> icon: Edit,
<div className="text-xs text-[var(--text-tertiary)]"> variant: 'outline',
Entrega prevista onClick: () => {
</div> setSelectedOrder(order);
</div> setModalMode('edit');
</div> setShowForm(true);
}
{/* Payment Status */} }
<div className="flex items-center justify-between"> ]}
<div className="text-sm text-[var(--text-secondary)]"> />
Estado del pago: );
</div> })}
{getPaymentStatusBadge(order.paymentStatus)}
</div>
{/* Notes */}
{order.notes && (
<div className="bg-[var(--bg-secondary)] p-2 rounded text-xs text-[var(--text-secondary)] italic">
"{order.notes}"
</div>
)}
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => console.log('View order', order.id)}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => console.log('Edit order', order.id)}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
</div> </div>
)} )}
@@ -499,6 +453,115 @@ const ProcurementPage: React.FC = () => {
</Card> </Card>
</div> </div>
)} )}
{/* Purchase Order Modal */}
{showForm && selectedOrder && (
<StatusModal
isOpen={showForm}
onClose={() => {
setShowForm(false);
setSelectedOrder(null);
setModalMode('view');
}}
mode={modalMode}
onModeChange={setModalMode}
title={selectedOrder.supplier}
subtitle={`Orden de Compra ${selectedOrder.id}`}
statusIndicator={getPurchaseStatusConfig(selectedOrder.status, selectedOrder.paymentStatus)}
size="lg"
sections={[
{
title: 'Información del Proveedor',
icon: Package,
fields: [
{
label: 'Proveedor',
value: selectedOrder.supplier,
highlight: true,
editable: true,
required: true,
placeholder: 'Nombre del proveedor'
},
{
label: 'ID de Orden',
value: selectedOrder.id
},
{
label: 'Estado de Pago',
value: selectedOrder.paymentStatus === 'paid' ? 'Pagado' : selectedOrder.paymentStatus === 'pending' ? 'Pendiente' : 'Vencido',
type: 'status'
}
]
},
{
title: 'Fechas Importantes',
icon: Calendar,
fields: [
{
label: 'Fecha de pedido',
value: selectedOrder.orderDate,
type: 'date',
editable: true
},
{
label: 'Fecha de entrega',
value: selectedOrder.deliveryDate,
type: 'date',
highlight: true,
editable: true,
required: true
}
]
},
{
title: 'Información Financiera',
icon: DollarSign,
fields: [
{
label: 'Importe total',
value: selectedOrder.totalAmount,
type: 'currency',
highlight: true,
editable: true,
required: true,
placeholder: '0.00'
},
{
label: 'Número de artículos',
value: `${selectedOrder.items?.length} productos`
}
]
},
{
title: 'Artículos Pedidos',
icon: ShoppingCart,
fields: [
{
label: 'Lista de productos',
value: selectedOrder.items?.map(item => `${item.name}: ${item.quantity} ${item.unit} - ${formatters.currency(item.total)}`),
type: 'list',
span: 2
}
]
},
...(selectedOrder.notes ? [{
title: 'Notas Adicionales',
fields: [
{
label: 'Observaciones',
value: selectedOrder.notes,
span: 2 as const,
editable: true,
placeholder: 'Añadir notas sobre la orden de compra...'
}
]
}] : [])
]}
onEdit={() => {
console.log('Editing purchase order:', selectedOrder.id);
}}
/>
)}
</div> </div>
); );
}; };

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Download, Clock, Users, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Calendar, Zap } from 'lucide-react'; import { Plus, Download, Clock, Users, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Calendar, Zap, Package } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui'; import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { pagePresets } from '../../../../components/ui/Stats/StatsPresets'; import { pagePresets } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production'; import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
@@ -10,6 +10,7 @@ const ProductionPage: React.FC = () => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedOrder, setSelectedOrder] = useState<typeof mockProductionOrders[0] | null>(null); const [selectedOrder, setSelectedOrder] = useState<typeof mockProductionOrders[0] | null>(null);
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const mockProductionStats = { const mockProductionStats = {
dailyTarget: 150, dailyTarget: 150,
@@ -111,42 +112,25 @@ const ProductionPage: React.FC = () => {
}, },
]; ];
const getStatusBadge = (status: string) => { const getProductionStatusConfig = (status: string, priority: string) => {
const statusConfig = { const statusConfig = {
pending: { color: 'warning', text: 'Pendiente', icon: Clock }, pending: { text: 'Pendiente', icon: Clock },
in_progress: { color: 'info', text: 'En Proceso', icon: Timer }, in_progress: { text: 'En Proceso', icon: Timer },
completed: { color: 'success', text: 'Completado', icon: CheckCircle }, completed: { text: 'Completado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle }, cancelled: { text: 'Cancelado', icon: AlertCircle },
}; };
const config = statusConfig[status as keyof typeof statusConfig]; const config = statusConfig[status as keyof typeof statusConfig];
const Icon = config?.icon; const Icon = config?.icon;
return ( const isUrgent = priority === 'urgent';
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'outline', text: 'Baja' },
medium: { color: 'secondary', text: 'Media' },
high: { color: 'warning', text: 'Alta' },
urgent: { color: 'error', text: 'Urgente', icon: Zap },
};
const config = priorityConfig[priority as keyof typeof priorityConfig]; return {
const Icon = config?.icon; color: getStatusColor(status),
return ( text: config?.text || status,
<Badge icon: Icon,
variant={config?.color as any} isCritical: isUrgent,
icon={Icon && <Icon size={12} />} isHighlight: false
text={config?.text || priority} };
/>
);
}; };
const filteredOrders = mockProductionOrders.filter(order => { const filteredOrders = mockProductionOrders.filter(order => {
@@ -245,120 +229,52 @@ const ProductionPage: React.FC = () => {
{/* Production Orders Grid */} {/* Production Orders Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => ( {filteredOrders.map((order) => {
<Card key={order.id} className="p-4"> const statusConfig = getProductionStatusConfig(order.status, order.priority);
<div className="space-y-4">
{/* Header */} return (
<div className="flex items-start justify-between"> <StatusCard
<div className="flex items-start gap-3"> key={order.id}
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg"> id={order.id}
<ChefHat className="w-4 h-4 text-[var(--text-tertiary)]" /> statusIndicator={statusConfig}
</div> title={order.recipeName}
<div> subtitle={`Asignado a: ${order.assignedTo}`}
<div className="font-medium text-[var(--text-primary)]"> primaryValue={order.quantity}
{order.recipeName} primaryValueLabel="unidades"
</div> secondaryInfo={{
<div className="text-sm text-[var(--text-secondary)]"> label: 'Horario',
ID: {order.id} value: `${new Date(order.startTime).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })} → ${new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}`
</div> }}
</div> progress={{
</div> label: 'Progreso',
{getStatusBadge(order.status)} percentage: order.progress,
</div> color: statusConfig.color
}}
{/* Priority and Quantity */} actions={[
<div className="flex items-center justify-between"> {
<div> label: 'Ver',
{getPriorityBadge(order.priority)} icon: Eye,
</div> variant: 'outline',
<div className="text-right"> onClick: () => {
<div className="text-lg font-bold text-[var(--text-primary)]">
{order.quantity}
</div>
<div className="text-xs text-[var(--text-tertiary)]">
unidades
</div>
</div>
</div>
{/* Assigned Worker */}
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-sm text-[var(--text-primary)]">
{order.assignedTo}
</span>
</div>
{/* Time Information */}
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Inicio:</span>
<span className="text-[var(--text-primary)]">
{new Date(order.startTime).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[var(--text-secondary)]">Est. finalización:</span>
<span className="text-[var(--text-primary)]">
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
{/* Progress Bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Progreso</span>
<span className="text-[var(--text-primary)]">
{order.progress}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
order.progress === 100
? 'bg-[var(--color-success)]'
: order.progress > 50
? 'bg-[var(--color-info)]'
: order.progress > 0
? 'bg-[var(--color-warning)]'
: 'bg-[var(--bg-quaternary)]'
}`}
style={{ width: `${order.progress}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedOrder(order); setSelectedOrder(order);
setModalMode('view');
setShowForm(true); setShowForm(true);
}} }
> },
<Eye className="w-4 h-4 mr-1" /> {
Ver label: 'Editar',
</Button> icon: Edit,
<Button variant: 'outline',
variant="outline" onClick: () => {
size="sm"
className="flex-1"
onClick={() => {
setSelectedOrder(order); setSelectedOrder(order);
setModalMode('edit');
setShowForm(true); setShowForm(true);
}} }
> }
<Edit className="w-4 h-4 mr-1" /> ]}
Editar />
</Button> );
</div> })}
</div>
</Card>
))}
</div> </div>
{/* Empty State */} {/* Empty State */}
@@ -388,52 +304,70 @@ const ProductionPage: React.FC = () => {
<QualityControl /> <QualityControl />
)} )}
{/* Production Order Form Modal - Placeholder */} {/* Production Order Modal */}
{showForm && ( {showForm && selectedOrder && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <StatusModal
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6"> isOpen={showForm}
<div className="flex items-center justify-between mb-4"> onClose={() => {
<h2 className="text-xl font-semibold text-[var(--text-primary)]"> setShowForm(false);
{selectedOrder ? 'Ver Orden de Producción' : 'Nueva Orden de Producción'} setSelectedOrder(null);
</h2> setModalMode('view');
<Button }}
variant="outline" mode={modalMode}
size="sm" onModeChange={setModalMode}
onClick={() => { title={selectedOrder.recipeName}
setShowForm(false); subtitle={`Orden de Producción #${selectedOrder.id}`}
setSelectedOrder(null); statusIndicator={getProductionStatusConfig(selectedOrder.status, selectedOrder.priority)}
}} size="lg"
> sections={[
Cerrar {
</Button> title: 'Información General',
</div> icon: Package,
{selectedOrder && ( fields: [
<div className="space-y-4"> {
<h3 className="text-lg font-medium">{selectedOrder.recipeName}</h3> label: 'Cantidad',
<div className="grid grid-cols-2 gap-4 text-sm"> value: `${selectedOrder.quantity} unidades`,
<div> highlight: true
<span className="font-medium">Cantidad:</span> {selectedOrder.quantity} unidades },
</div> {
<div> label: 'Asignado a',
<span className="font-medium">Asignado a:</span> {selectedOrder.assignedTo} value: selectedOrder.assignedTo,
</div> },
<div> {
<span className="font-medium">Estado:</span> {selectedOrder.status} label: 'Prioridad',
</div> value: selectedOrder.priority,
<div> type: 'status'
<span className="font-medium">Progreso:</span> {selectedOrder.progress}% },
</div> {
<div> label: 'Progreso',
<span className="font-medium">Inicio:</span> {new Date(selectedOrder.startTime).toLocaleString('es-ES')} value: selectedOrder.progress,
</div> type: 'percentage',
<div> highlight: true
<span className="font-medium">Finalización:</span> {new Date(selectedOrder.estimatedCompletion).toLocaleString('es-ES')} }
</div> ]
</div> },
</div> {
)} title: 'Cronograma',
</Card> icon: Clock,
</div> fields: [
{
label: 'Hora de inicio',
value: selectedOrder.startTime,
type: 'datetime'
},
{
label: 'Finalización estimada',
value: selectedOrder.estimatedCompletion,
type: 'datetime'
}
]
}
]}
onEdit={() => {
// Handle edit mode
console.log('Editing production order:', selectedOrder.id);
}}
/>
)} )}
</div> </div>
); );

View File

@@ -1,12 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Download, Star, Clock, Users, DollarSign, Package, Eye, Edit, ChefHat, Timer } from 'lucide-react'; import { Plus, Download, Star, Clock, Users, DollarSign, Package, Eye, Edit, ChefHat, Timer } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui'; import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
const RecipesPage: React.FC = () => { const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
const [selectedRecipe, setSelectedRecipe] = useState<typeof mockRecipes[0] | null>(null); const [selectedRecipe, setSelectedRecipe] = useState<typeof mockRecipes[0] | null>(null);
const mockRecipes = [ const mockRecipes = [
@@ -100,38 +101,34 @@ const RecipesPage: React.FC = () => {
}, },
]; ];
const getCategoryBadge = (category: string) => { const getRecipeStatusConfig = (category: string, difficulty: string, rating: number) => {
const categoryConfig = { const categoryConfig = {
bread: { color: 'default', text: 'Pan' }, bread: { text: 'Pan', icon: ChefHat },
pastry: { color: 'warning', text: 'Bollería' }, pastry: { text: 'Bollería', icon: ChefHat },
cake: { color: 'secondary', text: 'Tarta' }, cake: { text: 'Tarta', icon: ChefHat },
cookie: { color: 'info', text: 'Galleta' }, cookie: { text: 'Galleta', icon: ChefHat },
other: { color: 'outline', text: 'Otro' }, other: { text: 'Otro', icon: ChefHat },
}; };
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return (
<Badge
variant={config.color as any}
text={config.text}
/>
);
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = { const difficultyConfig = {
easy: { color: 'success', text: 'Fácil' }, easy: { icon: '', label: 'Fácil' },
medium: { color: 'warning', text: 'Medio' }, medium: { icon: '●●', label: 'Medio' },
hard: { color: 'error', text: 'Difícil' }, hard: { icon: '●●●', label: 'Difícil' },
}; };
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig]; const categoryInfo = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return ( const difficultyInfo = difficultyConfig[difficulty as keyof typeof difficultyConfig];
<Badge const isPopular = rating >= 4.7;
variant={config?.color as any}
text={config?.text || difficulty} return {
/> color: getStatusColor(category),
); text: categoryInfo.text,
icon: categoryInfo.icon,
difficultyIcon: difficultyInfo?.icon || '●',
difficultyLabel: difficultyInfo?.label || difficulty,
isCritical: false,
isHighlight: isPopular
};
}; };
const formatTime = (minutes: number) => { const formatTime = (minutes: number) => {
@@ -245,130 +242,65 @@ const RecipesPage: React.FC = () => {
{/* Recipes Grid */} {/* Recipes Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredRecipes.map((recipe) => ( {filteredRecipes.map((recipe) => {
<Card key={recipe.id} className="p-4"> const statusConfig = getRecipeStatusConfig(recipe.category, recipe.difficulty, recipe.rating);
<div className="space-y-4"> const profitMargin = Math.round((recipe.profit / recipe.price) * 100);
{/* Header with Image */} const totalTime = formatTime(recipe.prepTime + recipe.bakingTime);
<div className="flex items-start gap-3">
<div className="flex-shrink-0"> return (
<img <StatusCard
src={recipe.image} key={recipe.id}
alt={recipe.name} id={recipe.id}
className="w-16 h-16 rounded-lg object-cover bg-[var(--bg-secondary)]" statusIndicator={statusConfig}
/> title={recipe.name}
</div> subtitle={`${statusConfig.text}${statusConfig.difficultyLabel}${statusConfig.isHighlight ? ' ★' + recipe.rating : ''}`}
<div className="flex-1 min-w-0"> primaryValue={formatters.currency(recipe.profit)}
<div className="font-medium text-[var(--text-primary)] truncate"> primaryValueLabel="margen"
{recipe.name} secondaryInfo={{
</div> label: 'Precio de venta',
<div className="flex items-center gap-1 mt-1"> value: `${formatters.currency(recipe.price)} (costo: ${formatters.currency(recipe.cost)})`
<Star className="w-3 h-3 text-yellow-400 fill-current" /> }}
<span className="text-xs text-[var(--text-secondary)]">{recipe.rating}</span> progress={{
</div> label: 'Margen de beneficio',
<p className="text-xs text-[var(--text-secondary)] mt-1 line-clamp-2"> percentage: profitMargin,
{recipe.description} color: profitMargin > 50 ? '#10b981' : profitMargin > 30 ? '#f59e0b' : '#ef4444'
</p> }}
</div> metadata={[
</div> `Tiempo: ${totalTime}`,
`Porciones: ${recipe.yield}`,
{/* Badges */} `${recipe.ingredients.length} ingredientes principales`
<div className="flex gap-2 flex-wrap"> ]}
{getCategoryBadge(recipe.category)} actions={[
{getDifficultyBadge(recipe.difficulty)} {
</div> label: 'Ver',
icon: Eye,
{/* Time and Yield */} variant: 'outline',
<div className="flex items-center justify-between text-sm"> onClick: () => {
<div className="flex items-center gap-1">
<Clock className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{recipe.yield} porciones
</span>
</div>
</div>
{/* Financial Info */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Costo:</span>
<span className="font-medium text-[var(--text-primary)]">
{formatters.currency(recipe.cost)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Precio:</span>
<span className="font-medium text-[var(--color-success)]">
{formatters.currency(recipe.price)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Margen:</span>
<span className="font-bold text-[var(--color-success)]">
{formatters.currency(recipe.profit)}
</span>
</div>
</div>
{/* Profit Margin Bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Margen de beneficio</span>
<span className="text-[var(--text-primary)]">
{Math.round((recipe.profit / recipe.price) * 100)}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
(recipe.profit / recipe.price) > 0.5
? 'bg-[var(--color-success)]'
: (recipe.profit / recipe.price) > 0.3
? 'bg-[var(--color-warning)]'
: 'bg-[var(--color-error)]'
}`}
style={{ width: `${Math.min((recipe.profit / recipe.price) * 100, 100)}%` }}
/>
</div>
</div>
{/* Ingredients Count */}
<div className="text-xs text-[var(--text-secondary)]">
{recipe.ingredients.length} ingredientes principales
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedRecipe(recipe); setSelectedRecipe(recipe);
setModalMode('view');
setShowForm(true); setShowForm(true);
}} }
> },
<Eye className="w-4 h-4 mr-1" /> {
Ver label: 'Editar',
</Button> icon: Edit,
<Button variant: 'outline',
variant="primary" onClick: () => {
size="sm" setSelectedRecipe(recipe);
className="flex-1" setModalMode('edit');
onClick={() => console.log('Produce recipe', recipe.id)} setShowForm(true);
> }
<ChefHat className="w-4 h-4 mr-1" /> },
Producir {
</Button> label: 'Producir',
</div> icon: ChefHat,
</div> variant: 'primary',
</Card> onClick: () => console.log('Produce recipe', recipe.id)
))} }
]}
/>
);
})}
</div> </div>
{/* Empty State */} {/* Empty State */}
@@ -388,57 +320,133 @@ const RecipesPage: React.FC = () => {
</div> </div>
)} )}
{/* Recipe Form Modal - Placeholder */} {/* Recipe Details Modal */}
{showForm && ( {showForm && selectedRecipe && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <StatusModal
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6"> isOpen={showForm}
<div className="flex items-center justify-between mb-4"> onClose={() => {
<h2 className="text-xl font-semibold text-[var(--text-primary)]"> setShowForm(false);
{selectedRecipe ? 'Ver Receta' : 'Nueva Receta'} setSelectedRecipe(null);
</h2> setModalMode('view');
<Button }}
variant="outline" mode={modalMode}
size="sm" onModeChange={setModalMode}
onClick={() => { title={selectedRecipe.name}
setShowForm(false); subtitle={selectedRecipe.description}
setSelectedRecipe(null); statusIndicator={getRecipeStatusConfig(selectedRecipe.category, selectedRecipe.difficulty, selectedRecipe.rating)}
}} image={selectedRecipe.image}
> size="xl"
Cerrar sections={[
</Button> {
</div> title: 'Información Básica',
{selectedRecipe && ( icon: ChefHat,
<div className="space-y-4"> fields: [
<img {
src={selectedRecipe.image} label: 'Categoría',
alt={selectedRecipe.name} value: selectedRecipe.category,
className="w-full h-48 object-cover rounded-lg" type: 'status'
/> },
<h3 className="text-lg font-medium">{selectedRecipe.name}</h3> {
<p className="text-[var(--text-secondary)]">{selectedRecipe.description}</p> label: 'Dificultad',
<div className="grid grid-cols-2 gap-4 text-sm"> value: selectedRecipe.difficulty
<div> },
<span className="font-medium">Tiempo total:</span> {formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime)} {
</div> label: 'Valoración',
<div> value: `${selectedRecipe.rating}`,
<span className="font-medium">Rendimiento:</span> {selectedRecipe.yield} porciones highlight: selectedRecipe.rating >= 4.7
</div> },
</div> {
<div> label: 'Rendimiento',
<h4 className="font-medium mb-2">Ingredientes:</h4> value: `${selectedRecipe.yield} porciones`
<ul className="space-y-1 text-sm"> }
{selectedRecipe.ingredients.map((ing, i) => ( ]
<li key={i} className="flex justify-between"> },
<span>{ing.name}</span> {
<span>{ing.quantity} {ing.unit}</span> title: 'Tiempos',
</li> icon: Clock,
))} fields: [
</ul> {
</div> label: 'Tiempo de preparación',
</div> value: formatTime(selectedRecipe.prepTime)
)} },
</Card> {
</div> label: 'Tiempo de horneado',
value: formatTime(selectedRecipe.bakingTime)
},
{
label: 'Tiempo total',
value: formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime),
highlight: true
}
]
},
{
title: 'Análisis Financiero',
icon: DollarSign,
fields: [
{
label: 'Costo de producción',
value: selectedRecipe.cost,
type: 'currency'
},
{
label: 'Precio de venta',
value: selectedRecipe.price,
type: 'currency'
},
{
label: 'Margen de beneficio',
value: selectedRecipe.profit,
type: 'currency',
highlight: true
},
{
label: 'Porcentaje de margen',
value: Math.round((selectedRecipe.profit / selectedRecipe.price) * 100),
type: 'percentage',
highlight: true
}
]
},
{
title: 'Ingredientes',
icon: Package,
fields: [
{
label: 'Lista de ingredientes',
value: selectedRecipe.ingredients.map(ing => `${ing.name}: ${ing.quantity} ${ing.unit}`),
type: 'list',
span: 2
}
]
},
{
title: 'Etiquetas',
fields: [
{
label: 'Tags',
value: selectedRecipe.tags.join(', '),
span: 2
}
]
}
]}
actions={[
{
label: 'Producir',
icon: ChefHat,
variant: 'primary',
onClick: () => {
console.log('Producing recipe:', selectedRecipe.id);
setShowForm(false);
setSelectedRecipe(null);
}
}
]}
onEdit={() => {
console.log('Editing recipe:', selectedRecipe.id);
}}
/>
)} )}
</div> </div>
); );

131
frontend/src/styles/colors.d.ts vendored Normal file
View File

@@ -0,0 +1,131 @@
export interface ColorScale {
50: string;
100: string;
200: string;
300: string;
400: string;
500: string;
600: string;
700: string;
800: string;
900: string;
}
export interface StatusColorConfig {
primary: string;
light: string;
dark: string;
}
export interface BaseColors {
primary: ColorScale;
secondary: ColorScale;
success: ColorScale;
warning: ColorScale;
error: ColorScale;
info: ColorScale;
neutral: ColorScale;
}
export interface BakeryColors {
[key: string]: string;
}
export interface StatusColors {
pending: StatusColorConfig;
inProgress: StatusColorConfig;
completed: StatusColorConfig;
cancelled: StatusColorConfig;
normal: StatusColorConfig;
low: StatusColorConfig;
out: StatusColorConfig;
expired: StatusColorConfig;
approved: StatusColorConfig;
inTransit: StatusColorConfig;
delivered: StatusColorConfig;
bread: StatusColorConfig;
pastry: StatusColorConfig;
cake: StatusColorConfig;
cookie: StatusColorConfig;
other: StatusColorConfig;
}
export interface ChartColors {
[key: string]: string;
}
export interface ThemeColors {
bg: {
primary: string;
secondary: string;
tertiary: string;
quaternary: string;
overlay: string;
modalBackdrop: string;
};
text: {
primary: string;
secondary: string;
tertiary: string;
quaternary: string;
inverse: string;
muted: string;
disabled: string;
};
border: {
primary: string;
secondary: string;
tertiary: string;
focus: string;
error: string;
success: string;
};
surface: {
primary: string;
secondary: string;
tertiary: string;
raised: string;
overlay: string;
};
input: {
bg: string;
border: string;
borderFocus: string;
borderError: string;
placeholder: string;
};
nav: {
bg: string;
border: string;
itemHover: string;
itemActive: string;
};
card: {
bg: string;
border: string;
shadow: string;
};
}
declare const baseColors: BaseColors;
declare const bakeryColors: BakeryColors;
declare const statusColors: StatusColors;
declare const chartColors: ChartColors;
declare const lightTheme: ThemeColors;
declare const darkTheme: ThemeColors;
declare const tailwindColorConfig: any;
declare const generateCSSVariables: (theme?: string) => { [key: string]: string };
declare const _default: {
baseColors: BaseColors;
bakeryColors: BakeryColors;
statusColors: StatusColors;
chartColors: ChartColors;
lightTheme: ThemeColors;
darkTheme: ThemeColors;
tailwindColorConfig: any;
generateCSSVariables: (theme?: string) => { [key: string]: string };
};
export { baseColors, bakeryColors, statusColors, chartColors, lightTheme, darkTheme, tailwindColorConfig, generateCSSVariables };
export default _default;

View File

@@ -123,6 +123,94 @@ export const bakeryColors = {
cream: '#fffdd0', cream: '#fffdd0',
}; };
// Status Colors for Cards and Components
export const statusColors = {
// Order/Production Statuses
pending: {
primary: '#f59e0b',
light: '#fef3c7',
dark: '#d97706',
},
inProgress: {
primary: '#3b82f6',
light: '#dbeafe',
dark: '#2563eb',
},
completed: {
primary: '#10b981',
light: '#d1fae5',
dark: '#059669',
},
cancelled: {
primary: '#ef4444',
light: '#fee2e2',
dark: '#dc2626',
},
// Inventory Statuses
normal: {
primary: '#10b981',
light: '#d1fae5',
dark: '#059669',
},
low: {
primary: '#f59e0b',
light: '#fef3c7',
dark: '#d97706',
},
out: {
primary: '#ef4444',
light: '#fee2e2',
dark: '#dc2626',
},
expired: {
primary: '#ef4444',
light: '#fee2e2',
dark: '#dc2626',
},
// Purchase Order Statuses
approved: {
primary: '#3b82f6',
light: '#dbeafe',
dark: '#2563eb',
},
inTransit: {
primary: '#8b5cf6',
light: '#ede9fe',
dark: '#7c3aed',
},
delivered: {
primary: '#10b981',
light: '#d1fae5',
dark: '#059669',
},
// Recipe Categories
bread: {
primary: '#8b5cf6',
light: '#ede9fe',
dark: '#7c3aed',
},
pastry: {
primary: '#f59e0b',
light: '#fef3c7',
dark: '#d97706',
},
cake: {
primary: '#ef4444',
light: '#fee2e2',
dark: '#dc2626',
},
cookie: {
primary: '#3b82f6',
light: '#dbeafe',
dark: '#2563eb',
},
other: {
primary: '#6b7280',
light: '#f3f4f6',
dark: '#4b5563',
},
};
// Chart Colors (Colorblind-Safe Data Visualization) // Chart Colors (Colorblind-Safe Data Visualization)
export const chartColors = { export const chartColors = {
primary: '#d97706', // Orange primary: '#d97706', // Orange
@@ -313,6 +401,7 @@ export const generateCSSVariables = (theme = 'light') => {
export default { export default {
baseColors, baseColors,
bakeryColors, bakeryColors,
statusColors,
chartColors, chartColors,
lightTheme, lightTheme,
darkTheme, darkTheme,