Improve the frontend 3
This commit is contained in:
@@ -13,10 +13,11 @@ import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../featu
|
||||
import { useDashboardStats } from '../../api/hooks/dashboard';
|
||||
import { usePurchaseOrder, useApprovePurchaseOrder, useRejectPurchaseOrder } from '../../api/hooks/purchase-orders';
|
||||
import { useBatchDetails, useUpdateBatchStatus } from '../../api/hooks/production';
|
||||
import { useRunDailyWorkflow } from '../../api';
|
||||
import { ProductionStatusEnum } from '../../api';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Clock,
|
||||
Euro,
|
||||
Package,
|
||||
FileText,
|
||||
@@ -28,9 +29,10 @@ import {
|
||||
Factory,
|
||||
Timer,
|
||||
TrendingDown,
|
||||
Leaf
|
||||
Leaf,
|
||||
Play
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { showToast } from '../../utils/toast';
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -76,18 +78,43 @@ const DashboardPage: React.FC = () => {
|
||||
const approvePOMutation = useApprovePurchaseOrder();
|
||||
const rejectPOMutation = useRejectPurchaseOrder();
|
||||
const updateBatchStatusMutation = useUpdateBatchStatus();
|
||||
const orchestratorMutation = useRunDailyWorkflow();
|
||||
|
||||
const handleRunOrchestrator = async () => {
|
||||
try {
|
||||
await orchestratorMutation.mutateAsync(currentTenant?.id || '');
|
||||
showToast.success('Flujo de planificación ejecutado exitosamente');
|
||||
} catch (error) {
|
||||
console.error('Error running orchestrator:', error);
|
||||
showToast.error('Error al ejecutar flujo de planificación');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[Dashboard] Demo mode:', isDemoMode);
|
||||
console.log('[Dashboard] Should start tour:', shouldStartTour());
|
||||
console.log('[Dashboard] SessionStorage demo_tour_should_start:', sessionStorage.getItem('demo_tour_should_start'));
|
||||
console.log('[Dashboard] SessionStorage demo_tour_start_step:', sessionStorage.getItem('demo_tour_start_step'));
|
||||
|
||||
if (isDemoMode && shouldStartTour()) {
|
||||
// Check if there's a tour intent from redirection (higher priority)
|
||||
const shouldStartFromRedirect = sessionStorage.getItem('demo_tour_should_start') === 'true';
|
||||
const redirectStartStep = parseInt(sessionStorage.getItem('demo_tour_start_step') || '0', 10);
|
||||
|
||||
if (isDemoMode && (shouldStartTour() || shouldStartFromRedirect)) {
|
||||
console.log('[Dashboard] Starting tour in 1.5s...');
|
||||
const timer = setTimeout(() => {
|
||||
console.log('[Dashboard] Executing startTour()');
|
||||
startTour();
|
||||
clearTourStartPending();
|
||||
if (shouldStartFromRedirect) {
|
||||
// Start tour from the specific step that was intended
|
||||
startTour(redirectStartStep);
|
||||
// Clear the redirect intent
|
||||
sessionStorage.removeItem('demo_tour_should_start');
|
||||
sessionStorage.removeItem('demo_tour_start_step');
|
||||
} else {
|
||||
// Start tour normally (from beginning or resume)
|
||||
startTour();
|
||||
clearTourStartPending();
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
@@ -114,10 +141,10 @@ const DashboardPage: React.FC = () => {
|
||||
batchId,
|
||||
statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS }
|
||||
});
|
||||
toast.success('Lote iniciado');
|
||||
showToast.success('Lote iniciado');
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
toast.error('Error al iniciar lote');
|
||||
showToast.error('Error al iniciar lote');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,10 +155,10 @@ const DashboardPage: React.FC = () => {
|
||||
batchId,
|
||||
statusUpdate: { status: ProductionStatusEnum.ON_HOLD }
|
||||
});
|
||||
toast.success('Lote pausado');
|
||||
showToast.success('Lote pausado');
|
||||
} catch (error) {
|
||||
console.error('Error pausing batch:', error);
|
||||
toast.error('Error al pausar lote');
|
||||
showToast.error('Error al pausar lote');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,10 +174,10 @@ const DashboardPage: React.FC = () => {
|
||||
poId,
|
||||
notes: 'Aprobado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden aprobada');
|
||||
showToast.success('Orden aprobada');
|
||||
} catch (error) {
|
||||
console.error('Error approving PO:', error);
|
||||
toast.error('Error al aprobar orden');
|
||||
showToast.error('Error al aprobar orden');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,10 +188,10 @@ const DashboardPage: React.FC = () => {
|
||||
poId,
|
||||
reason: 'Rechazado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
showToast.success('Orden rechazada');
|
||||
} catch (error) {
|
||||
console.error('Error rejecting PO:', error);
|
||||
toast.error('Error al rechazar orden');
|
||||
showToast.error('Error al rechazar orden');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -355,6 +382,18 @@ const DashboardPage: React.FC = () => {
|
||||
<PageHeader
|
||||
title={t('dashboard:title', 'Dashboard')}
|
||||
description={t('dashboard:subtitle', 'Overview of your bakery operations')}
|
||||
actions={[
|
||||
{
|
||||
id: 'run-orchestrator',
|
||||
label: orchestratorMutation.isPending ? 'Ejecutando...' : 'Ejecutar Planificación Diaria',
|
||||
icon: Play,
|
||||
onClick: handleRunOrchestrator,
|
||||
variant: 'primary', // Primary button for visibility
|
||||
size: 'sm',
|
||||
disabled: orchestratorMutation.isPending,
|
||||
loading: orchestratorMutation.isPending
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Critical Metrics using StatsGrid */}
|
||||
@@ -447,12 +486,12 @@ const DashboardPage: React.FC = () => {
|
||||
poId: poDetails.id,
|
||||
notes: 'Aprobado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden aprobada');
|
||||
showToast.success('Orden aprobada');
|
||||
setShowPOModal(false);
|
||||
setSelectedPOId(null);
|
||||
} catch (error) {
|
||||
console.error('Error approving PO:', error);
|
||||
toast.error('Error al aprobar orden');
|
||||
showToast.error('Error al aprobar orden');
|
||||
}
|
||||
},
|
||||
variant: 'primary' as const,
|
||||
@@ -467,12 +506,12 @@ const DashboardPage: React.FC = () => {
|
||||
poId: poDetails.id,
|
||||
reason: 'Rechazado desde el dashboard'
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
showToast.success('Orden rechazada');
|
||||
setShowPOModal(false);
|
||||
setSelectedPOId(null);
|
||||
} catch (error) {
|
||||
console.error('Error rejecting PO:', error);
|
||||
toast.error('Error al rechazar orden');
|
||||
showToast.error('Error al rechazar orden');
|
||||
}
|
||||
},
|
||||
variant: 'outline' as const,
|
||||
@@ -521,12 +560,12 @@ const DashboardPage: React.FC = () => {
|
||||
batchId: batchDetails.id,
|
||||
statusUpdate: { status: ProductionStatusEnum.IN_PROGRESS }
|
||||
});
|
||||
toast.success('Lote iniciado');
|
||||
showToast.success('Lote iniciado');
|
||||
setShowBatchModal(false);
|
||||
setSelectedBatchId(null);
|
||||
} catch (error) {
|
||||
console.error('Error starting batch:', error);
|
||||
toast.error('Error al iniciar lote');
|
||||
showToast.error('Error al iniciar lote');
|
||||
}
|
||||
},
|
||||
variant: 'primary' as const,
|
||||
@@ -542,12 +581,12 @@ const DashboardPage: React.FC = () => {
|
||||
batchId: batchDetails.id,
|
||||
statusUpdate: { status: ProductionStatusEnum.ON_HOLD }
|
||||
});
|
||||
toast.success('Lote pausado');
|
||||
showToast.success('Lote pausado');
|
||||
setShowBatchModal(false);
|
||||
setSelectedBatchId(null);
|
||||
} catch (error) {
|
||||
console.error('Error pausing batch:', error);
|
||||
toast.error('Error al pausar lote');
|
||||
showToast.error('Error al pausar lote');
|
||||
}
|
||||
},
|
||||
variant: 'outline' as const,
|
||||
@@ -561,4 +600,4 @@ const DashboardPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { showToast } from '../../../../utils/toast';
|
||||
import { useSettings, useUpdateSettings } from '../../../../api/hooks/settings';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import type {
|
||||
@@ -13,6 +13,10 @@ import type {
|
||||
SupplierSettings,
|
||||
POSSettings,
|
||||
OrderSettings,
|
||||
ReplenishmentSettings,
|
||||
SafetyStockSettings,
|
||||
MOQSettings,
|
||||
SupplierSelectionSettings,
|
||||
} from '../../../../api/types/settings';
|
||||
import ProcurementSettingsCard from './cards/ProcurementSettingsCard';
|
||||
import InventorySettingsCard from './cards/InventorySettingsCard';
|
||||
@@ -20,9 +24,13 @@ import ProductionSettingsCard from './cards/ProductionSettingsCard';
|
||||
import SupplierSettingsCard from './cards/SupplierSettingsCard';
|
||||
import POSSettingsCard from './cards/POSSettingsCard';
|
||||
import OrderSettingsCard from './cards/OrderSettingsCard';
|
||||
import ReplenishmentSettingsCard from './cards/ReplenishmentSettingsCard';
|
||||
import SafetyStockSettingsCard from './cards/SafetyStockSettingsCard';
|
||||
import MOQSettingsCard from './cards/MOQSettingsCard';
|
||||
import SupplierSelectionSettingsCard from './cards/SupplierSelectionSettingsCard';
|
||||
|
||||
const AjustesPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
@@ -52,6 +60,10 @@ const AjustesPage: React.FC = () => {
|
||||
const [supplierSettings, setSupplierSettings] = useState<SupplierSettings | null>(null);
|
||||
const [posSettings, setPosSettings] = useState<POSSettings | null>(null);
|
||||
const [orderSettings, setOrderSettings] = useState<OrderSettings | null>(null);
|
||||
const [replenishmentSettings, setReplenishmentSettings] = useState<ReplenishmentSettings | null>(null);
|
||||
const [safetyStockSettings, setSafetyStockSettings] = useState<SafetyStockSettings | null>(null);
|
||||
const [moqSettings, setMoqSettings] = useState<MOQSettings | null>(null);
|
||||
const [supplierSelectionSettings, setSupplierSelectionSettings] = useState<SupplierSelectionSettings | null>(null);
|
||||
|
||||
// Load settings into local state when data is fetched
|
||||
React.useEffect(() => {
|
||||
@@ -62,13 +74,18 @@ const AjustesPage: React.FC = () => {
|
||||
setSupplierSettings(settings.supplier_settings);
|
||||
setPosSettings(settings.pos_settings);
|
||||
setOrderSettings(settings.order_settings);
|
||||
setReplenishmentSettings(settings.replenishment_settings);
|
||||
setSafetyStockSettings(settings.safety_stock_settings);
|
||||
setMoqSettings(settings.moq_settings);
|
||||
setSupplierSelectionSettings(settings.supplier_selection_settings);
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const handleSaveAll = async () => {
|
||||
if (!tenantId || !procurementSettings || !inventorySettings || !productionSettings ||
|
||||
!supplierSettings || !posSettings || !orderSettings) {
|
||||
!supplierSettings || !posSettings || !orderSettings || !replenishmentSettings ||
|
||||
!safetyStockSettings || !moqSettings || !supplierSelectionSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,14 +101,18 @@ const AjustesPage: React.FC = () => {
|
||||
supplier_settings: supplierSettings,
|
||||
pos_settings: posSettings,
|
||||
order_settings: orderSettings,
|
||||
replenishment_settings: replenishmentSettings,
|
||||
safety_stock_settings: safetyStockSettings,
|
||||
moq_settings: moqSettings,
|
||||
supplier_selection_settings: supplierSelectionSettings,
|
||||
},
|
||||
});
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast('Ajustes guardados correctamente', { type: 'success' });
|
||||
showToast.success('Ajustes guardados correctamente');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
|
||||
addToast(`Error al guardar ajustes: ${errorMessage}`, { type: 'error' });
|
||||
showToast.error(`Error al guardar ajustes: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -105,6 +126,10 @@ const AjustesPage: React.FC = () => {
|
||||
setSupplierSettings(settings.supplier_settings);
|
||||
setPosSettings(settings.pos_settings);
|
||||
setOrderSettings(settings.order_settings);
|
||||
setReplenishmentSettings(settings.replenishment_settings);
|
||||
setSafetyStockSettings(settings.safety_stock_settings);
|
||||
setMoqSettings(settings.moq_settings);
|
||||
setSupplierSelectionSettings(settings.supplier_selection_settings);
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
};
|
||||
@@ -256,6 +281,54 @@ const AjustesPage: React.FC = () => {
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Replenishment Settings */}
|
||||
{replenishmentSettings && (
|
||||
<ReplenishmentSettingsCard
|
||||
settings={replenishmentSettings}
|
||||
onChange={(newSettings) => {
|
||||
setReplenishmentSettings(newSettings);
|
||||
handleCategoryChange('replenishment');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Safety Stock Settings */}
|
||||
{safetyStockSettings && (
|
||||
<SafetyStockSettingsCard
|
||||
settings={safetyStockSettings}
|
||||
onChange={(newSettings) => {
|
||||
setSafetyStockSettings(newSettings);
|
||||
handleCategoryChange('safety_stock');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* MOQ Settings */}
|
||||
{moqSettings && (
|
||||
<MOQSettingsCard
|
||||
settings={moqSettings}
|
||||
onChange={(newSettings) => {
|
||||
setMoqSettings(newSettings);
|
||||
handleCategoryChange('moq');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Supplier Selection Settings */}
|
||||
{supplierSelectionSettings && (
|
||||
<SupplierSelectionSettingsCard
|
||||
settings={supplierSelectionSettings}
|
||||
onChange={(newSettings) => {
|
||||
setSupplierSelectionSettings(newSettings);
|
||||
handleCategoryChange('supplier_selection');
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Save Banner */}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { MOQSettings } from '@services/types/settings';
|
||||
import { Input } from '@components/ui/Input';
|
||||
|
||||
interface MOQSettingsCardProps {
|
||||
settings: MOQSettings;
|
||||
onChange: (settings: MOQSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MOQSettingsCard: React.FC<MOQSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof MOQSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleChange = (field: keyof MOQSettings, value: boolean) => {
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Configuración de MOQ (Cantidad Mínima de Pedido)
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Consolidation Window Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Ventana de Consolidación (1-30)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="30"
|
||||
value={settings.consolidation_window_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('consolidation_window_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Batch Size */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Tamaño Mínimo de Lote (0.1-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0.1"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
value={settings.min_batch_size}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('min_batch_size', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Batch Size */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Tamaño Máximo de Lote (1-10000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
step="1"
|
||||
value={settings.max_batch_size}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('max_batch_size', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle Options */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allow_early_ordering"
|
||||
checked={settings.allow_early_ordering}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('allow_early_ordering', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="allow_early_ordering" className="text-sm text-[var(--text-secondary)]">
|
||||
Permitir Pedido Anticipado
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_batch_optimization"
|
||||
checked={settings.enable_batch_optimization}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('enable_batch_optimization', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="enable_batch_optimization" className="text-sm text-[var(--text-secondary)]">
|
||||
Habilitar Optimización de Lotes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MOQSettingsCard;
|
||||
@@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { ReplenishmentSettings } from '@services/types/settings';
|
||||
import { Slider } from '@components/ui/Slider';
|
||||
import { Input } from '@components/ui/Input';
|
||||
|
||||
interface ReplenishmentSettingsCardProps {
|
||||
settings: ReplenishmentSettings;
|
||||
onChange: (settings: ReplenishmentSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ReplenishmentSettingsCard: React.FC<ReplenishmentSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof ReplenishmentSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleChange = (field: keyof ReplenishmentSettings, value: boolean) => {
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Planeamiento de Reposición
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Projection Horizon Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Proyección (1-30)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="30"
|
||||
value={settings.projection_horizon_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('projection_horizon_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Service Level */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Nivel de Servicio ({(settings.service_level * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.service_level]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('service_level', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Buffer Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Buffer (0-14)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="14"
|
||||
value={settings.buffer_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('buffer_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Demand Forecast Days */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Días de Previsión de Demanda (1-90)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="90"
|
||||
value={settings.demand_forecast_days}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('demand_forecast_days', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Order Quantity */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Cantidad Mínima de Pedido (0.1-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0.1"
|
||||
max="100"
|
||||
step="0.1"
|
||||
value={settings.min_order_quantity}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('min_order_quantity', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Order Quantity */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Cantidad Máxima de Pedido (1-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
step="1"
|
||||
value={settings.max_order_quantity}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('max_order_quantity', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enable Auto Replenishment Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_auto_replenishment"
|
||||
checked={settings.enable_auto_replenishment}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('enable_auto_replenishment', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="enable_auto_replenishment" className="text-sm text-[var(--text-secondary)]">
|
||||
Habilitar Reposición Automática
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReplenishmentSettingsCard;
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { SafetyStockSettings } from '@services/types/settings';
|
||||
import { Slider } from '@components/ui/Slider';
|
||||
import { Input } from '@components/ui/Input';
|
||||
import { Select } from '@components/ui/Select';
|
||||
|
||||
interface SafetyStockSettingsCardProps {
|
||||
settings: SafetyStockSettings;
|
||||
onChange: (settings: SafetyStockSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SafetyStockSettingsCard: React.FC<SafetyStockSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof SafetyStockSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleStringChange = (field: keyof SafetyStockSettings, value: any) => {
|
||||
const stringValue = typeof value === 'object' && value !== null ? value[0] : value;
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: stringValue.toString(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Configuración de Stock de Seguridad
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Service Level */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Nivel de Servicio ({(settings.service_level * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.service_level]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('service_level', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Method */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Método de Cálculo
|
||||
</label>
|
||||
<Select
|
||||
value={settings.method}
|
||||
onChange={(value) => handleStringChange('method', value)}
|
||||
disabled={disabled}
|
||||
options={[
|
||||
{ value: 'statistical', label: 'Estadístico (Z×σ×√L)' },
|
||||
{ value: 'fixed_percentage', label: 'Porcentaje Fijo (20%)' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Min Safety Stock */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Stock de Seguridad Mínimo (0-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
value={settings.min_safety_stock}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('min_safety_stock', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Safety Stock */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Stock de Seguridad Máximo (0-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
value={settings.max_safety_stock}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('max_safety_stock', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reorder Point Calculation */}
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Método de Punto de Reorden
|
||||
</label>
|
||||
<Select
|
||||
value={settings.reorder_point_calculation}
|
||||
onChange={(value) => handleStringChange('reorder_point_calculation', value)}
|
||||
disabled={disabled}
|
||||
options={[
|
||||
{ value: 'safety_stock_plus_lead_time_demand', label: 'Stock de Seguridad + Demanda de Tiempo de Entrega' },
|
||||
{ value: 'safety_stock_only', label: 'Solo Stock de Seguridad' },
|
||||
{ value: 'fixed_quantity', label: 'Cantidad Fija' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SafetyStockSettingsCard;
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@components/ui';
|
||||
import { SupplierSelectionSettings } from '@services/types/settings';
|
||||
import { Slider } from '@components/ui/Slider';
|
||||
import { Input } from '@components/ui/Input';
|
||||
|
||||
interface SupplierSelectionSettingsCardProps {
|
||||
settings: SupplierSelectionSettings;
|
||||
onChange: (settings: SupplierSelectionSettings) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SupplierSelectionSettingsCard: React.FC<SupplierSelectionSettingsCardProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleNumberChange = (field: keyof SupplierSelectionSettings, value: string) => {
|
||||
const numValue = value === '' ? 0 : Number(value);
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: numValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleChange = (field: keyof SupplierSelectionSettings, value: boolean) => {
|
||||
onChange({
|
||||
...settings,
|
||||
[field]: value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">
|
||||
Configuración de Selección de Proveedores
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Price Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso del Precio ({(settings.price_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.price_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('price_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Lead Time Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso del Tiempo de Entrega ({(settings.lead_time_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.lead_time_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('lead_time_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quality Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso de la Calidad ({(settings.quality_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.quality_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('quality_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reliability Weight */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Peso de la Confiabilidad ({(settings.reliability_weight * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.reliability_weight]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('reliability_weight', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diversification Threshold */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Umbral de Diversificación (0-1000)
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1000"
|
||||
value={settings.diversification_threshold}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberChange('diversification_threshold', e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Single Percentage */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-secondary)]">
|
||||
Máximo % para Proveedor Único ({(settings.max_single_percentage * 100).toFixed(0)}%)
|
||||
</label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[settings.max_single_percentage]}
|
||||
onValueChange={([value]: number[]) => handleNumberChange('max_single_percentage', value.toString())}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enable Supplier Score Optimization Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_supplier_score_optimization"
|
||||
checked={settings.enable_supplier_score_optimization}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleToggleChange('enable_supplier_score_optimization', e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="rounded border-[var(--border-primary)]"
|
||||
/>
|
||||
<label htmlFor="enable_supplier_score_optimization" className="text-sm text-[var(--text-secondary)]">
|
||||
Habilitar Optimización por Puntuación de Proveedores
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierSelectionSettingsCard;
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useMemo } from 'react';
|
||||
import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react';
|
||||
import { Button, Badge, Modal, Table, Select, StatsGrid, StatusCard, SearchAndFilter, type FilterConfig, Card, EmptyState } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useIngredients } from '../../../../api/hooks/inventory';
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ interface ModelStatus {
|
||||
}
|
||||
|
||||
const ModelsConfigPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
@@ -160,10 +160,10 @@ const ModelsConfigPage: React.FC = () => {
|
||||
request: trainingSettings
|
||||
});
|
||||
|
||||
addToast(`Entrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' });
|
||||
showToast.success(`Entrenamiento iniciado para ${selectedIngredient.name}`);
|
||||
setShowTrainingModal(false);
|
||||
} catch (error) {
|
||||
addToast('Error al iniciar el entrenamiento', { type: 'error' });
|
||||
showToast.error('Error al iniciar el entrenamiento');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -206,12 +206,12 @@ const ModelsConfigPage: React.FC = () => {
|
||||
request: settings
|
||||
});
|
||||
|
||||
addToast(`Reentrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' });
|
||||
showToast.success(`Reentrenamiento iniciado para ${selectedIngredient.name}`);
|
||||
setShowRetrainModal(false);
|
||||
setSelectedIngredient(null);
|
||||
setSelectedModel(null);
|
||||
} catch (error) {
|
||||
addToast('Error al reentrenar el modelo', { type: 'error' });
|
||||
showToast.error('Error al reentrenar el modelo');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { useIngredients } from '../../../../api/hooks/inventory';
|
||||
import { useTenantId } from '../../../../hooks/useTenantId';
|
||||
import { ProductType, ProductCategory, IngredientResponse } from '../../../../api/types/inventory';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { usePOSConfigurationData, usePOSConfigurationManager, usePOSTransactions, usePOSTransactionsDashboard, usePOSTransaction } from '../../../../api/hooks/pos';
|
||||
import { POSConfiguration } from '../../../../api/types/pos';
|
||||
import { posService } from '../../../../api/services/pos';
|
||||
@@ -546,7 +546,7 @@ const POSPage: React.FC = () => {
|
||||
const [testingConnection, setTestingConnection] = useState<string | null>(null);
|
||||
|
||||
const tenantId = useTenantId();
|
||||
const { addToast } = useToast();
|
||||
|
||||
|
||||
// POS Configuration hooks
|
||||
const posData = usePOSConfigurationData(tenantId);
|
||||
@@ -674,12 +674,12 @@ const POSPage: React.FC = () => {
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
addToast('Conexión exitosa', { type: 'success' });
|
||||
showToast.success('Conexión exitosa');
|
||||
} else {
|
||||
addToast(`Error en la conexión: ${response.message || 'Error desconocido'}`, { type: 'error' });
|
||||
showToast.error(`Error en la conexión: ${response.message || 'Error desconocido'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al probar la conexión', { type: 'error' });
|
||||
showToast.error('Error al probar la conexión');
|
||||
} finally {
|
||||
setTestingConnection(null);
|
||||
}
|
||||
@@ -695,10 +695,10 @@ const POSPage: React.FC = () => {
|
||||
tenant_id: tenantId,
|
||||
config_id: configId,
|
||||
});
|
||||
addToast('Configuración eliminada correctamente', { type: 'success' });
|
||||
showToast.success('Configuración eliminada correctamente');
|
||||
loadPosConfigurations();
|
||||
} catch (error) {
|
||||
addToast('Error al eliminar la configuración', { type: 'error' });
|
||||
showToast.error('Error al eliminar la configuración');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -762,7 +762,7 @@ const POSPage: React.FC = () => {
|
||||
});
|
||||
|
||||
setCart([]);
|
||||
addToast('Venta procesada exitosamente', { type: 'success' });
|
||||
showToast.success('Venta procesada exitosamente');
|
||||
};
|
||||
|
||||
// Loading and error states
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useTriggerDailyScheduler } from '../../../../api';
|
||||
import type { PurchaseOrderStatus, PurchaseOrderPriority, PurchaseOrderDetail } from '../../../../api/services/purchase_orders';
|
||||
import { useTenantStore } from '../../../../stores/tenant.store';
|
||||
import { useUserById } from '../../../../api/hooks/user';
|
||||
import toast from 'react-hot-toast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
|
||||
const ProcurementPage: React.FC = () => {
|
||||
// State
|
||||
@@ -59,7 +59,6 @@ const ProcurementPage: React.FC = () => {
|
||||
const approvePOMutation = useApprovePurchaseOrder();
|
||||
const rejectPOMutation = useRejectPurchaseOrder();
|
||||
const updatePOMutation = useUpdatePurchaseOrder();
|
||||
const triggerSchedulerMutation = useTriggerDailyScheduler();
|
||||
|
||||
// Filter POs
|
||||
const filteredPOs = useMemo(() => {
|
||||
@@ -129,11 +128,11 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: po.id,
|
||||
data: { status: 'SENT_TO_SUPPLIER' }
|
||||
});
|
||||
toast.success('Orden enviada al proveedor');
|
||||
showToast.success('Orden enviada al proveedor');
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error sending PO to supplier:', error);
|
||||
toast.error('Error al enviar orden al proveedor');
|
||||
showToast.error('Error al enviar orden al proveedor');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -144,11 +143,11 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: po.id,
|
||||
data: { status: 'CONFIRMED' }
|
||||
});
|
||||
toast.success('Orden confirmada');
|
||||
showToast.success('Orden confirmada');
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error confirming PO:', error);
|
||||
toast.error('Error al confirmar orden');
|
||||
showToast.error('Error al confirmar orden');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,10 +161,10 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: selectedPOId,
|
||||
notes: approvalNotes || undefined
|
||||
});
|
||||
toast.success('Orden aprobada exitosamente');
|
||||
showToast.success('Orden aprobada exitosamente');
|
||||
} else {
|
||||
if (!approvalNotes.trim()) {
|
||||
toast.error('Debes proporcionar una razón para rechazar');
|
||||
showToast.error('Debes proporcionar una razón para rechazar');
|
||||
return;
|
||||
}
|
||||
await rejectPOMutation.mutateAsync({
|
||||
@@ -173,7 +172,7 @@ const ProcurementPage: React.FC = () => {
|
||||
poId: selectedPOId,
|
||||
reason: approvalNotes
|
||||
});
|
||||
toast.success('Orden rechazada');
|
||||
showToast.success('Orden rechazada');
|
||||
}
|
||||
setShowApprovalModal(false);
|
||||
setShowDetailsModal(false);
|
||||
@@ -181,18 +180,18 @@ const ProcurementPage: React.FC = () => {
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error in approval action:', error);
|
||||
toast.error('Error al procesar aprobación');
|
||||
showToast.error('Error al procesar aprobación');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTriggerScheduler = async () => {
|
||||
try {
|
||||
await triggerSchedulerMutation.mutateAsync(tenantId);
|
||||
toast.success('Scheduler ejecutado exitosamente');
|
||||
showToast.success('Scheduler ejecutado exitosamente');
|
||||
refetchPOs();
|
||||
} catch (error) {
|
||||
console.error('Error triggering scheduler:', error);
|
||||
toast.error('Error al ejecutar scheduler');
|
||||
showToast.error('Error al ejecutar scheduler');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -715,16 +714,6 @@ const ProcurementPage: React.FC = () => {
|
||||
title="Órdenes de Compra"
|
||||
description="Gestiona órdenes de compra y aprovisionamiento"
|
||||
actions={[
|
||||
{
|
||||
id: 'trigger-scheduler',
|
||||
label: triggerSchedulerMutation.isPending ? 'Ejecutando...' : 'Ejecutar Scheduler',
|
||||
icon: Play,
|
||||
onClick: handleTriggerScheduler,
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
disabled: triggerSchedulerMutation.isPending,
|
||||
loading: triggerSchedulerMutation.isPending
|
||||
},
|
||||
{
|
||||
id: 'create-po',
|
||||
label: 'Nueva Orden',
|
||||
@@ -857,7 +846,7 @@ const ProcurementPage: React.FC = () => {
|
||||
onSuccess={() => {
|
||||
setShowCreatePOModal(false);
|
||||
refetchPOs();
|
||||
toast.success('Orden de compra creada exitosamente');
|
||||
showToast.success('Orden de compra creada exitosamente');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from '../../../../api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProcessStage } from '../../../../api/types/qualityTemplates';
|
||||
import toast from 'react-hot-toast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
|
||||
const ProductionPage: React.FC = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -58,7 +58,6 @@ const ProductionPage: React.FC = () => {
|
||||
// Mutations
|
||||
const createBatchMutation = useCreateProductionBatch();
|
||||
const updateBatchStatusMutation = useUpdateBatchStatus();
|
||||
const triggerSchedulerMutation = useTriggerProductionScheduler();
|
||||
|
||||
// Handlers
|
||||
const handleCreateBatch = async (batchData: ProductionBatchCreate) => {
|
||||
@@ -76,10 +75,10 @@ const ProductionPage: React.FC = () => {
|
||||
const handleTriggerScheduler = async () => {
|
||||
try {
|
||||
await triggerSchedulerMutation.mutateAsync(tenantId);
|
||||
toast.success('Scheduler ejecutado exitosamente');
|
||||
showToast.success('Scheduler ejecutado exitosamente');
|
||||
} catch (error) {
|
||||
console.error('Error triggering scheduler:', error);
|
||||
toast.error('Error al ejecutar scheduler');
|
||||
showToast.error('Error al ejecutar scheduler');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -300,16 +299,6 @@ const ProductionPage: React.FC = () => {
|
||||
title="Gestión de Producción"
|
||||
description="Planifica y controla la producción diaria de tu panadería"
|
||||
actions={[
|
||||
{
|
||||
id: 'trigger-scheduler',
|
||||
label: triggerSchedulerMutation.isPending ? 'Ejecutando...' : 'Ejecutar Scheduler',
|
||||
icon: Play,
|
||||
onClick: handleTriggerScheduler,
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
disabled: triggerSchedulerMutation.isPending,
|
||||
loading: triggerSchedulerMutation.isPending
|
||||
},
|
||||
{
|
||||
id: 'create-batch',
|
||||
label: 'Nueva Orden de Producción',
|
||||
@@ -731,4 +720,4 @@ const ProductionPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionPage;
|
||||
export default ProductionPage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Store, MapPin, Clock, Phone, Mail, Globe, Save, X, Edit3, Zap, Plus, Settings, Trash2, Wifi, WifiOff, AlertCircle, CheckCircle, Loader, Eye, EyeOff, Info } from 'lucide-react';
|
||||
import { Button, Card, Input, Select, Modal, Badge, Tabs } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { usePOSConfigurationData, usePOSConfigurationManager } from '../../../../api/hooks/pos';
|
||||
import { POSConfiguration, POSProviderConfig } from '../../../../api/types/pos';
|
||||
import { posService } from '../../../../api/services/pos';
|
||||
@@ -38,7 +38,7 @@ interface BusinessHours {
|
||||
|
||||
|
||||
const BakeryConfigPage: React.FC = () => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { loadUserTenants, setCurrentTenant } = useTenantActions();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
@@ -287,9 +287,9 @@ const BakeryConfigPage: React.FC = () => {
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast('Configuración actualizada correctamente', { type: 'success' });
|
||||
showToast.success('Configuración actualizada correctamente');
|
||||
} catch (error) {
|
||||
addToast(`Error al actualizar: ${error instanceof Error ? error.message : 'Error desconocido'}`, { type: 'error' });
|
||||
showToast.error(`Error al actualizar: ${error instanceof Error ? error.message : 'Error desconocido'}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -364,7 +364,7 @@ const BakeryConfigPage: React.FC = () => {
|
||||
.map(field => field.label);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
addToast(`Campos requeridos: ${missingFields.join(', ')}`, 'error');
|
||||
showToast.error(`Campos requeridos: ${missingFields.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ const BakeryConfigPage: React.FC = () => {
|
||||
config_id: selectedPosConfig.id,
|
||||
...posFormData,
|
||||
});
|
||||
addToast('Configuración actualizada correctamente', 'success');
|
||||
showToast.success('Configuración actualizada correctamente');
|
||||
setShowEditPosModal(false);
|
||||
loadPosConfigurations();
|
||||
} else {
|
||||
@@ -384,12 +384,12 @@ const BakeryConfigPage: React.FC = () => {
|
||||
tenant_id: tenantId,
|
||||
...posFormData,
|
||||
});
|
||||
addToast('Configuración creada correctamente', 'success');
|
||||
showToast.success('Configuración creada correctamente');
|
||||
setShowAddPosModal(false);
|
||||
loadPosConfigurations();
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al guardar la configuración', 'error');
|
||||
showToast.error('Error al guardar la configuración');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -402,12 +402,12 @@ const BakeryConfigPage: React.FC = () => {
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
addToast('Conexión exitosa', 'success');
|
||||
showToast.success('Conexión exitosa');
|
||||
} else {
|
||||
addToast(`Error en la conexión: ${response.message || 'Error desconocido'}`, 'error');
|
||||
showToast.error(`Error en la conexión: ${response.message || 'Error desconocido'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('Error al probar la conexión', 'error');
|
||||
showToast.error('Error al probar la conexión');
|
||||
} finally {
|
||||
setTestingConnection(null);
|
||||
}
|
||||
@@ -423,10 +423,10 @@ const BakeryConfigPage: React.FC = () => {
|
||||
tenant_id: tenantId,
|
||||
config_id: configId,
|
||||
});
|
||||
addToast('Configuración eliminada correctamente', 'success');
|
||||
showToast.success('Configuración eliminada correctamente');
|
||||
loadPosConfigurations();
|
||||
} catch (error) {
|
||||
addToast('Error al eliminar la configuración', 'error');
|
||||
showToast.error('Error al eliminar la configuración');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1116,4 +1116,4 @@ const BakeryConfigPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BakeryConfigPage;
|
||||
export default BakeryConfigPage;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Store, MapPin, Clock, Settings as SettingsIcon, Save, X, AlertCircle, L
|
||||
import { Button, Card, Input, Select } from '../../../../components/ui';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/Tabs';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useUpdateTenant } from '../../../../api/hooks/tenant';
|
||||
import { useCurrentTenant, useTenantActions } from '../../../../stores/tenant.store';
|
||||
import { useSettings, useUpdateSettings } from '../../../../api/hooks/settings';
|
||||
@@ -49,7 +49,7 @@ interface BusinessHours {
|
||||
|
||||
const BakerySettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation('settings');
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { loadUserTenants, setCurrentTenant } = useTenantActions();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
@@ -221,10 +221,10 @@ const BakerySettingsPage: React.FC = () => {
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast(t('bakery.save_success'), { type: 'success' });
|
||||
showToast.success(t('bakery.save_success'));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : t('common.error');
|
||||
addToast(`${t('bakery.save_error')}: ${errorMessage}`, { type: 'error' });
|
||||
showToast.error(`${t('bakery.save_error')}: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -252,10 +252,10 @@ const BakerySettingsPage: React.FC = () => {
|
||||
});
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
addToast(t('bakery.save_success'), { type: 'success' });
|
||||
showToast.success(t('bakery.save_success'));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : t('common.error');
|
||||
addToast(`${t('bakery.save_error')}: ${errorMessage}`, { type: 'error' });
|
||||
showToast.error(`${t('bakery.save_error')}: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
Sun,
|
||||
Settings
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
|
||||
// Backend-aligned preference types
|
||||
export interface NotificationPreferences {
|
||||
@@ -75,7 +75,7 @@ const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
|
||||
onReset,
|
||||
hasChanges
|
||||
}) => {
|
||||
const { addToast } = useToast();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [preferences, setPreferences] = useState<NotificationPreferences>({
|
||||
@@ -161,9 +161,9 @@ const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await onSave(preferences);
|
||||
addToast('Preferencias guardadas correctamente', 'success');
|
||||
showToast.success('Preferencias guardadas correctamente');
|
||||
} catch (error) {
|
||||
addToast('Error al guardar las preferencias', 'error');
|
||||
showToast.error('Error al guardar las preferencias');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -700,4 +700,4 @@ const CommunicationPreferences: React.FC<CommunicationPreferencesProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunicationPreferences;
|
||||
export default CommunicationPreferences;
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { Button, Card, Avatar, Input, Select } from '../../../../components/ui';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/Tabs';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useAuthUser, useAuthActions } from '../../../../stores/auth.store';
|
||||
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
@@ -49,7 +49,7 @@ interface PasswordData {
|
||||
const NewProfileSettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation('settings');
|
||||
const navigate = useNavigate();
|
||||
const { addToast } = useToast();
|
||||
|
||||
const user = useAuthUser();
|
||||
const { logout } = useAuthActions();
|
||||
const currentTenant = useCurrentTenant();
|
||||
@@ -169,9 +169,9 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
await updateProfileMutation.mutateAsync(profileData);
|
||||
|
||||
setIsEditing(false);
|
||||
addToast(t('profile.save_changes'), { type: 'success' });
|
||||
showToast.success(t('profile.save_changes'));
|
||||
} catch (error) {
|
||||
addToast(t('common.error'), { type: 'error' });
|
||||
showToast.error(t('common.error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -191,9 +191,9 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
|
||||
setShowPasswordForm(false);
|
||||
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
addToast(t('profile.password.change_success'), { type: 'success' });
|
||||
showToast.success(t('profile.password.change_success'));
|
||||
} catch (error) {
|
||||
addToast(t('profile.password.change_error'), { type: 'error' });
|
||||
showToast.error(t('profile.password.change_error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -246,9 +246,9 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
addToast(t('profile.privacy.export_success'), { type: 'success' });
|
||||
showToast.success(t('profile.privacy.export_success'));
|
||||
} catch (err) {
|
||||
addToast(t('profile.privacy.export_error'), { type: 'error' });
|
||||
showToast.error(t('profile.privacy.export_error'));
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
@@ -256,12 +256,12 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
|
||||
const handleAccountDeletion = async () => {
|
||||
if (deleteConfirmEmail.toLowerCase() !== user?.email?.toLowerCase()) {
|
||||
addToast(t('common.error'), { type: 'error' });
|
||||
showToast.error(t('common.error'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deletePassword) {
|
||||
addToast(t('common.error'), { type: 'error' });
|
||||
showToast.error(t('common.error'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -270,14 +270,14 @@ const NewProfileSettingsPage: React.FC = () => {
|
||||
const { authService } = await import('../../../../api');
|
||||
await authService.deleteAccount(deleteConfirmEmail, deletePassword, deleteReason);
|
||||
|
||||
addToast(t('common.success'), { type: 'success' });
|
||||
showToast.success(t('common.success'));
|
||||
|
||||
setTimeout(() => {
|
||||
logout();
|
||||
navigate('/');
|
||||
}, 2000);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || t('common.error'), { type: 'error' });
|
||||
showToast.error(err.message || t('common.error'));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, Card, Avatar, Input, Select, Tabs, Badge, Modal } from '../../.
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { useAuthProfile, useUpdateProfile, useChangePassword } from '../../../../api/hooks/auth';
|
||||
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -30,7 +30,7 @@ interface PasswordData {
|
||||
const ProfilePage: React.FC = () => {
|
||||
const user = useAuthUser();
|
||||
const { t } = useTranslation(['settings', 'auth']);
|
||||
const { addToast } = useToast();
|
||||
|
||||
|
||||
const { data: profile, isLoading: profileLoading, error: profileError } = useAuthProfile();
|
||||
const updateProfileMutation = useUpdateProfile();
|
||||
@@ -176,9 +176,9 @@ const ProfilePage: React.FC = () => {
|
||||
await updateProfileMutation.mutateAsync(profileData);
|
||||
|
||||
setIsEditing(false);
|
||||
addToast('Perfil actualizado correctamente', 'success');
|
||||
showToast.success('Perfil actualizado correctamente');
|
||||
} catch (error) {
|
||||
addToast('No se pudo actualizar tu perfil', 'error');
|
||||
showToast.error('No se pudo actualizar tu perfil');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -198,9 +198,9 @@ const ProfilePage: React.FC = () => {
|
||||
|
||||
setShowPasswordForm(false);
|
||||
setPasswordData({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
addToast('Contraseña actualizada correctamente', 'success');
|
||||
showToast.success('Contraseña actualizada correctamente');
|
||||
} catch (error) {
|
||||
addToast('No se pudo cambiar tu contraseña', 'error');
|
||||
showToast.error('No se pudo cambiar tu contraseña');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -269,7 +269,7 @@ const ProfilePage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('No se encontró información del tenant', 'error');
|
||||
showToast.error('No se encontró información del tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ const ProfilePage: React.FC = () => {
|
||||
setAvailablePlans(plans);
|
||||
} catch (error) {
|
||||
console.error('Error loading subscription data:', error);
|
||||
addToast("No se pudo cargar la información de suscripción", 'error');
|
||||
showToast.error("No se pudo cargar la información de suscripción");
|
||||
} finally {
|
||||
setSubscriptionLoading(false);
|
||||
}
|
||||
@@ -299,7 +299,7 @@ const ProfilePage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId || !selectedPlan) {
|
||||
addToast('Información de tenant no disponible', 'error');
|
||||
showToast.error('Información de tenant no disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,24 +312,24 @@ const ProfilePage: React.FC = () => {
|
||||
);
|
||||
|
||||
if (!validation.can_upgrade) {
|
||||
addToast(validation.reason || 'No se puede actualizar el plan', 'error');
|
||||
return;
|
||||
showToast.error(validation.reason || 'No se puede actualizar el plan');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await subscriptionService.upgradePlan(tenantId, selectedPlan);
|
||||
|
||||
if (result.success) {
|
||||
addToast(result.message, 'success');
|
||||
showToast.success(result.message);
|
||||
|
||||
await loadSubscriptionData();
|
||||
setUpgradeDialogOpen(false);
|
||||
setSelectedPlan('');
|
||||
} else {
|
||||
addToast('Error al cambiar el plan', 'error');
|
||||
showToast.error('Error al cambiar el plan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error upgrading plan:', error);
|
||||
addToast('Error al procesar el cambio de plan', 'error');
|
||||
showToast.error('Error al procesar el cambio de plan');
|
||||
} finally {
|
||||
setUpgrading(false);
|
||||
}
|
||||
@@ -953,4 +953,4 @@ const ProfilePage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
export default ProfilePage;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DialogModal } from '../../../../components/ui/DialogModal/DialogModal';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { subscriptionService, type UsageSummary, type AvailablePlans } from '../../../../api';
|
||||
import { useSubscriptionEvents } from '../../../../contexts/SubscriptionEventsContext';
|
||||
import { SubscriptionPricingCards } from '../../../../components/subscription/SubscriptionPricingCards';
|
||||
@@ -13,7 +13,6 @@ import { SubscriptionPricingCards } from '../../../../components/subscription/Su
|
||||
const SubscriptionPage: React.FC = () => {
|
||||
const user = useAuthUser();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { addToast } = useToast();
|
||||
const { notifySubscriptionChanged } = useSubscriptionEvents();
|
||||
|
||||
const [usageSummary, setUsageSummary] = useState<UsageSummary | null>(null);
|
||||
@@ -36,7 +35,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('No se encontró información del tenant', { type: 'error' });
|
||||
showToast.error('No se encontró información del tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,7 +119,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
setAvailablePlans(plans);
|
||||
} catch (error) {
|
||||
console.error('Error loading subscription data:', error);
|
||||
addToast("No se pudo cargar la información de suscripción", { type: 'error' });
|
||||
showToast.error("No se pudo cargar la información de suscripción");
|
||||
} finally {
|
||||
setSubscriptionLoading(false);
|
||||
}
|
||||
@@ -135,7 +134,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId || !selectedPlan) {
|
||||
addToast('Información de tenant no disponible', { type: 'error' });
|
||||
showToast.error('Información de tenant no disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,14 +147,17 @@ const SubscriptionPage: React.FC = () => {
|
||||
);
|
||||
|
||||
if (!validation.can_upgrade) {
|
||||
addToast(validation.reason || 'No se puede actualizar el plan', { type: 'error' });
|
||||
showToast.error(validation.reason || 'No se puede actualizar el plan');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await subscriptionService.upgradePlan(tenantId, selectedPlan);
|
||||
|
||||
if (result.success) {
|
||||
addToast(result.message, { type: 'success' });
|
||||
showToast.success(result.message);
|
||||
|
||||
// Invalidate cache to ensure fresh data on next fetch
|
||||
subscriptionService.invalidateCache();
|
||||
|
||||
// Broadcast subscription change event to refresh sidebar and other components
|
||||
notifySubscriptionChanged();
|
||||
@@ -164,11 +166,11 @@ const SubscriptionPage: React.FC = () => {
|
||||
setUpgradeDialogOpen(false);
|
||||
setSelectedPlan('');
|
||||
} else {
|
||||
addToast('Error al cambiar el plan', { type: 'error' });
|
||||
showToast.error('Error al cambiar el plan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error upgrading plan:', error);
|
||||
addToast('Error al procesar el cambio de plan', { type: 'error' });
|
||||
showToast.error('Error al procesar el cambio de plan');
|
||||
} finally {
|
||||
setUpgrading(false);
|
||||
}
|
||||
@@ -182,7 +184,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('Información de tenant no disponible', { type: 'error' });
|
||||
showToast.error('Información de tenant no disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,9 +201,8 @@ const SubscriptionPage: React.FC = () => {
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
addToast(
|
||||
`Suscripción cancelada. Acceso de solo lectura a partir del ${effectiveDate} (${daysRemaining} días restantes)`,
|
||||
{ type: 'success' }
|
||||
showToast.success(
|
||||
`Suscripción cancelada. Acceso de solo lectura a partir del ${effectiveDate} (${daysRemaining} días restantes)`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,7 +210,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
setCancellationDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Error cancelling subscription:', error);
|
||||
addToast('Error al cancelar la suscripción', { type: 'error' });
|
||||
showToast.error('Error al cancelar la suscripción');
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
@@ -219,7 +220,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
|
||||
if (!tenantId) {
|
||||
addToast('No se encontró información del tenant', { type: 'error' });
|
||||
showToast.error('No se encontró información del tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading invoices:', error);
|
||||
addToast('Error al cargar las facturas', { type: 'error' });
|
||||
showToast.error('Error al cargar las facturas');
|
||||
} finally {
|
||||
setInvoicesLoading(false);
|
||||
}
|
||||
@@ -245,7 +246,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
const handleDownloadInvoice = (invoiceId: string) => {
|
||||
// In a real implementation, this would download the actual invoice
|
||||
console.log(`Downloading invoice: ${invoiceId}`);
|
||||
addToast(`Descargando factura ${invoiceId}`, { type: 'info' });
|
||||
showToast.info(`Descargando factura ${invoiceId}`);
|
||||
};
|
||||
|
||||
const ProgressBar: React.FC<{ value: number; className?: string }> = ({ value, className = '' }) => {
|
||||
@@ -389,7 +390,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.users.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.users.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.users.unlimited ? 'Ilimitado' : `${usageSummary.usage.users.limit - usageSummary.usage.users.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.users.unlimited ? 'Ilimitado' : `${(usageSummary.usage.users.limit ?? 0) - usageSummary.usage.users.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -410,7 +411,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.locations.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.locations.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.locations.unlimited ? 'Ilimitado' : `${usageSummary.usage.locations.limit - usageSummary.usage.locations.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.locations.unlimited ? 'Ilimitado' : `${(usageSummary.usage.locations.limit ?? 0) - usageSummary.usage.locations.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -437,7 +438,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.products.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.products.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.products.unlimited ? 'Ilimitado' : `${usageSummary.usage.products.limit - usageSummary.usage.products.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.products.unlimited ? 'Ilimitado' : `${(usageSummary.usage.products.limit ?? 0) - usageSummary.usage.products.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -458,7 +459,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.recipes.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.recipes.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.recipes.unlimited ? 'Ilimitado' : `${usageSummary.usage.recipes.limit - usageSummary.usage.recipes.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.recipes.unlimited ? 'Ilimitado' : `${(usageSummary.usage.recipes.limit ?? 0) - usageSummary.usage.recipes.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -479,7 +480,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.suppliers.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.suppliers.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.suppliers.unlimited ? 'Ilimitado' : `${usageSummary.usage.suppliers.limit - usageSummary.usage.suppliers.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.suppliers.unlimited ? 'Ilimitado' : `${(usageSummary.usage.suppliers.limit ?? 0) - usageSummary.usage.suppliers.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -506,7 +507,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.training_jobs_today.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.training_jobs_today.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.training_jobs_today.unlimited ? 'Ilimitado' : `${usageSummary.usage.training_jobs_today.limit - usageSummary.usage.training_jobs_today.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.training_jobs_today.unlimited ? 'Ilimitado' : `${(usageSummary.usage.training_jobs_today.limit ?? 0) - usageSummary.usage.training_jobs_today.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -527,7 +528,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.forecasts_today.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.forecasts_today.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.forecasts_today.unlimited ? 'Ilimitado' : `${usageSummary.usage.forecasts_today.limit - usageSummary.usage.forecasts_today.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.forecasts_today.unlimited ? 'Ilimitado' : `${(usageSummary.usage.forecasts_today.limit ?? 0) - usageSummary.usage.forecasts_today.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -554,7 +555,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.api_calls_this_hour.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.api_calls_this_hour.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.api_calls_this_hour.unlimited ? 'Ilimitado' : `${usageSummary.usage.api_calls_this_hour.limit - usageSummary.usage.api_calls_this_hour.current} restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.api_calls_this_hour.unlimited ? 'Ilimitado' : `${(usageSummary.usage.api_calls_this_hour.limit ?? 0) - usageSummary.usage.api_calls_this_hour.current} restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -575,7 +576,7 @@ const SubscriptionPage: React.FC = () => {
|
||||
<ProgressBar value={usageSummary.usage.file_storage_used_gb.usage_percentage} />
|
||||
<p className="text-xs text-[var(--text-secondary)] flex items-center justify-between">
|
||||
<span>{usageSummary.usage.file_storage_used_gb.usage_percentage}% utilizado</span>
|
||||
<span className="font-medium">{usageSummary.usage.file_storage_used_gb.unlimited ? 'Ilimitado' : `${(usageSummary.usage.file_storage_used_gb.limit - usageSummary.usage.file_storage_used_gb.current).toFixed(2)} GB restantes`}</span>
|
||||
<span className="font-medium">{usageSummary.usage.file_storage_used_gb.unlimited ? 'Ilimitado' : `${((usageSummary.usage.file_storage_used_gb.limit ?? 0) - usageSummary.usage.file_storage_used_gb.current).toFixed(2)} GB restantes`}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,13 @@ import { useUserActivity } from '../../../../api/hooks/user';
|
||||
import { userService } from '../../../../api/services/user';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant, useCurrentTenantAccess } from '../../../../stores/tenant.store';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { showToast } from '../../../../utils/toast';
|
||||
import { TENANT_ROLES, type TenantRole } from '../../../../types/roles';
|
||||
import { subscriptionService } from '../../../../api/services/subscription';
|
||||
|
||||
const TeamPage: React.FC = () => {
|
||||
const { t } = useTranslation(['settings']);
|
||||
const { addToast } = useToast();
|
||||
|
||||
const currentUser = useAuthUser();
|
||||
const currentTenant = useCurrentTenant();
|
||||
const currentTenantAccess = useCurrentTenantAccess();
|
||||
@@ -310,7 +310,7 @@ const TeamPage: React.FC = () => {
|
||||
setShowActivityModal(true);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user activity:', error);
|
||||
addToast('Error al cargar la actividad del usuario', { type: 'error' });
|
||||
showToast.error('Error al cargar la actividad del usuario');
|
||||
} finally {
|
||||
setActivityLoading(false);
|
||||
}
|
||||
@@ -359,9 +359,9 @@ const TeamPage: React.FC = () => {
|
||||
memberUserId,
|
||||
});
|
||||
|
||||
addToast('Miembro removido exitosamente', { type: 'success' });
|
||||
showToast.success('Miembro removido exitosamente');
|
||||
} catch (error) {
|
||||
addToast('Error al remover miembro', { type: 'error' });
|
||||
showToast.error('Error al remover miembro');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -375,9 +375,9 @@ const TeamPage: React.FC = () => {
|
||||
newRole,
|
||||
});
|
||||
|
||||
addToast('Rol actualizado exitosamente', { type: 'success' });
|
||||
showToast.success('Rol actualizado exitosamente');
|
||||
} catch (error) {
|
||||
addToast('Error al actualizar rol', { type: 'error' });
|
||||
showToast.error('Error al actualizar rol');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -556,7 +556,7 @@ const TeamPage: React.FC = () => {
|
||||
if (!usageCheck.allowed) {
|
||||
const errorMessage = usageCheck.message ||
|
||||
`Has alcanzado el límite de ${usageCheck.limit} usuarios para tu plan. Actualiza tu suscripción para agregar más miembros.`;
|
||||
addToast(errorMessage, { type: 'error' });
|
||||
showToast.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
@@ -579,14 +579,14 @@ const TeamPage: React.FC = () => {
|
||||
timezone: 'Europe/Madrid'
|
||||
}
|
||||
});
|
||||
addToast('Usuario creado y agregado exitosamente', { type: 'success' });
|
||||
showToast.success('Usuario creado y agregado exitosamente');
|
||||
} else {
|
||||
await addMemberMutation.mutateAsync({
|
||||
tenantId,
|
||||
userId: userData.userId!,
|
||||
role,
|
||||
});
|
||||
addToast('Miembro agregado exitosamente', { type: 'success' });
|
||||
showToast.success('Miembro agregado exitosamente');
|
||||
}
|
||||
|
||||
setShowAddForm(false);
|
||||
@@ -597,9 +597,8 @@ const TeamPage: React.FC = () => {
|
||||
// Limit error already toasted above
|
||||
throw error;
|
||||
}
|
||||
addToast(
|
||||
userData.createUser ? 'Error al crear usuario' : 'Error al agregar miembro',
|
||||
{ type: 'error' }
|
||||
showToast.error(
|
||||
userData.createUser ? 'Error al crear usuario' : 'Error al agregar miembro'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user