Fix few issues
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Plus, AlertTriangle, Package, CheckCircle, Eye, Clock, Euro, ArrowRight, Minus, Edit, Trash2, Archive, TrendingUp, History } from 'lucide-react';
|
||||
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor } from '../../../../components/ui';
|
||||
import { Button, StatsGrid, StatusCard, getStatusColor, SearchAndFilter, type FilterConfig, Card } from '../../../../components/ui';
|
||||
import { LoadingSpinner } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
@@ -20,6 +20,8 @@ import { IngredientResponse, StockCreate, StockMovementCreate, IngredientCreate
|
||||
|
||||
const InventoryPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<IngredientResponse | null>(null);
|
||||
|
||||
// Modal states for focused actions
|
||||
@@ -206,6 +208,19 @@ const InventoryPage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Apply status filter
|
||||
if (statusFilter) {
|
||||
items = items.filter(ingredient => {
|
||||
const status = getInventoryStatusConfig(ingredient);
|
||||
return status.text.toLowerCase().includes(statusFilter.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
// Apply category filter
|
||||
if (categoryFilter) {
|
||||
items = items.filter(ingredient => ingredient.category === categoryFilter);
|
||||
}
|
||||
|
||||
|
||||
// Sort by priority: expired → out of stock → low stock → normal → overstock
|
||||
// Within each priority level, sort by most critical items first
|
||||
@@ -259,7 +274,7 @@ const InventoryPage: React.FC = () => {
|
||||
|
||||
return aPriority - bPriority;
|
||||
});
|
||||
}, [ingredients, searchTerm]);
|
||||
}, [ingredients, searchTerm, statusFilter, categoryFilter]);
|
||||
|
||||
// Helper function to get category display name
|
||||
const getCategoryDisplayName = (category?: string): string => {
|
||||
@@ -502,19 +517,40 @@ const InventoryPage: React.FC = () => {
|
||||
|
||||
|
||||
|
||||
{/* Simplified Controls */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Buscar artículos por nombre, categoría o proveedor..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Search and Filter Controls */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder="Buscar artículos por nombre, categoría o proveedor..."
|
||||
filters={[
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Estado',
|
||||
type: 'dropdown',
|
||||
value: statusFilter,
|
||||
onChange: (value) => setStatusFilter(value as string),
|
||||
placeholder: 'Todos los estados',
|
||||
options: [
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'bajo', label: 'Stock Bajo' },
|
||||
{ value: 'sin stock', label: 'Sin Stock' },
|
||||
{ value: 'caducado', label: 'Caducado' },
|
||||
{ value: 'sobrestock', label: 'Sobrestock' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
label: 'Categoría',
|
||||
type: 'dropdown',
|
||||
value: categoryFilter,
|
||||
onChange: (value) => setCategoryFilter(value as string),
|
||||
placeholder: 'Todas las categorías',
|
||||
options: Array.from(new Set(ingredients.map(item => item.category)))
|
||||
.filter(Boolean)
|
||||
.map(category => ({ value: category, label: getCategoryDisplayName(category) }))
|
||||
}
|
||||
] as FilterConfig[]}
|
||||
/>
|
||||
|
||||
{/* Inventory Items Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -551,6 +587,7 @@ const InventoryPage: React.FC = () => {
|
||||
percentage: stockPercentage,
|
||||
color: statusConfig.color
|
||||
} : undefined}
|
||||
onClick={() => handleShowInfo(ingredient)}
|
||||
actions={[
|
||||
// Primary action - View item details
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, AlertTriangle, Settings, CheckCircle, Eye, Wrench, Thermometer, Activity, Search, Filter, Bell, History, Calendar, Edit, Trash2 } from 'lucide-react';
|
||||
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor } from '../../../../components/ui';
|
||||
import { Button, StatsGrid, StatusCard, getStatusColor, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { Badge } from '../../../../components/ui/Badge';
|
||||
import { LoadingSpinner } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
@@ -150,7 +150,8 @@ const MOCK_EQUIPMENT: Equipment[] = [
|
||||
const MaquinariaPage: React.FC = () => {
|
||||
const { t } = useTranslation(['equipment', 'common']);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<Equipment['status'] | 'all'>('all');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<Equipment | null>(null);
|
||||
const [showMaintenanceModal, setShowMaintenanceModal] = useState(false);
|
||||
const [showEquipmentModal, setShowEquipmentModal] = useState(false);
|
||||
@@ -231,11 +232,12 @@ const MaquinariaPage: React.FC = () => {
|
||||
eq.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
eq.type.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus = statusFilter === 'all' || eq.status === statusFilter;
|
||||
const matchesStatus = !statusFilter || eq.status === statusFilter;
|
||||
const matchesType = !typeFilter || eq.type === typeFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
return matchesSearch && matchesStatus && matchesType;
|
||||
});
|
||||
}, [MOCK_EQUIPMENT, searchTerm, statusFilter]);
|
||||
}, [MOCK_EQUIPMENT, searchTerm, statusFilter, typeFilter]);
|
||||
|
||||
const equipmentStats = useMemo(() => {
|
||||
const total = MOCK_EQUIPMENT.length;
|
||||
@@ -342,36 +344,44 @@ const MaquinariaPage: React.FC = () => {
|
||||
columns={3}
|
||||
/>
|
||||
|
||||
{/* Controls */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<Input
|
||||
placeholder={t('common:forms.search_placeholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="w-4 h-4 text-[var(--text-tertiary)]" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as Equipment['status'] | 'all')}
|
||||
className="px-3 py-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
>
|
||||
<option value="all">{t('common:forms.select_option')}</option>
|
||||
<option value="operational">{t('equipment_status.operational')}</option>
|
||||
<option value="warning">{t('equipment_status.warning')}</option>
|
||||
<option value="maintenance">{t('equipment_status.maintenance')}</option>
|
||||
<option value="down">{t('equipment_status.down')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Search and Filter Controls */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder={t('common:forms.search_placeholder')}
|
||||
filters={[
|
||||
{
|
||||
key: 'status',
|
||||
label: t('fields.status'),
|
||||
type: 'dropdown',
|
||||
value: statusFilter,
|
||||
onChange: (value) => setStatusFilter(value as string),
|
||||
placeholder: t('common:forms.select_option'),
|
||||
options: [
|
||||
{ value: 'operational', label: t('equipment_status.operational') },
|
||||
{ value: 'warning', label: t('equipment_status.warning') },
|
||||
{ value: 'maintenance', label: t('equipment_status.maintenance') },
|
||||
{ value: 'down', label: t('equipment_status.down') }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: 'Tipo',
|
||||
type: 'dropdown',
|
||||
value: typeFilter,
|
||||
onChange: (value) => setTypeFilter(value as string),
|
||||
placeholder: 'Todos los tipos',
|
||||
options: [
|
||||
{ value: 'oven', label: 'Horno' },
|
||||
{ value: 'mixer', label: 'Batidora' },
|
||||
{ value: 'proofer', label: 'Fermentadora' },
|
||||
{ value: 'freezer', label: 'Congelador' },
|
||||
{ value: 'packaging', label: 'Empaquetado' },
|
||||
{ value: 'other', label: 'Otro' }
|
||||
]
|
||||
}
|
||||
] as FilterConfig[]}
|
||||
/>
|
||||
|
||||
{/* Equipment Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -397,6 +407,7 @@ const MaquinariaPage: React.FC = () => {
|
||||
label: t('fields.uptime'),
|
||||
value: `${equipment.uptime.toFixed(1)}%`
|
||||
}}
|
||||
onClick={() => handleShowMaintenanceDetails(equipment)}
|
||||
actions={[
|
||||
{
|
||||
label: t('actions.view_details'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Loader, Euro } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal, Tabs } from '../../../../components/ui';
|
||||
import { Button, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal, Tabs, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import {
|
||||
@@ -28,6 +28,7 @@ import { useTranslation } from 'react-i18next';
|
||||
const OrdersPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'orders' | 'customers'>('orders');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
|
||||
@@ -119,8 +120,10 @@ const OrdersPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredOrders = orders.filter(order => {
|
||||
return order.order_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
order.id.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesSearch = order.order_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
order.id.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = !statusFilter || order.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const filteredCustomers = customers.filter(customer => {
|
||||
@@ -321,19 +324,29 @@ const OrdersPage: React.FC = () => {
|
||||
columns={3}
|
||||
/>
|
||||
|
||||
{/* Simplified Controls */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder={activeTab === 'orders' ? 'Buscar pedidos por número o ID...' : 'Buscar clientes por nombre, código o email...'}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Search and Filter Controls */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder={activeTab === 'orders'
|
||||
? 'Buscar pedidos por número o ID...'
|
||||
: 'Buscar clientes por nombre, código o email...'
|
||||
}
|
||||
filters={activeTab === 'orders' ? [
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Estado',
|
||||
type: 'dropdown',
|
||||
value: statusFilter,
|
||||
onChange: (value) => setStatusFilter(value as string),
|
||||
placeholder: 'Todos los estados',
|
||||
options: Object.values(OrderStatus).map(status => ({
|
||||
value: status,
|
||||
label: t(`orders:order_status.${status.toLowerCase()}`)
|
||||
}))
|
||||
}
|
||||
] as FilterConfig[] : []}
|
||||
/>
|
||||
|
||||
{/* Content Grid - Mobile-first responsive */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -367,6 +380,12 @@ const OrdersPage: React.FC = () => {
|
||||
`${t(`orders:delivery_methods.${order.delivery_method.toLowerCase()}`)}`,
|
||||
...(paymentNote ? [paymentNote] : [])
|
||||
]}
|
||||
onClick={() => {
|
||||
setSelectedOrder(order);
|
||||
setIsCreating(false);
|
||||
setModalMode('view');
|
||||
setShowForm(true);
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
label: 'Ver Detalles',
|
||||
@@ -418,6 +437,12 @@ const OrdersPage: React.FC = () => {
|
||||
customer.email || 'Sin email',
|
||||
`Desde ${new Date(customer.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' })}`
|
||||
]}
|
||||
onClick={() => {
|
||||
setSelectedCustomer(customer);
|
||||
setIsCreating(false);
|
||||
setModalMode('view');
|
||||
setShowForm(true);
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
label: 'Ver Detalles',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, ShoppingCart, Truck, Euro, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, X, Save, Building2, Play, Zap, User } from 'lucide-react';
|
||||
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, EditViewModal } from '../../../../components/ui';
|
||||
import { Button, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { CreatePurchaseOrderModal } from '../../../../components/domain/procurement/CreatePurchaseOrderModal';
|
||||
@@ -16,6 +16,7 @@ import { useTenantStore } from '../../../../stores/tenant.store';
|
||||
|
||||
const ProcurementPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [selectedPlan, setSelectedPlan] = useState<any>(null);
|
||||
@@ -199,8 +200,10 @@ const ProcurementPage: React.FC = () => {
|
||||
const matchesSearch = plan.plan_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
plan.status.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(plan.special_requirements && plan.special_requirements.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
return matchesSearch;
|
||||
|
||||
const matchesStatus = !statusFilter || plan.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
}) || [];
|
||||
|
||||
|
||||
@@ -350,18 +353,29 @@ const ProcurementPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Buscar planes por número, estado o notas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder="Buscar planes por número, estado o notas..."
|
||||
filters={[
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Estado',
|
||||
type: 'dropdown',
|
||||
value: statusFilter,
|
||||
onChange: (value) => setStatusFilter(value as string),
|
||||
placeholder: 'Todos los estados',
|
||||
options: [
|
||||
{ value: 'draft', label: 'Borrador' },
|
||||
{ value: 'pending_approval', label: 'Pendiente Aprobación' },
|
||||
{ value: 'approved', label: 'Aprobado' },
|
||||
{ value: 'in_execution', label: 'En Ejecución' },
|
||||
{ value: 'completed', label: 'Completado' },
|
||||
{ value: 'cancelled', label: 'Cancelado' }
|
||||
]
|
||||
}
|
||||
] as FilterConfig[]}
|
||||
/>
|
||||
|
||||
{/* Procurement Plans Grid - Mobile-Optimized */}
|
||||
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3">
|
||||
@@ -461,9 +475,9 @@ const ProcurementPage: React.FC = () => {
|
||||
id={plan.plan_number}
|
||||
statusIndicator={statusConfig}
|
||||
title={`Plan ${plan.plan_number}`}
|
||||
subtitle={`${new Date(plan.plan_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' })} • ${plan.procurement_strategy}`}
|
||||
subtitle={`${new Date(plan.plan_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })} • ${plan.procurement_strategy}`}
|
||||
primaryValue={plan.total_requirements}
|
||||
primaryValueLabel="requerimientos"
|
||||
primaryValueLabel="reqs"
|
||||
secondaryInfo={{
|
||||
label: 'Presupuesto',
|
||||
value: `€${formatters.compact(plan.total_estimated_cost)}`
|
||||
@@ -476,7 +490,7 @@ const ProcurementPage: React.FC = () => {
|
||||
metadata={[
|
||||
`Período: ${new Date(plan.plan_period_start).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })} - ${new Date(plan.plan_period_end).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
|
||||
`Creado: ${new Date(plan.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' })}`,
|
||||
...(plan.special_requirements ? [`Req. especiales: ${plan.special_requirements}`] : [])
|
||||
...(plan.special_requirements ? [`Especiales: ${plan.special_requirements.length > 30 ? plan.special_requirements.substring(0, 30) + '...' : plan.special_requirements}`] : [])
|
||||
]}
|
||||
actions={actions}
|
||||
/>
|
||||
@@ -639,7 +653,7 @@ const ProcurementPage: React.FC = () => {
|
||||
isCritical: true
|
||||
}}
|
||||
title={requirement.product_name}
|
||||
subtitle={`${requirement.requirement_number} • ${requirement.supplier_name || 'Sin proveedor'} • Plan: ${filteredPlans.find(p => p.id === selectedPlanForRequirements)?.plan_number || 'N/A'}`}
|
||||
subtitle={`${requirement.requirement_number} • ${requirement.supplier_name || 'Sin proveedor'}`}
|
||||
primaryValue={requirement.required_quantity}
|
||||
primaryValueLabel={requirement.unit_of_measure}
|
||||
secondaryInfo={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Plus, Clock, AlertCircle, CheckCircle, Timer, ChefHat, Eye, Edit, Package, Zap, User, PlusCircle } from 'lucide-react';
|
||||
import { Button, Input, Card, StatsGrid, EditViewModal, Toggle } from '../../../../components/ui';
|
||||
import { Button, StatsGrid, EditViewModal, Toggle, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { statusColors } from '../../../../styles/colors';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { LoadingSpinner } from '../../../../components/ui';
|
||||
@@ -28,6 +28,8 @@ import { ProcessStage } from '../../../../api/types/qualityTemplates';
|
||||
|
||||
const ProductionPage: React.FC = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [priorityFilter, setPriorityFilter] = useState('');
|
||||
const [selectedBatch, setSelectedBatch] = useState<ProductionBatchResponse | null>(null);
|
||||
const [showBatchModal, setShowBatchModal] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
@@ -200,17 +202,32 @@ const ProductionPage: React.FC = () => {
|
||||
const batches = activeBatchesData?.batches || [];
|
||||
|
||||
const filteredBatches = useMemo(() => {
|
||||
if (!searchQuery) return batches;
|
||||
let filtered = batches;
|
||||
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
return batches.filter(batch =>
|
||||
batch.product_name.toLowerCase().includes(searchLower) ||
|
||||
batch.batch_number.toLowerCase().includes(searchLower) ||
|
||||
(batch.staff_assigned && batch.staff_assigned.some(staff =>
|
||||
staff.toLowerCase().includes(searchLower)
|
||||
))
|
||||
);
|
||||
}, [batches, searchQuery]);
|
||||
// Apply search filter
|
||||
if (searchQuery) {
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(batch =>
|
||||
batch.product_name.toLowerCase().includes(searchLower) ||
|
||||
batch.batch_number.toLowerCase().includes(searchLower) ||
|
||||
(batch.staff_assigned && batch.staff_assigned.some(staff =>
|
||||
staff.toLowerCase().includes(searchLower)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply status filter
|
||||
if (statusFilter) {
|
||||
filtered = filtered.filter(batch => batch.status === statusFilter);
|
||||
}
|
||||
|
||||
// Apply priority filter
|
||||
if (priorityFilter) {
|
||||
filtered = filtered.filter(batch => batch.priority === priorityFilter);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [batches, searchQuery, statusFilter, priorityFilter]);
|
||||
|
||||
// Calculate production stats from real data
|
||||
const productionStats = useMemo(() => {
|
||||
@@ -362,19 +379,38 @@ const ProductionPage: React.FC = () => {
|
||||
|
||||
{/* Production Batches Section - No tabs needed */}
|
||||
<>
|
||||
{/* Search Controls */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Buscar lotes por producto, número de lote o personal..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Search and Filter Controls */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
searchPlaceholder="Buscar lotes por producto, número de lote o personal..."
|
||||
filters={[
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Estado',
|
||||
type: 'dropdown',
|
||||
value: statusFilter,
|
||||
onChange: (value) => setStatusFilter(value as string),
|
||||
placeholder: 'Todos los estados',
|
||||
options: Object.values(ProductionStatusEnum).map(status => ({
|
||||
value: status,
|
||||
label: status.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||
}))
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
label: 'Prioridad',
|
||||
type: 'dropdown',
|
||||
value: priorityFilter,
|
||||
onChange: (value) => setPriorityFilter(value as string),
|
||||
placeholder: 'Todas las prioridades',
|
||||
options: Object.values(ProductionPriorityEnum).map(priority => ({
|
||||
value: priority,
|
||||
label: priority.charAt(0).toUpperCase() + priority.slice(1).toLowerCase()
|
||||
}))
|
||||
}
|
||||
] as FilterConfig[]}
|
||||
/>
|
||||
|
||||
{/* Production Batches Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Plus, Star, Clock, Euro, Package, Eye, Edit, ChefHat, Timer, CheckCircle } from 'lucide-react';
|
||||
import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, EditViewModal } from '../../../../components/ui';
|
||||
import { Button, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { LoadingSpinner } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
@@ -14,6 +14,9 @@ import { QualityCheckConfigurationModal } from '../../../../components/domain/re
|
||||
|
||||
const RecipesPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState('');
|
||||
const [difficultyFilter, setDifficultyFilter] = useState('');
|
||||
const [isSignatureOnly, setIsSignatureOnly] = useState(false);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [selectedRecipe, setSelectedRecipe] = useState<RecipeResponse | null>(null);
|
||||
@@ -104,15 +107,35 @@ const RecipesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredRecipes = useMemo(() => {
|
||||
if (!searchTerm) return recipes;
|
||||
let filtered = recipes;
|
||||
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return recipes.filter(recipe =>
|
||||
recipe.name.toLowerCase().includes(searchLower) ||
|
||||
(recipe.description && recipe.description.toLowerCase().includes(searchLower)) ||
|
||||
(recipe.category && recipe.category.toLowerCase().includes(searchLower))
|
||||
);
|
||||
}, [recipes, searchTerm]);
|
||||
// Apply search filter
|
||||
if (searchTerm) {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(recipe =>
|
||||
recipe.name.toLowerCase().includes(searchLower) ||
|
||||
(recipe.description && recipe.description.toLowerCase().includes(searchLower)) ||
|
||||
(recipe.category && recipe.category.toLowerCase().includes(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply category filter
|
||||
if (categoryFilter) {
|
||||
filtered = filtered.filter(recipe => recipe.category === categoryFilter);
|
||||
}
|
||||
|
||||
// Apply difficulty filter
|
||||
if (difficultyFilter) {
|
||||
filtered = filtered.filter(recipe => recipe.difficulty_level?.toString() === difficultyFilter);
|
||||
}
|
||||
|
||||
// Apply signature filter
|
||||
if (isSignatureOnly) {
|
||||
filtered = filtered.filter(recipe => recipe.is_signature_item);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [recipes, searchTerm, categoryFilter, difficultyFilter, isSignatureOnly]);
|
||||
|
||||
const recipeStats = useMemo(() => {
|
||||
const stats = {
|
||||
@@ -454,19 +477,51 @@ const RecipesPage: React.FC = () => {
|
||||
columns={3}
|
||||
/>
|
||||
|
||||
{/* Simplified Controls */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Buscar recetas por nombre, descripción o categoría..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Search and Filter Controls */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder="Buscar recetas por nombre, descripción o categoría..."
|
||||
filters={[
|
||||
{
|
||||
key: 'category',
|
||||
label: 'Categoría',
|
||||
type: 'dropdown',
|
||||
value: categoryFilter,
|
||||
onChange: (value) => setCategoryFilter(value as string),
|
||||
placeholder: 'Todas las categorías',
|
||||
options: [
|
||||
{ value: 'bread', label: 'Pan' },
|
||||
{ value: 'pastry', label: 'Bollería' },
|
||||
{ value: 'cake', label: 'Tarta' },
|
||||
{ value: 'cookie', label: 'Galleta' },
|
||||
{ value: 'other', label: 'Otro' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'difficulty',
|
||||
label: 'Dificultad',
|
||||
type: 'dropdown',
|
||||
value: difficultyFilter,
|
||||
onChange: (value) => setDifficultyFilter(value as string),
|
||||
placeholder: 'Todas las dificultades',
|
||||
options: [
|
||||
{ value: '1', label: 'Nivel 1 - Fácil' },
|
||||
{ value: '2', label: 'Nivel 2 - Medio' },
|
||||
{ value: '3', label: 'Nivel 3 - Difícil' },
|
||||
{ value: '4', label: 'Nivel 4 - Muy Difícil' },
|
||||
{ value: '5', label: 'Nivel 5 - Extremo' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'signature',
|
||||
label: 'Solo recetas especiales',
|
||||
type: 'checkbox',
|
||||
value: isSignatureOnly,
|
||||
onChange: (value) => setIsSignatureOnly(value as boolean)
|
||||
}
|
||||
] as FilterConfig[]}
|
||||
/>
|
||||
|
||||
{/* Recipes Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -500,6 +555,11 @@ const RecipesPage: React.FC = () => {
|
||||
`Rendimiento: ${recipe.yield_quantity} ${recipe.yield_unit}`,
|
||||
`${recipe.ingredients?.length || 0} ingredientes principales`
|
||||
]}
|
||||
onClick={() => {
|
||||
setSelectedRecipe(recipe);
|
||||
setModalMode('view');
|
||||
setShowForm(true);
|
||||
}}
|
||||
actions={[
|
||||
// Primary action - View recipe details
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Building2, Phone, Mail, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Euro, Loader } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal } from '../../../../components/ui';
|
||||
import { Button, Badge, StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { SupplierStatus, SupplierType, PaymentTerms } from '../../../../api/types/suppliers';
|
||||
@@ -12,6 +12,8 @@ import { useTranslation } from 'react-i18next';
|
||||
const SuppliersPage: React.FC = () => {
|
||||
const [activeTab] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<any>(null);
|
||||
@@ -72,8 +74,12 @@ const SuppliersPage: React.FC = () => {
|
||||
return t(`suppliers:payment_terms.${terms.toLowerCase()}`);
|
||||
};
|
||||
|
||||
// Filtering is now handled by the API query parameters
|
||||
const filteredSuppliers = suppliers;
|
||||
// Apply additional client-side filtering
|
||||
const filteredSuppliers = suppliers.filter(supplier => {
|
||||
const matchesStatus = !statusFilter || supplier.status === statusFilter;
|
||||
const matchesType = !typeFilter || supplier.supplier_type === typeFilter;
|
||||
return matchesStatus && matchesType;
|
||||
});
|
||||
|
||||
const supplierStats = statisticsData || {
|
||||
total_suppliers: 0,
|
||||
@@ -191,19 +197,38 @@ const SuppliersPage: React.FC = () => {
|
||||
columns={3}
|
||||
/>
|
||||
|
||||
{/* Simplified Controls */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Buscar proveedores por nombre, código, email o contacto..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* Search and Filter Controls */}
|
||||
<SearchAndFilter
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder="Buscar proveedores por nombre, código, email o contacto..."
|
||||
filters={[
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Estado',
|
||||
type: 'dropdown',
|
||||
value: statusFilter,
|
||||
onChange: (value) => setStatusFilter(value as string),
|
||||
placeholder: 'Todos los estados',
|
||||
options: Object.values(SupplierStatus).map(status => ({
|
||||
value: status,
|
||||
label: t(`suppliers:status.${status.toLowerCase()}`)
|
||||
}))
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: 'Tipo',
|
||||
type: 'dropdown',
|
||||
value: typeFilter,
|
||||
onChange: (value) => setTypeFilter(value as string),
|
||||
placeholder: 'Todos los tipos',
|
||||
options: Object.values(SupplierType).map(type => ({
|
||||
value: type,
|
||||
label: t(`suppliers:types.${type.toLowerCase()}`)
|
||||
}))
|
||||
}
|
||||
] as FilterConfig[]}
|
||||
/>
|
||||
|
||||
{/* Suppliers Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -229,6 +254,12 @@ const SuppliersPage: React.FC = () => {
|
||||
supplier.phone || 'Sin teléfono',
|
||||
`Creado: ${new Date(supplier.created_at).toLocaleDateString('es-ES')}`
|
||||
]}
|
||||
onClick={() => {
|
||||
setSelectedSupplier(supplier);
|
||||
setIsCreating(false);
|
||||
setModalMode('view');
|
||||
setShowForm(true);
|
||||
}}
|
||||
actions={[
|
||||
// Primary action - View supplier details
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user