Fix few issues

This commit is contained in:
Urtzi Alfaro
2025-09-26 12:12:17 +02:00
parent d573c38621
commit a27f159e24
32 changed files with 2694 additions and 575 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react';
import { Button, Card, Badge, Modal, Table, Select, Input, StatsGrid, StatusCard } from '../../../../components/ui';
import { Button, Badge, Modal, Table, Select, StatsGrid, StatusCard, SearchAndFilter, type FilterConfig, Card } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useToast } from '../../../../hooks/ui/useToast';
import { useCurrentTenant } from '../../../../stores/tenant.store';
@@ -265,31 +265,28 @@ const ModelsConfigPage: React.FC = () => {
</Card>
)}
{/* Filters */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<Input
placeholder="Buscar ingrediente..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="w-full sm:w-48">
<Select
value={statusFilter}
onChange={(value) => setStatusFilter(value as string)}
options={[
{ value: 'all', label: 'Todos los estados' },
{ value: 'no_model', label: 'Sin modelo' },
{ value: 'active', label: 'Activo' },
{ value: 'training', label: 'Entrenando' },
{ value: 'error', label: 'Error' },
]}
/>
</div>
</div>
</Card>
{/* Search and Filter Controls */}
<SearchAndFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="Buscar ingrediente..."
filters={[
{
key: 'status',
label: 'Estado',
type: 'dropdown',
value: statusFilter,
onChange: (value) => setStatusFilter(value as string),
placeholder: 'Todos los estados',
options: [
{ value: 'no_model', label: 'Sin modelo' },
{ value: 'active', label: 'Activo' },
{ value: 'training', label: 'Entrenando' },
{ value: 'error', label: 'Error' }
]
}
] as FilterConfig[]}
/>
{/* Models Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
@@ -345,6 +342,13 @@ const ModelsConfigPage: React.FC = () => {
label: 'Último entrenamiento',
value: new Date(status.lastTrainingDate).toLocaleDateString('es-ES')
} : undefined}
onClick={() => {
if (status.hasModel) {
handleViewModelDetails(status.ingredient);
} else {
handleStartTraining(status.ingredient);
}
}}
actions={[
// Primary action - View details or train model
{

View File

@@ -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
{

View File

@@ -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'),

View File

@@ -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',

View File

@@ -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={{

View File

@@ -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">

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -1,7 +1,7 @@
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Users, Plus, Search, Shield, Trash2, Crown, UserCheck } from 'lucide-react';
import { Button, Card, Input, StatusCard, getStatusColor, StatsGrid } from '../../../../components/ui';
import { Button, StatusCard, getStatusColor, StatsGrid, SearchAndFilter, type FilterConfig } from '../../../../components/ui';
import AddTeamMemberModal from '../../../../components/domain/team/AddTeamMemberModal';
import { PageHeader } from '../../../../components/layout';
import { useTeamMembers, useAddTeamMember, useRemoveTeamMember, useUpdateMemberRole, useTenantAccess } from '../../../../api/hooks/tenant';
@@ -345,38 +345,27 @@ const TeamPage: React.FC = () => {
gap="md"
/>
{/* Filters and Search */}
<Card className="p-6">
<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 text-[var(--text-tertiary)] h-4 w-4" />
<Input
placeholder="Buscar miembros del equipo..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2 flex-wrap">
{roles.map((role) => (
<button
key={role.value}
onClick={() => setSelectedRole(role.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
selectedRole === role.value
? 'bg-blue-600 text-white'
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
}`}
>
{role.label} ({role.count})
</button>
))}
</div>
</div>
</Card>
{/* Search and Filter Controls */}
<SearchAndFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="Buscar miembros del equipo..."
filters={[
{
key: 'role',
label: 'Rol',
type: 'buttons',
value: selectedRole,
onChange: (value) => setSelectedRole(value as string),
multiple: false,
options: roles.map(role => ({
value: role.value,
label: role.label,
count: role.count
}))
}
] as FilterConfig[]}
/>
{/* Add Member Button */}
{canManageTeam && filteredMembers.length > 0 && (