Improve the frontend and repository layer

This commit is contained in:
Urtzi Alfaro
2025-10-23 07:44:54 +02:00
parent 8d30172483
commit 07c33fa578
112 changed files with 14726 additions and 2733 deletions

View File

@@ -0,0 +1,299 @@
import React, { useState } from 'react';
import { Settings, Save, RotateCcw, AlertCircle, Loader } from 'lucide-react';
import { Button, Card } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useToast } from '../../../../hooks/ui/useToast';
import { useSettings, useUpdateSettings } from '../../../../api/hooks/settings';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import type {
TenantSettings,
ProcurementSettings,
InventorySettings,
ProductionSettings,
SupplierSettings,
POSSettings,
OrderSettings,
} from '../../../../api/types/settings';
import ProcurementSettingsCard from './cards/ProcurementSettingsCard';
import InventorySettingsCard from './cards/InventorySettingsCard';
import ProductionSettingsCard from './cards/ProductionSettingsCard';
import SupplierSettingsCard from './cards/SupplierSettingsCard';
import POSSettingsCard from './cards/POSSettingsCard';
import OrderSettingsCard from './cards/OrderSettingsCard';
const AjustesPage: React.FC = () => {
const { addToast } = useToast();
const currentTenant = useCurrentTenant();
const tenantId = currentTenant?.id || '';
const { data: settings, isLoading, error, isFetching } = useSettings(tenantId, {
enabled: !!tenantId,
retry: 2,
staleTime: 5 * 60 * 100,
});
// Debug logging
React.useEffect(() => {
console.log('🔍 AjustesPage - tenantId:', tenantId);
console.log('🔍 AjustesPage - settings:', settings);
console.log('🔍 AjustesPage - isLoading:', isLoading);
console.log('🔍 AjustesPage - isFetching:', isFetching);
console.log('🔍 AjustesPage - error:', error);
}, [tenantId, settings, isLoading, isFetching, error]);
const updateSettingsMutation = useUpdateSettings();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// Local state for each category
const [procurementSettings, setProcurementSettings] = useState<ProcurementSettings | null>(null);
const [inventorySettings, setInventorySettings] = useState<InventorySettings | null>(null);
const [productionSettings, setProductionSettings] = useState<ProductionSettings | null>(null);
const [supplierSettings, setSupplierSettings] = useState<SupplierSettings | null>(null);
const [posSettings, setPosSettings] = useState<POSSettings | null>(null);
const [orderSettings, setOrderSettings] = useState<OrderSettings | null>(null);
// Load settings into local state when data is fetched
React.useEffect(() => {
if (settings) {
setProcurementSettings(settings.procurement_settings);
setInventorySettings(settings.inventory_settings);
setProductionSettings(settings.production_settings);
setSupplierSettings(settings.supplier_settings);
setPosSettings(settings.pos_settings);
setOrderSettings(settings.order_settings);
setHasUnsavedChanges(false);
}
}, [settings]);
const handleSaveAll = async () => {
if (!tenantId || !procurementSettings || !inventorySettings || !productionSettings ||
!supplierSettings || !posSettings || !orderSettings) {
return;
}
setIsSaving(true);
try {
await updateSettingsMutation.mutateAsync({
tenantId,
updates: {
procurement_settings: procurementSettings,
inventory_settings: inventorySettings,
production_settings: productionSettings,
supplier_settings: supplierSettings,
pos_settings: posSettings,
order_settings: orderSettings,
},
});
setHasUnsavedChanges(false);
addToast('Ajustes guardados correctamente', { type: 'success' });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
addToast(`Error al guardar ajustes: ${errorMessage}`, { type: 'error' });
} finally {
setIsSaving(false);
}
};
const handleResetAll = () => {
if (settings) {
setProcurementSettings(settings.procurement_settings);
setInventorySettings(settings.inventory_settings);
setProductionSettings(settings.production_settings);
setSupplierSettings(settings.supplier_settings);
setPosSettings(settings.pos_settings);
setOrderSettings(settings.order_settings);
setHasUnsavedChanges(false);
}
};
const handleCategoryChange = (category: string) => {
setHasUnsavedChanges(true);
};
if (isLoading || !currentTenant) {
return (
<div className="p-6 space-y-6">
<PageHeader
title="Ajustes"
description="Configura los parámetros operativos de tu panadería"
/>
<div className="flex items-center justify-center h-64">
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
<span className="ml-2 text-[var(--text-secondary)]">Cargando ajustes...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6 space-y-6">
<PageHeader
title="Ajustes"
description="Error al cargar los ajustes"
/>
<Card className="p-6">
<div className="text-red-600">
Error al cargar los ajustes: {error.message || 'Error desconocido'}
</div>
</Card>
</div>
);
}
return (
<div className="p-6 space-y-6 pb-32">
<PageHeader
title="Ajustes"
description="Configura los parámetros operativos de tu panadería"
/>
{/* Top Action Bar */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<Settings className="w-4 h-4 text-[var(--color-primary)]" />
<span className="text-[var(--text-secondary)]">
Ajusta los parámetros según las necesidades de tu negocio
</span>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleResetAll}
disabled={!hasUnsavedChanges || isSaving}
>
<RotateCcw className="w-4 h-4" />
Restablecer Todo
</Button>
<Button
variant="primary"
size="sm"
onClick={handleSaveAll}
isLoading={isSaving}
disabled={!hasUnsavedChanges}
loadingText="Guardando..."
>
<Save className="w-4 h-4" />
Guardar Cambios
</Button>
</div>
</div>
{/* Settings Categories */}
<div className="space-y-6">
{/* Procurement Settings */}
{procurementSettings && (
<ProcurementSettingsCard
settings={procurementSettings}
onChange={(newSettings) => {
setProcurementSettings(newSettings);
handleCategoryChange('procurement');
}}
disabled={isSaving}
/>
)}
{/* Inventory Settings */}
{inventorySettings && (
<InventorySettingsCard
settings={inventorySettings}
onChange={(newSettings) => {
setInventorySettings(newSettings);
handleCategoryChange('inventory');
}}
disabled={isSaving}
/>
)}
{/* Production Settings */}
{productionSettings && (
<ProductionSettingsCard
settings={productionSettings}
onChange={(newSettings) => {
setProductionSettings(newSettings);
handleCategoryChange('production');
}}
disabled={isSaving}
/>
)}
{/* Supplier Settings */}
{supplierSettings && (
<SupplierSettingsCard
settings={supplierSettings}
onChange={(newSettings) => {
setSupplierSettings(newSettings);
handleCategoryChange('supplier');
}}
disabled={isSaving}
/>
)}
{/* POS Settings */}
{posSettings && (
<POSSettingsCard
settings={posSettings}
onChange={(newSettings) => {
setPosSettings(newSettings);
handleCategoryChange('pos');
}}
disabled={isSaving}
/>
)}
{/* Order Settings */}
{orderSettings && (
<OrderSettingsCard
settings={orderSettings}
onChange={(newSettings) => {
setOrderSettings(newSettings);
handleCategoryChange('order');
}}
disabled={isSaving}
/>
)}
</div>
{/* Floating Save Banner */}
{hasUnsavedChanges && (
<div className="fixed bottom-6 right-6 z-50">
<Card className="p-4 shadow-lg border-2 border-[var(--color-primary)]">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
<AlertCircle className="w-4 h-4 text-yellow-500" />
Tienes cambios sin guardar
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleResetAll}
disabled={isSaving}
>
<RotateCcw className="w-4 h-4" />
Descartar
</Button>
<Button
variant="primary"
size="sm"
onClick={handleSaveAll}
isLoading={isSaving}
loadingText="Guardando..."
>
<Save className="w-4 h-4" />
Guardar
</Button>
</div>
</div>
</Card>
</div>
)}
</div>
);
};
export default AjustesPage;

