Add new page designs

This commit is contained in:
Urtzi Alfaro
2025-08-30 19:11:15 +02:00
parent 221781731c
commit 62b1ab9cb1
12 changed files with 2129 additions and 1240 deletions

View File

@@ -1,22 +1,15 @@
import React, { useState } from 'react';
import { Plus, Calendar, Clock, Users, AlertCircle, Search, Download, Filter } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { DataTable } from '../../../../components/shared';
import type { DataTableColumn, DataTableFilter, DataTablePagination, DataTableSelection } from '../../../../components/shared';
import { Plus, Download, Clock, Users, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Calendar, Zap } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { pagePresets } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout';
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
const ProductionPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('schedule');
const [searchQuery, setSearchQuery] = useState('');
const [filters, setFilters] = useState<DataTableFilter[]>([]);
const [selectedBatches, setSelectedBatches] = useState<any[]>([]);
const [pagination, setPagination] = useState<DataTablePagination>({
page: 1,
pageSize: 10,
total: 8 // Updated to match the number of mock orders
});
const [isLoading, setIsLoading] = useState(false);
const [selectedOrder, setSelectedOrder] = useState<typeof mockProductionOrders[0] | null>(null);
const [showForm, setShowForm] = useState(false);
const mockProductionStats = {
dailyTarget: 150,
@@ -27,41 +20,6 @@ const ProductionPage: React.FC = () => {
quality: 94,
};
// Handler functions for table actions
const handleViewBatch = (batch: any) => {
console.log('Ver lote:', batch);
// Implement view logic
};
const handleEditBatch = (batch: any) => {
console.log('Editar lote:', batch);
// Implement edit logic
};
const handleSearchChange = (query: string) => {
setSearchQuery(query);
// Implement search logic
};
const handleFiltersChange = (newFilters: DataTableFilter[]) => {
setFilters(newFilters);
// Implement filtering logic
};
const handlePageChange = (page: number, pageSize: number) => {
setPagination(prev => ({ ...prev, page, pageSize }));
// Implement pagination logic
};
const handleBatchSelection = (selectedRows: any[]) => {
setSelectedBatches(selectedRows);
};
const handleExport = (format: 'csv' | 'xlsx') => {
console.log(`Exportando en formato ${format}`);
// Implement export logic
};
const mockProductionOrders = [
{
id: '1',
@@ -155,248 +113,78 @@ const ProductionPage: React.FC = () => {
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: { color: 'yellow', text: 'Pendiente' },
in_progress: { color: 'blue', text: 'En Proceso' },
completed: { color: 'green', text: 'Completado' },
cancelled: { color: 'red', text: 'Cancelado' },
pending: { color: 'warning', text: 'Pendiente', icon: Clock },
in_progress: { color: 'info', text: 'En Proceso', icon: Timer },
completed: { color: 'success', text: 'Completado', icon: CheckCircle },
cancelled: { color: 'error', text: 'Cancelado', icon: AlertCircle },
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || status}
/>
);
};
const getPriorityBadge = (priority: string) => {
const priorityConfig = {
low: { color: 'gray', text: 'Baja' },
medium: { color: 'yellow', text: 'Media' },
high: { color: 'orange', text: 'Alta' },
urgent: { color: 'red', text: 'Urgente' },
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 <Badge variant={config.color as any}>{config.text}</Badge>;
const Icon = config?.icon;
return (
<Badge
variant={config?.color as any}
icon={Icon && <Icon size={12} />}
text={config?.text || priority}
/>
);
};
const columns: DataTableColumn[] = [
{
id: 'recipeName',
key: 'recipeName',
header: 'Receta',
sortable: true,
filterable: true,
type: 'text',
width: 200,
cell: (value) => (
<div className="text-sm font-medium text-[var(--text-primary)]">{value}</div>
),
},
{
id: 'quantity',
key: 'quantity',
header: 'Cantidad',
sortable: true,
filterable: true,
type: 'number',
width: 120,
align: 'center',
cell: (value) => `${value} unidades`,
},
{
id: 'status',
key: 'status',
header: 'Estado',
sortable: true,
filterable: true,
type: 'select',
width: 130,
align: 'center',
selectOptions: [
{ value: 'pending', label: 'Pendiente' },
{ value: 'in_progress', label: 'En Proceso' },
{ value: 'completed', label: 'Completado' },
{ value: 'cancelled', label: 'Cancelado' }
],
cell: (value) => getStatusBadge(value),
},
{
id: 'priority',
key: 'priority',
header: 'Prioridad',
sortable: true,
filterable: true,
type: 'select',
width: 120,
align: 'center',
selectOptions: [
{ value: 'low', label: 'Baja' },
{ value: 'medium', label: 'Media' },
{ value: 'high', label: 'Alta' },
{ value: 'urgent', label: 'Urgente' }
],
cell: (value) => getPriorityBadge(value),
},
{
id: 'assignedTo',
key: 'assignedTo',
header: 'Asignado a',
sortable: true,
filterable: true,
type: 'text',
width: 180,
hideOnMobile: true,
cell: (value) => (
<div className="flex items-center">
<Users className="h-4 w-4 text-[var(--text-tertiary)] mr-2" />
<span className="text-sm text-[var(--text-primary)]">{value}</span>
</div>
),
},
{
id: 'progress',
key: 'progress',
header: 'Progreso',
sortable: true,
filterable: true,
type: 'number',
width: 150,
align: 'center',
hideOnMobile: true,
cell: (value) => (
<div className="flex items-center">
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mr-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${value}%` }}
></div>
</div>
<span className="text-sm text-[var(--text-primary)]">{value}%</span>
</div>
),
},
{
id: 'estimatedCompletion',
key: 'estimatedCompletion',
header: 'Tiempo Estimado',
sortable: true,
filterable: true,
type: 'date',
width: 140,
align: 'center',
hideOnMobile: true,
cell: (value) => new Date(value).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
}),
},
{
id: 'actions',
key: 'actions',
header: 'Acciones',
sortable: false,
filterable: false,
width: 150,
align: 'right',
sticky: 'right',
cell: (value, row) => (
<div className="flex space-x-2">
<Button variant="outline" size="sm" onClick={() => handleViewBatch(row)}>
Ver
</Button>
<Button variant="outline" size="sm" onClick={() => handleEditBatch(row)}>
Editar
</Button>
</div>
),
},
];
const filteredOrders = mockProductionOrders.filter(order => {
const matchesSearch = order.recipeName.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.assignedTo.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.id.toLowerCase().includes(searchQuery.toLowerCase());
return matchesSearch;
});
return (
<div className="p-6 space-y-6">
<div className="space-y-6">
<PageHeader
title="Gestión de Producción"
description="Planifica y controla la producción diaria de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Producción
</Button>
}
actions={[
{
id: "export",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => console.log('Export production orders')
},
{
id: "new",
label: "Nueva Orden de Producción",
variant: "primary" as const,
icon: Plus,
onClick: () => setShowForm(true)
}
]}
/>
{/* Production Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Meta Diaria</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">{mockProductionStats.dailyTarget}</p>
</div>
<Calendar className="h-8 w-8 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Completado</p>
<p className="text-2xl font-bold text-[var(--color-success)]">{mockProductionStats.completed}</p>
</div>
<div className="h-8 w-8 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-[var(--color-success)]" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">En Proceso</p>
<p className="text-2xl font-bold text-[var(--color-info)]">{mockProductionStats.inProgress}</p>
</div>
<Clock className="h-8 w-8 text-[var(--color-info)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Pendiente</p>
<p className="text-2xl font-bold text-[var(--color-primary)]">{mockProductionStats.pending}</p>
</div>
<AlertCircle className="h-8 w-8 text-[var(--color-primary)]" />
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Eficiencia</p>
<p className="text-2xl font-bold text-purple-600">{mockProductionStats.efficiency}%</p>
</div>
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Calidad</p>
<p className="text-2xl font-bold text-indigo-600">{mockProductionStats.quality}%</p>
</div>
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</Card>
</div>
<StatsGrid
stats={pagePresets.production(mockProductionStats)}
columns={6}
/>
{/* Tabs Navigation */}
<div className="border-b border-[var(--border-primary)]">
@@ -434,56 +222,162 @@ const ProductionPage: React.FC = () => {
</nav>
</div>
{/* Tab Content */}
{/* Production Orders Tab */}
{activeTab === 'schedule' && (
<Card>
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium text-[var(--text-primary)]">Órdenes de Producción</h3>
<div className="flex space-x-2">
{selectedBatches.length > 0 && (
<Button variant="outline" size="sm">
<Download className="w-4 h-4 mr-2" />
Acciones en lote ({selectedBatches.length})
</Button>
)}
<Button variant="outline" size="sm">
<Filter className="w-4 h-4 mr-2" />
Vista Calendario
</Button>
<>
{/* Simplified Controls */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar órdenes por receta, asignado o ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full"
/>
</div>
<Button variant="outline" onClick={() => console.log('Export filtered')}>
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</div>
<DataTable
data={mockProductionOrders}
columns={columns}
isLoading={isLoading}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
searchPlaceholder="Buscar por receta, asignado, estado..."
filters={filters}
onFiltersChange={handleFiltersChange}
pagination={pagination}
onPageChange={handlePageChange}
selection={{
mode: 'multiple',
selectedRows: selectedBatches,
onSelectionChange: handleBatchSelection,
getRowId: (row) => row.id
}}
enableExport={true}
onExport={handleExport}
density="normal"
horizontalScroll={true}
emptyStateMessage="No se encontraron órdenes de producción"
emptyStateAction={{
label: "Nueva Orden",
onClick: () => console.log('Nueva orden de producción')
}}
onRowClick={(row) => console.log('Ver detalles:', row)}
/>
</Card>
{/* Production Orders Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredOrders.map((order) => (
<Card key={order.id} className="p-4">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 bg-[var(--color-primary)]/10 p-2 rounded-lg">
<ChefHat className="w-4 h-4 text-[var(--text-tertiary)]" />
</div>
<div>
<div className="font-medium text-[var(--text-primary)]">
{order.recipeName}
</div>
<div className="text-sm text-[var(--text-secondary)]">
ID: {order.id}
</div>
</div>
</div>
{getStatusBadge(order.status)}
</div>
{/* Priority and Quantity */}
<div className="flex items-center justify-between">
<div>
{getPriorityBadge(order.priority)}
</div>
<div className="text-right">
<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);
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedOrder(order);
setShowForm(true);
}}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</div>
</Card>
))}
</div>
</Card>
{/* Empty State */}
{filteredOrders.length === 0 && (
<div className="text-center py-12">
<ChefHat 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">
No se encontraron órdenes de producción
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Intenta ajustar la búsqueda o crear una nueva orden de producción
</p>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nueva Orden de Producción
</Button>
</div>
)}
</>
)}
{activeTab === 'batches' && (
@@ -493,6 +387,54 @@ const ProductionPage: React.FC = () => {
{activeTab === 'quality' && (
<QualityControl />
)}
{/* Production Order Form Modal - Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{selectedOrder ? 'Ver Orden de Producción' : 'Nueva Orden de Producción'}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
setShowForm(false);
setSelectedOrder(null);
}}
>
Cerrar
</Button>
</div>
{selectedOrder && (
<div className="space-y-4">
<h3 className="text-lg font-medium">{selectedOrder.recipeName}</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Cantidad:</span> {selectedOrder.quantity} unidades
</div>
<div>
<span className="font-medium">Asignado a:</span> {selectedOrder.assignedTo}
</div>
<div>
<span className="font-medium">Estado:</span> {selectedOrder.status}
</div>
<div>
<span className="font-medium">Progreso:</span> {selectedOrder.progress}%
</div>
<div>
<span className="font-medium">Inicio:</span> {new Date(selectedOrder.startTime).toLocaleString('es-ES')}
</div>
<div>
<span className="font-medium">Finalización:</span> {new Date(selectedOrder.estimatedCompletion).toLocaleString('es-ES')}
</div>
</div>
</div>
)}
</Card>
</div>
)}
</div>
);
};