View File

@@ -0,0 +1,280 @@
import React from 'react';
import { Package, AlertCircle, Thermometer, Clock } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { InventorySettings } from '../../../../../api/types/settings';
interface InventorySettingsCardProps {
settings: InventorySettings;
onChange: (settings: InventorySettings) => void;
disabled?: boolean;
}
const InventorySettingsCard: React.FC<InventorySettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const handleChange = (field: keyof InventorySettings) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.type === 'checkbox' ? e.target.checked :
e.target.type === 'number' ? parseFloat(e.target.value) :
e.target.value;
onChange({ ...settings, [field]: value });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<Package className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
Gestión de Inventario
</h3>
<div className="space-y-6">
{/* Stock Management */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Package className="w-4 h-4 mr-2" />
Control de Stock
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
<Input
type="number"
label="Umbral de Stock Bajo"
value={settings.low_stock_threshold}
onChange={handleChange('low_stock_threshold')}
disabled={disabled}
min={1}
max={1000}
step={1}
placeholder="10"
/>
<Input
type="number"
label="Punto de Reorden"
value={settings.reorder_point}
onChange={handleChange('reorder_point')}
disabled={disabled}
min={1}
max={1000}
step={1}
placeholder="20"
/>
<Input
type="number"
label="Cantidad de Reorden"
value={settings.reorder_quantity}
onChange={handleChange('reorder_quantity')}
disabled={disabled}
min={1}
max={1000}
step={1}
placeholder="50"
/>
</div>
</div>
{/* Expiration Management */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Clock className="w-4 h-4 mr-2" />
Gestión de Caducidad
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
<Input
type="number"
label="Días para 'Próximo a Caducar'"
value={settings.expiring_soon_days}
onChange={handleChange('expiring_soon_days')}
disabled={disabled}
min={1}
max={30}
step={1}
placeholder="7"
/>
<Input
type="number"
label="Días para Alerta de Caducidad"
value={settings.expiration_warning_days}
onChange={handleChange('expiration_warning_days')}
disabled={disabled}
min={1}
max={14}
step={1}
placeholder="3"
/>
<Input
type="number"
label="Umbral de Calidad (0-10)"
value={settings.quality_score_threshold}
onChange={handleChange('quality_score_threshold')}
disabled={disabled}
min={0}
max={10}
step={0.1}
placeholder="8.0"
/>
</div>
</div>
{/* Temperature Monitoring */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Thermometer className="w-4 h-4 mr-2" />
Monitorización de Temperatura
</h4>
<div className="space-y-4 pl-6">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="temperature_monitoring_enabled"
checked={settings.temperature_monitoring_enabled}
onChange={handleChange('temperature_monitoring_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="temperature_monitoring_enabled" className="text-sm text-[var(--text-secondary)]">
Habilitar monitorización de temperatura
</label>
</div>
{settings.temperature_monitoring_enabled && (
<>
{/* Refrigeration */}
<div>
<label className="block text-xs font-medium text-[var(--text-tertiary)] mb-2">
Refrigeración (°C)
</label>
<div className="grid grid-cols-2 gap-4">
<Input
type="number"
label="Temperatura Mínima"
value={settings.refrigeration_temp_min}
onChange={handleChange('refrigeration_temp_min')}
disabled={disabled}
min={-5}
max={10}
step={0.5}
placeholder="1.0"
/>
<Input
type="number"
label="Temperatura Máxima"
value={settings.refrigeration_temp_max}
onChange={handleChange('refrigeration_temp_max')}
disabled={disabled}
min={-5}
max={10}
step={0.5}
placeholder="4.0"
/>
</div>
</div>
{/* Freezer */}
<div>
<label className="block text-xs font-medium text-[var(--text-tertiary)] mb-2">
Congelador (°C)
</label>
<div className="grid grid-cols-2 gap-4">
<Input
type="number"
label="Temperatura Mínima"
value={settings.freezer_temp_min}
onChange={handleChange('freezer_temp_min')}
disabled={disabled}
min={-30}
max={0}
step={1}
placeholder="-20.0"
/>
<Input
type="number"
label="Temperatura Máxima"
value={settings.freezer_temp_max}
onChange={handleChange('freezer_temp_max')}
disabled={disabled}
min={-30}
max={0}
step={1}
placeholder="-15.0"
/>
</div>
</div>
{/* Room Temperature */}
<div>
<label className="block text-xs font-medium text-[var(--text-tertiary)] mb-2">
Temperatura Ambiente (°C)
</label>
<div className="grid grid-cols-2 gap-4">
<Input
type="number"
label="Temperatura Mínima"
value={settings.room_temp_min}
onChange={handleChange('room_temp_min')}
disabled={disabled}
min={10}
max={35}
step={1}
placeholder="18.0"
/>
<Input
type="number"
label="Temperatura Máxima"
value={settings.room_temp_max}
onChange={handleChange('room_temp_max')}
disabled={disabled}
min={10}
max={35}
step={1}
placeholder="25.0"
/>
</div>
</div>
{/* Alert Timing */}
<div>
<h5 className="text-xs font-medium text-[var(--text-tertiary)] mb-2 flex items-center">
<AlertCircle className="w-3 h-3 mr-1" />
Alertas de Desviación
</h5>
<div className="grid grid-cols-2 gap-4">
<Input
type="number"
label="Desviación Normal (minutos)"
value={settings.temp_deviation_alert_minutes}
onChange={handleChange('temp_deviation_alert_minutes')}
disabled={disabled}
min={1}
max={60}
step={1}
placeholder="15"
/>
<Input
type="number"
label="Desviación Crítica (minutos)"
value={settings.critical_temp_deviation_minutes}
onChange={handleChange('critical_temp_deviation_minutes')}
disabled={disabled}
min={1}
max={30}
step={1}
placeholder="5"
/>
</div>
</div>
</>
)}
</div>
</div>
</div>
</Card>
);
};
export default InventorySettingsCard;

View File

@@ -0,0 +1,150 @@
import React from 'react';
import { ShoppingBag, Tag, Clock, TrendingUp, MapPin } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { OrderSettings } from '../../../../../api/types/settings';
interface OrderSettingsCardProps {
settings: OrderSettings;
onChange: (settings: OrderSettings) => void;
disabled?: boolean;
}
const OrderSettingsCard: React.FC<OrderSettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const handleChange = (field: keyof OrderSettings) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.type === 'checkbox' ? e.target.checked :
e.target.type === 'number' ? parseFloat(e.target.value) :
e.target.value;
onChange({ ...settings, [field]: value });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<ShoppingBag className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
Pedidos y Reglas de Negocio
</h3>
<div className="space-y-6">
{/* Discount & Pricing */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Tag className="w-4 h-4 mr-2" />
Descuentos y Precios
</h4>
<div className="space-y-4 pl-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
type="number"
label="Descuento Máximo (%)"
value={settings.max_discount_percentage}
onChange={handleChange('max_discount_percentage')}
disabled={disabled}
min={0}
max={100}
step={1}
placeholder="50.0"
helperText="Porcentaje máximo de descuento permitido en pedidos"
/>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="discount_enabled"
checked={settings.discount_enabled}
onChange={handleChange('discount_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="discount_enabled" className="text-sm text-[var(--text-secondary)]">
Habilitar descuentos en pedidos
</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="dynamic_pricing_enabled"
checked={settings.dynamic_pricing_enabled}
onChange={handleChange('dynamic_pricing_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="dynamic_pricing_enabled" className="text-sm text-[var(--text-secondary)]">
Habilitar precios dinámicos
</label>
</div>
</div>
</div>
</div>
{/* Delivery Settings */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<MapPin className="w-4 h-4 mr-2" />
Configuración de Entrega
</h4>
<div className="space-y-4 pl-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
type="number"
label="Ventana de Entrega Predeterminada (horas)"
value={settings.default_delivery_window_hours}
onChange={handleChange('default_delivery_window_hours')}
disabled={disabled}
min={1}
max={168}
step={1}
placeholder="48"
helperText="Tiempo predeterminado para la entrega de pedidos"
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="delivery_tracking_enabled"
checked={settings.delivery_tracking_enabled}
onChange={handleChange('delivery_tracking_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="delivery_tracking_enabled" className="text-sm text-[var(--text-secondary)]">
Habilitar seguimiento de entregas
</label>
</div>
</div>
</div>
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<TrendingUp className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="flex-1">
<h5 className="text-sm font-semibold text-blue-900 mb-1">
Reglas de Negocio
</h5>
<p className="text-xs text-blue-700 mb-2">
Estos ajustes controlan las reglas de negocio que se aplican a los pedidos.
</p>
<ul className="text-xs text-blue-700 space-y-1 list-disc list-inside">
<li><strong>Precios dinámicos:</strong> Ajusta automáticamente los precios según demanda, inventario y otros factores</li>
<li><strong>Descuentos:</strong> Permite aplicar descuentos a productos y pedidos dentro del límite establecido</li>
<li><strong>Seguimiento de entregas:</strong> Permite a los clientes rastrear sus pedidos en tiempo real</li>
</ul>
</div>
</div>
</div>
</div>
</Card>
);
};
export default OrderSettingsCard;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { Smartphone, RefreshCw, Clock } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { POSSettings } from '../../../../../api/types/settings';
interface POSSettingsCardProps {
settings: POSSettings;
onChange: (settings: POSSettings) => void;
disabled?: boolean;
}
const POSSettingsCard: React.FC<POSSettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const handleChange = (field: keyof POSSettings) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.type === 'checkbox' ? e.target.checked :
e.target.type === 'number' ? parseInt(e.target.value, 10) :
e.target.value;
onChange({ ...settings, [field]: value });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<Smartphone className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
Punto de Venta (POS)
</h3>
<div className="space-y-6">
{/* Sync Settings */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<RefreshCw className="w-4 h-4 mr-2" />
Sincronización
</h4>
<div className="space-y-4 pl-6">
<Input
type="number"
label="Intervalo de Sincronización (minutos)"
value={settings.sync_interval_minutes}
onChange={handleChange('sync_interval_minutes')}
disabled={disabled}
min={1}
max={60}
step={1}
placeholder="5"
helperText="Frecuencia con la que se sincroniza el POS con el sistema central"
/>
<div className="space-y-3">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="auto_sync_products"
checked={settings.auto_sync_products}
onChange={handleChange('auto_sync_products')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="auto_sync_products" className="text-sm text-[var(--text-secondary)]">
Sincronización automática de productos
</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="auto_sync_transactions"
checked={settings.auto_sync_transactions}
onChange={handleChange('auto_sync_transactions')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="auto_sync_transactions" className="text-sm text-[var(--text-secondary)]">
Sincronización automática de transacciones
</label>
</div>
</div>
</div>
</div>
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<Smartphone className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="flex-1">
<h5 className="text-sm font-semibold text-blue-900 mb-1">
Integración POS
</h5>
<p className="text-xs text-blue-700 mb-2">
Estos ajustes controlan cómo se sincroniza la información entre el sistema central
y los terminales de punto de venta.
</p>
<ul className="text-xs text-blue-700 space-y-1 list-disc list-inside">
<li>Un intervalo más corto mantiene los datos más actualizados pero consume más recursos</li>
<li>La sincronización automática garantiza que los cambios se reflejen inmediatamente</li>
<li>Desactivar la sincronización automática requiere sincronización manual</li>
</ul>
</div>
</div>
</div>
</div>
</Card>
);
};
export default POSSettingsCard;

View File

@@ -0,0 +1,191 @@
import React from 'react';
import { ShoppingCart, TrendingUp, Clock, AlertTriangle } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { ProcurementSettings } from '../../../../../api/types/settings';
interface ProcurementSettingsCardProps {
settings: ProcurementSettings;
onChange: (settings: ProcurementSettings) => void;
disabled?: boolean;
}
const ProcurementSettingsCard: React.FC<ProcurementSettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const handleChange = (field: keyof ProcurementSettings) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.type === 'checkbox' ? e.target.checked :
e.target.type === 'number' ? parseFloat(e.target.value) :
e.target.value;
onChange({ ...settings, [field]: value });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<ShoppingCart className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
Compras y Aprovisionamiento
</h3>
<div className="space-y-6">
{/* Auto-Approval Settings */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<TrendingUp className="w-4 h-4 mr-2" />
Auto-Aprobación de Órdenes de Compra
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
<div className="flex items-center gap-2 md:col-span-2 xl:col-span-3">
<input
type="checkbox"
id="auto_approve_enabled"
checked={settings.auto_approve_enabled}
onChange={handleChange('auto_approve_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="auto_approve_enabled" className="text-sm text-[var(--text-secondary)]">
Habilitar auto-aprobación de órdenes de compra
</label>
</div>
<Input
type="number"
label="Umbral de Auto-Aprobación (EUR)"
value={settings.auto_approve_threshold_eur}
onChange={handleChange('auto_approve_threshold_eur')}
disabled={disabled || !settings.auto_approve_enabled}
min={0}
max={10000}
step={50}
placeholder="500.0"
/>
<Input
type="number"
label="Puntuación Mínima de Proveedor"
value={settings.auto_approve_min_supplier_score}
onChange={handleChange('auto_approve_min_supplier_score')}
disabled={disabled || !settings.auto_approve_enabled}
min={0}
max={1}
step={0.01}
placeholder="0.80"
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="require_approval_new_suppliers"
checked={settings.require_approval_new_suppliers}
onChange={handleChange('require_approval_new_suppliers')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="require_approval_new_suppliers" className="text-sm text-[var(--text-secondary)]">
Requiere aprobación para nuevos proveedores
</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="require_approval_critical_items"
checked={settings.require_approval_critical_items}
onChange={handleChange('require_approval_critical_items')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="require_approval_critical_items" className="text-sm text-[var(--text-secondary)]">
Requiere aprobación para artículos críticos
</label>
</div>
</div>
</div>
{/* Planning & Forecasting */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Clock className="w-4 h-4 mr-2" />
Planificación y Previsión
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
<Input
type="number"
label="Tiempo de Entrega (días)"
value={settings.procurement_lead_time_days}
onChange={handleChange('procurement_lead_time_days')}
disabled={disabled}
min={1}
max={30}
step={1}
placeholder="3"
/>
<Input
type="number"
label="Días de Previsión de Demanda"
value={settings.demand_forecast_days}
onChange={handleChange('demand_forecast_days')}
disabled={disabled}
min={1}
max={90}
step={1}
placeholder="14"
/>
<Input
type="number"
label="Stock de Seguridad (%)"
value={settings.safety_stock_percentage}
onChange={handleChange('safety_stock_percentage')}
disabled={disabled}
min={0}
max={100}
step={5}
placeholder="20.0"
/>
</div>
</div>
{/* Approval Workflow */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<AlertTriangle className="w-4 h-4 mr-2" />
Flujo de Aprobación
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-6">
<Input
type="number"
label="Recordatorio de Aprobación (horas)"
value={settings.po_approval_reminder_hours}
onChange={handleChange('po_approval_reminder_hours')}
disabled={disabled}
min={1}
max={168}
step={1}
placeholder="24"
/>
<Input
type="number"
label="Escalación Crítica (horas)"
value={settings.po_critical_escalation_hours}
onChange={handleChange('po_critical_escalation_hours')}
disabled={disabled}
min={1}
max={72}
step={1}
placeholder="12"
/>
</div>
</div>
</div>
</Card>
);
};
export default ProcurementSettingsCard;

View File

@@ -0,0 +1,281 @@
import React from 'react';
import { Factory, Calendar, TrendingUp, Clock, DollarSign } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { ProductionSettings } from '../../../../../api/types/settings';
interface ProductionSettingsCardProps {
settings: ProductionSettings;
onChange: (settings: ProductionSettings) => void;
disabled?: boolean;
}
const ProductionSettingsCard: React.FC<ProductionSettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const handleChange = (field: keyof ProductionSettings) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.type === 'checkbox' ? e.target.checked :
e.target.type === 'number' ? parseFloat(e.target.value) :
e.target.value;
onChange({ ...settings, [field]: value });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<Factory className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
Producción
</h3>
<div className="space-y-6">
{/* Planning & Batch Size */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Calendar className="w-4 h-4 mr-2" />
Planificación y Lotes
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
<Input
type="number"
label="Horizonte de Planificación (días)"
value={settings.planning_horizon_days}
onChange={handleChange('planning_horizon_days')}
disabled={disabled}
min={1}
max={30}
step={1}
placeholder="7"
/>
<Input
type="number"
label="Tamaño Mínimo de Lote"
value={settings.minimum_batch_size}
onChange={handleChange('minimum_batch_size')}
disabled={disabled}
min={0.1}
max={100}
step={0.1}
placeholder="1.0"
/>
<Input
type="number"
label="Tamaño Máximo de Lote"
value={settings.maximum_batch_size}
onChange={handleChange('maximum_batch_size')}
disabled={disabled}
min={1}
max={1000}
step={1}
placeholder="100.0"
/>
<Input
type="number"
label="Buffer de Producción (%)"
value={settings.production_buffer_percentage}
onChange={handleChange('production_buffer_percentage')}
disabled={disabled}
min={0}
max={50}
step={1}
placeholder="10.0"
/>
<div className="flex items-center gap-2 md:col-span-2 xl:col-span-2">
<input
type="checkbox"
id="schedule_optimization_enabled"
checked={settings.schedule_optimization_enabled}
onChange={handleChange('schedule_optimization_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="schedule_optimization_enabled" className="text-sm text-[var(--text-secondary)]">
Habilitar optimización de horarios
</label>
</div>
</div>
</div>
{/* Capacity & Working Hours */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Clock className="w-4 h-4 mr-2" />
Capacidad y Jornada Laboral
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
<Input
type="number"
label="Horas de Trabajo por Día"
value={settings.working_hours_per_day}
onChange={handleChange('working_hours_per_day')}
disabled={disabled}
min={1}
max={24}
step={1}
placeholder="12"
/>
<Input
type="number"
label="Máximo Horas Extra"
value={settings.max_overtime_hours}
onChange={handleChange('max_overtime_hours')}
disabled={disabled}
min={0}
max={12}
step={1}
placeholder="4"
/>
<Input
type="number"
label="Objetivo Utilización Capacidad"
value={settings.capacity_utilization_target}
onChange={handleChange('capacity_utilization_target')}
disabled={disabled}
min={0.5}
max={1}
step={0.01}
placeholder="0.85"
/>
<Input
type="number"
label="Umbral de Alerta de Capacidad"
value={settings.capacity_warning_threshold}
onChange={handleChange('capacity_warning_threshold')}
disabled={disabled}
min={0.7}
max={1}
step={0.01}
placeholder="0.95"
/>
</div>
</div>
{/* Quality Control */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<TrendingUp className="w-4 h-4 mr-2" />
Control de Calidad
</h4>
<div className="space-y-4 pl-6">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="quality_check_enabled"
checked={settings.quality_check_enabled}
onChange={handleChange('quality_check_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="quality_check_enabled" className="text-sm text-[var(--text-secondary)]">
Habilitar verificación de calidad
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
type="number"
label="Rendimiento Mínimo (%)"
value={settings.minimum_yield_percentage}
onChange={handleChange('minimum_yield_percentage')}
disabled={disabled || !settings.quality_check_enabled}
min={50}
max={100}
step={1}
placeholder="85.0"
/>
<Input
type="number"
label="Umbral de Puntuación de Calidad (0-10)"
value={settings.quality_score_threshold}
onChange={handleChange('quality_score_threshold')}
disabled={disabled || !settings.quality_check_enabled}
min={0}
max={10}
step={0.1}
placeholder="8.0"
/>
</div>
</div>
</div>
{/* Time Buffers */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Clock className="w-4 h-4 mr-2" />
Tiempos de Preparación
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-6">
<Input
type="number"
label="Tiempo de Preparación (minutos)"
value={settings.prep_time_buffer_minutes}
onChange={handleChange('prep_time_buffer_minutes')}
disabled={disabled}
min={0}
max={120}
step={5}
placeholder="30"
/>
<Input
type="number"
label="Tiempo de Limpieza (minutos)"
value={settings.cleanup_time_buffer_minutes}
onChange={handleChange('cleanup_time_buffer_minutes')}
disabled={disabled}
min={0}
max={120}
step={5}
placeholder="15"
/>
</div>
</div>
{/* Cost Settings */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<DollarSign className="w-4 h-4 mr-2" />
Costes
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-6">
<Input
type="number"
label="Coste Laboral por Hora (EUR)"
value={settings.labor_cost_per_hour_eur}
onChange={handleChange('labor_cost_per_hour_eur')}
disabled={disabled}
min={5}
max={100}
step={0.5}
placeholder="15.0"
/>
<Input
type="number"
label="Porcentaje de Gastos Generales (%)"
value={settings.overhead_cost_percentage}
onChange={handleChange('overhead_cost_percentage')}
disabled={disabled}
min={0}
max={50}
step={1}
placeholder="20.0"
/>
</div>
</div>
</div>
</Card>
);
};
export default ProductionSettingsCard;

View File

@@ -0,0 +1,196 @@
import React from 'react';
import { Truck, Calendar, TrendingUp, AlertTriangle, DollarSign } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { SupplierSettings } from '../../../../../api/types/settings';
interface SupplierSettingsCardProps {
settings: SupplierSettings;
onChange: (settings: SupplierSettings) => void;
disabled?: boolean;
}
const SupplierSettingsCard: React.FC<SupplierSettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const handleChange = (field: keyof SupplierSettings) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.type === 'number' ? parseFloat(e.target.value) : e.target.value;
onChange({ ...settings, [field]: value });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<Truck className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
Gestión de Proveedores
</h3>
<div className="space-y-6">
{/* Default Terms */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Calendar className="w-4 h-4 mr-2" />
Términos Predeterminados
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-6">
<Input
type="number"
label="Plazo de Pago Predeterminado (días)"
value={settings.default_payment_terms_days}
onChange={handleChange('default_payment_terms_days')}
disabled={disabled}
min={1}
max={90}
step={1}
placeholder="30"
/>
<Input
type="number"
label="Días de Entrega Predeterminados"
value={settings.default_delivery_days}
onChange={handleChange('default_delivery_days')}
disabled={disabled}
min={1}
max={30}
step={1}
placeholder="3"
/>
</div>
</div>
{/* Performance Thresholds - Delivery */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<TrendingUp className="w-4 h-4 mr-2" />
Umbrales de Rendimiento - Entregas
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-6">
<Input
type="number"
label="Tasa de Entrega Excelente (%)"
value={settings.excellent_delivery_rate}
onChange={handleChange('excellent_delivery_rate')}
disabled={disabled}
min={90}
max={100}
step={0.5}
placeholder="95.0"
/>
<Input
type="number"
label="Tasa de Entrega Buena (%)"
value={settings.good_delivery_rate}
onChange={handleChange('good_delivery_rate')}
disabled={disabled}
min={80}
max={99}
step={0.5}
placeholder="90.0"
/>
</div>
</div>
{/* Performance Thresholds - Quality */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<TrendingUp className="w-4 h-4 mr-2" />
Umbrales de Rendimiento - Calidad
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-6">
<Input
type="number"
label="Tasa de Calidad Excelente (%)"
value={settings.excellent_quality_rate}
onChange={handleChange('excellent_quality_rate')}
disabled={disabled}
min={90}
max={100}
step={0.5}
placeholder="98.0"
/>
<Input
type="number"
label="Tasa de Calidad Buena (%)"
value={settings.good_quality_rate}
onChange={handleChange('good_quality_rate')}
disabled={disabled}
min={80}
max={99}
step={0.5}
placeholder="95.0"
/>
</div>
</div>
{/* Critical Alerts */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<AlertTriangle className="w-4 h-4 mr-2" />
Alertas Críticas
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 pl-6">
<Input
type="number"
label="Retraso de Entrega Crítico (horas)"
value={settings.critical_delivery_delay_hours}
onChange={handleChange('critical_delivery_delay_hours')}
disabled={disabled}
min={1}
max={168}
step={1}
placeholder="24"
/>
<Input
type="number"
label="Tasa de Rechazo de Calidad Crítica (%)"
value={settings.critical_quality_rejection_rate}
onChange={handleChange('critical_quality_rejection_rate')}
disabled={disabled}
min={0}
max={50}
step={0.5}
placeholder="10.0"
/>
<Input
type="number"
label="Varianza de Coste Alta (%)"
value={settings.high_cost_variance_percentage}
onChange={handleChange('high_cost_variance_percentage')}
disabled={disabled}
min={0}
max={100}
step={1}
placeholder="15.0"
/>
</div>
</div>
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<TrendingUp className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="flex-1">
<h5 className="text-sm font-semibold text-blue-900 mb-1">
Evaluación de Proveedores
</h5>
<p className="text-xs text-blue-700">
Estos umbrales se utilizan para evaluar automáticamente el rendimiento de los proveedores.
Los proveedores con rendimiento por debajo de los umbrales "buenos" recibirán alertas automáticas.
</p>
</div>
</div>
</div>
</div>
</Card>
);
};
export default SupplierSettingsCard;