Start integrating the onboarding flow with backend 4

This commit is contained in:
Urtzi Alfaro
2025-09-05 12:55:26 +02:00
parent 0faaa25e58
commit 3fe1f17610
26 changed files with 2161 additions and 1002 deletions

View File

@@ -46,13 +46,11 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
const currentStep = steps[currentStepIndex];
const updateStepData = useCallback((stepId: string, data: any) => {
console.log(`OnboardingWizard - Updating step '${stepId}' with data:`, data);
setStepData(prev => {
const newStepData = {
...prev,
[stepId]: { ...prev[stepId], ...data }
};
console.log(`OnboardingWizard - Full step data after update:`, newStepData);
return newStepData;
});
@@ -420,7 +418,6 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
allStepData: stepData
}}
onDataChange={(data) => {
console.log(`OnboardingWizard - Step ${currentStep.id} calling onDataChange with:`, data);
updateStepData(currentStep.id, data);
}}
onNext={goToNextStep}

View File

@@ -2,7 +2,9 @@ import React, { useState, useEffect } from 'react';
import { CheckCircle, Star, Rocket, Gift, Download, Share2, ArrowRight, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { onboardingApiService } from '../../../../services/api/onboarding.service';
import { useInventory } from '../../../../hooks/api/useInventory';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useAlertActions } from '../../../../stores/alerts.store';
@@ -27,6 +29,12 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
}) => {
const user = useAuthUser();
const { createAlert } = useAlertActions();
const { showToast } = useToast();
const { createInventoryFromSuggestions, isLoading: inventoryLoading } = useInventory();
const certificateModal = useModal();
const demoModal = useModal();
const shareModal = useModal();
const [showConfetti, setShowConfetti] = useState(false);
const [completionStats, setCompletionStats] = useState<CompletionStats | null>(null);
const [isImportingSales, setIsImportingSales] = useState(false);
@@ -47,10 +55,10 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
setIsImportingSales(true);
try {
const result = await onboardingApiService.importSalesWithInventory(
user.tenant_id,
data.files.salesData,
data.inventoryMapping
// Sales data should already be imported during DataProcessingStep
// Just create inventory items from approved suggestions
const result = await createInventoryFromSuggestions(
data.approvedSuggestions || []
);
createAlert({
@@ -167,19 +175,28 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
};
console.log('Generating certificate:', certificateData);
alert(`🎓 Certificado generado para ${certificateData.bakeryName}\nPuntuación: ${certificateData.score}/100`);
certificateModal.openModal({
title: '🎓 Certificado Generado',
message: `Certificado generado para ${certificateData.bakeryName}\nPuntuación: ${certificateData.score}/100`
});
};
const scheduleDemo = () => {
// Mock demo scheduling
alert('📅 Te contactaremos pronto para agendar una demostración personalizada de las funcionalidades avanzadas.');
demoModal.openModal({
title: '📅 Demo Agendado',
message: 'Te contactaremos pronto para agendar una demostración personalizada de las funcionalidades avanzadas.'
});
};
const shareSuccess = () => {
// Mock social sharing
const shareText = `¡Acabo de completar la configuración de mi panadería inteligente con IA! 🥖🤖 Puntuación: ${completionStats?.completionScore}/100`;
navigator.clipboard.writeText(shareText);
alert('✅ Texto copiado al portapapeles. ¡Compártelo en tus redes sociales!');
shareModal.openModal({
title: '✅ ¡Compartido!',
message: 'Texto copiado al portapapeles. ¡Compártelo en tus redes sociales!'
});
};
const quickStartActions = [

View File

@@ -2,7 +2,12 @@ import React, { useState, useRef, useEffect } from 'react';
import { Upload, Brain, CheckCircle, AlertCircle, Download, FileText, Activity, TrendingUp } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { onboardingApiService } from '../../../../services/api/onboarding.service';
import { useInventory } from '../../../../hooks/api/useInventory';
import { useSales } from '../../../../hooks/api/useSales';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { salesService } from '../../../../services/api/sales.service';
import { inventoryService } from '../../../../services/api/inventory.service';
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
import { useCurrentTenant, useTenantLoading } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
@@ -30,70 +35,90 @@ interface ProcessingResult {
recommendations: string[];
}
// Real data processing service using backend APIs
const dataProcessingService = {
processFile: async (
file: File,
tenantId: string,
onProgress: (progress: number, stage: string, message: string) => void
) => {
try {
// Stage 1: Validate file with sales service
onProgress(20, 'validating', 'Validando estructura del archivo...');
const validationResult = await onboardingApiService.validateOnboardingFile(tenantId, file);
onProgress(40, 'validating', 'Verificando integridad de datos...');
if (!validationResult.is_valid) {
throw new Error('Archivo de datos inválido');
}
if (!validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos en el archivo');
}
// Stage 2: Generate AI suggestions with inventory service
onProgress(60, 'analyzing', 'Identificando productos únicos...');
onProgress(80, 'analyzing', 'Analizando patrones de venta...');
console.log('DataProcessingStep - Calling generateInventorySuggestions with:', {
tenantId,
fileName: file.name,
productList: validationResult.product_list
});
const suggestionsResult = await onboardingApiService.generateInventorySuggestions(
tenantId,
file,
validationResult.product_list
);
console.log('DataProcessingStep - AI suggestions result:', suggestionsResult);
onProgress(90, 'analyzing', 'Generando recomendaciones con IA...');
onProgress(100, 'completed', 'Procesamiento completado');
// Combine results
const combinedResult = {
...validationResult,
productsIdentified: suggestionsResult.total_products || validationResult.unique_products,
categoriesDetected: suggestionsResult.suggestions ?
new Set(suggestionsResult.suggestions.map(s => s.category)).size : 4,
businessModel: suggestionsResult.business_model_analysis?.model || 'production',
confidenceScore: suggestionsResult.high_confidence_count && suggestionsResult.total_products ?
Math.round((suggestionsResult.high_confidence_count / suggestionsResult.total_products) * 100) : 85,
recommendations: suggestionsResult.business_model_analysis?.recommendations || [],
aiSuggestions: suggestionsResult.suggestions || []
};
console.log('DataProcessingStep - Combined result:', combinedResult);
console.log('DataProcessingStep - Combined result aiSuggestions:', combinedResult.aiSuggestions);
console.log('DataProcessingStep - Combined result aiSuggestions length:', combinedResult.aiSuggestions?.length);
return combinedResult;
} catch (error) {
console.error('Data processing error:', error);
throw error;
// Data processing utility function
const processDataFile = async (
file: File,
onProgress: (progress: number, stage: string, message: string) => void,
validateSalesData: any,
generateInventorySuggestions: any
) => {
try {
// Stage 1: Validate file with sales service
onProgress(20, 'validating', 'Validando estructura del archivo...');
const validationResult = await validateSalesData(file);
onProgress(40, 'validating', 'Verificando integridad de datos...');
if (!validationResult.is_valid) {
throw new Error('Archivo de datos inválido');
}
if (!validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos en el archivo');
}
// Stage 2: Store validation result for later import (after inventory setup)
onProgress(50, 'validating', 'Procesando datos identificados...');
// Stage 3: Generate AI suggestions with inventory service
onProgress(60, 'analyzing', 'Identificando productos únicos...');
onProgress(80, 'analyzing', 'Analizando patrones de venta...');
console.log('DataProcessingStep - Validation result:', validationResult);
console.log('DataProcessingStep - Product list:', validationResult.product_list);
console.log('DataProcessingStep - Product list length:', validationResult.product_list?.length);
// Extract product list from validation result
const productList = validationResult.product_list || [];
console.log('DataProcessingStep - Generating AI suggestions with:', {
fileName: file.name,
productList: productList,
productListLength: productList.length
});
let suggestionsResult;
if (productList.length > 0) {
suggestionsResult = await generateInventorySuggestions(productList);
} else {
console.warn('DataProcessingStep - No products found, creating default suggestions');
suggestionsResult = {
suggestions: [],
total_products: validationResult.unique_products || 0,
business_model_analysis: {
model: 'production' as const,
recommendations: []
},
high_confidence_count: 0
};
}
console.log('DataProcessingStep - AI suggestions result:', suggestionsResult);
onProgress(90, 'analyzing', 'Generando recomendaciones con IA...');
onProgress(100, 'completed', 'Procesamiento completado');
// Combine results
const combinedResult = {
...validationResult,
salesDataFile: file, // Store file for later import after inventory setup
productsIdentified: suggestionsResult.total_products || validationResult.unique_products,
categoriesDetected: suggestionsResult.suggestions ?
new Set(suggestionsResult.suggestions.map(s => s.category)).size : 4,
businessModel: suggestionsResult.business_model_analysis?.model || 'production',
confidenceScore: suggestionsResult.high_confidence_count && suggestionsResult.total_products ?
Math.round((suggestionsResult.high_confidence_count / suggestionsResult.total_products) * 100) : 85,
recommendations: suggestionsResult.business_model_analysis?.recommendations || [],
aiSuggestions: suggestionsResult.suggestions || []
};
console.log('DataProcessingStep - Combined result:', combinedResult);
console.log('DataProcessingStep - Combined result aiSuggestions:', combinedResult.aiSuggestions);
console.log('DataProcessingStep - Combined result aiSuggestions length:', combinedResult.aiSuggestions?.length);
return combinedResult;
} catch (error) {
console.error('Data processing error:', error);
throw error;
}
};
@@ -111,6 +136,12 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
const tenantLoading = useTenantLoading();
const { createAlert } = useAlertActions();
// Use hooks for UI and direct service calls for now (until we extend hooks)
const { isLoading: inventoryLoading } = useInventory();
const { isLoading: salesLoading } = useSales();
const errorModal = useModal();
const { showToast } = useToast();
// Check if we're still loading user or tenant data
const isLoadingUserData = authLoading || tenantLoading;
@@ -189,13 +220,21 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
if (!validExtensions.includes(fileExtension)) {
alert('Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)');
showToast({
title: 'Formato inválido',
message: 'Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)',
type: 'error'
});
return;
}
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
alert('El archivo es demasiado grande. Máximo 10MB permitido.');
showToast({
title: 'Archivo muy grande',
message: 'El archivo es demasiado grande. Máximo 10MB permitido.',
type: 'error'
});
return;
}
@@ -236,14 +275,15 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
console.log('DataProcessingStep - Starting file processing with tenant:', tenantId);
const result = await dataProcessingService.processFile(
const result = await processDataFile(
file,
tenantId,
(newProgress, newStage, message) => {
setProgress(newProgress);
setStage(newStage as ProcessingStage);
setCurrentMessage(message);
}
},
salesService.validateSalesData.bind(salesService),
inventoryService.generateInventorySuggestions.bind(inventoryService)
);
setResults(result);
@@ -321,8 +361,14 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
return;
}
const templateData = await onboardingApiService.getSalesImportTemplate(tenantId, 'csv');
onboardingApiService.downloadTemplate(templateData, 'plantilla_ventas.csv', 'csv');
// Template download functionality can be implemented later if needed
console.warn('Template download not yet implemented in reorganized structure');
createAlert({
type: 'info',
category: 'system',
title: 'Descarga de plantilla no disponible',
message: 'Esta funcionalidad se implementará próximamente.'
});
createAlert({
type: 'success',

View File

@@ -2,8 +2,12 @@ import React, { useState, useEffect } from 'react';
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2, CheckCircle } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { onboardingApiService } from '../../../../services/api/onboarding.service';
import { useInventory } from '../../../../hooks/api/useInventory';
import { useSales } from '../../../../hooks/api/useSales';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
interface InventoryItem {
@@ -57,7 +61,28 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
isLastStep
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { createAlert } = useAlertActions();
const { showToast } = useToast();
// Use inventory hook for API operations
const {
createIngredient,
isLoading: inventoryLoading,
error: inventoryError,
clearError: clearInventoryError
} = useInventory();
// Use sales hook for importing sales data
const {
importSalesData,
isLoading: salesLoading
} = useSales();
// Use modal for confirmations and editing
const editModal = useModal();
const confirmModal = useModal();
const [isCreating, setIsCreating] = useState(false);
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
@@ -104,16 +129,30 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
? items
: items.filter(item => item.category === filterCategory);
// Create inventory items via API
// Create inventory items via API using hooks
const handleCreateInventory = async () => {
console.log('InventorySetup - Starting handleCreateInventory');
console.log('InventorySetup - data:', data);
console.log('InventorySetup - data.allStepData keys:', Object.keys(data.allStepData || {}));
const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts;
if (!user?.tenant_id || !approvedProducts || approvedProducts.length === 0) {
console.log('InventorySetup - approvedProducts:', approvedProducts);
// Get tenant ID from current tenant context or user
const tenantId = currentTenant?.id || user?.tenant_id;
console.log('InventorySetup - tenantId from currentTenant:', currentTenant?.id);
console.log('InventorySetup - tenantId from user:', user?.tenant_id);
console.log('InventorySetup - final tenantId:', tenantId);
if (!tenantId || !approvedProducts || approvedProducts.length === 0) {
console.log('InventorySetup - Missing requirements: tenantId =', tenantId, 'approvedProducts length =', approvedProducts?.length);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: 'No se pueden crear elementos de inventario sin productos aprobados.',
message: !tenantId ? 'No se pudo obtener información del tenant' : 'No se pueden crear elementos de inventario sin productos aprobados.',
source: 'onboarding'
});
return;
@@ -121,28 +160,135 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
setIsCreating(true);
try {
const result = await onboardingApiService.createInventoryFromSuggestions(
user.tenant_id,
approvedProducts
);
// Create ingredients one by one using the inventory hook
let successCount = 0;
let failCount = 0;
const createdItems: any[] = [];
const inventoryMapping: { [productName: string]: string } = {};
for (const product of approvedProducts) {
const ingredientData = {
name: product.suggested_name || product.name,
category: product.category || 'general',
unit_of_measure: product.unit_of_measure || 'unit',
shelf_life_days: product.estimated_shelf_life_days || 30,
requires_refrigeration: product.requires_refrigeration || false,
requires_freezing: product.requires_freezing || false,
is_seasonal: product.is_seasonal || false
};
try {
const success = await createIngredient(ingredientData);
if (success) {
successCount++;
// Mock created item data since hook doesn't return it
const createdItem = { ...ingredientData, id: `created-${Date.now()}-${successCount}` };
createdItems.push(createdItem);
inventoryMapping[product.original_name || product.name] = createdItem.id;
} else {
failCount++;
}
} catch (ingredientError) {
console.error('Error creating ingredient:', product.name, ingredientError);
failCount++;
// For onboarding, continue even if backend is not ready
}
}
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Inventario creado',
message: `Se crearon ${result.created_items.length} elementos de inventario exitosamente.`,
source: 'onboarding'
});
// Show results
if (successCount > 0) {
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Inventario creado',
message: `Se crearon ${successCount} elementos de inventario exitosamente.`,
source: 'onboarding'
});
} else if (failCount > 0) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al crear inventario',
message: `No se pudieron crear los elementos de inventario. Backend no disponible.`,
source: 'onboarding'
});
// Don't continue with sales import if inventory creation failed
return;
}
// Update the step data with created inventory
onDataChange({
// Now upload sales data to backend (required for ML training)
const salesDataFile = data.allStepData?.['data-processing']?.salesDataFile;
const processingResults = data.allStepData?.['data-processing']?.processingResults;
console.log('InventorySetup - salesDataFile:', salesDataFile);
console.log('InventorySetup - processingResults:', processingResults);
let salesImportResult = null;
if (salesDataFile && processingResults?.is_valid) {
try {
createAlert({
type: 'info',
category: 'system',
priority: 'medium',
title: 'Subiendo datos de ventas',
message: 'Subiendo historial de ventas al sistema para entrenamiento de IA...',
source: 'onboarding'
});
const importSuccess = await importSalesData(salesDataFile, 'csv');
if (importSuccess) {
salesImportResult = {
records_created: processingResults.total_records,
success: true,
imported: true
};
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Datos de ventas subidos',
message: `Se subieron ${processingResults.total_records} registros de ventas al sistema exitosamente.`,
source: 'onboarding'
});
} else {
throw new Error('Failed to upload sales data');
}
} catch (salesError) {
console.error('Error uploading sales data:', salesError);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al subir datos de ventas',
message: 'El inventario se creó correctamente, pero hubo un problema al subir los datos de ventas. Esto es requerido para el entrenamiento de IA.',
source: 'onboarding'
});
// Set failed result
salesImportResult = {
records_created: 0,
success: false,
error: salesError instanceof Error ? salesError.message : 'Error uploading sales data'
};
}
}
// Update the step data with created inventory and sales import result
console.log('InventorySetup - Updating step data with salesImportResult:', salesImportResult);
const updatedData = {
...data,
inventoryItems: items,
inventoryConfigured: true,
inventoryMapping: result.inventory_mapping,
createdInventoryItems: result.created_items
});
inventoryCreated: true, // Mark as created to prevent duplicate calls
inventoryMapping: inventoryMapping,
createdInventoryItems: createdItems,
salesImportResult: salesImportResult
};
console.log('InventorySetup - updatedData:', updatedData);
onDataChange(updatedData);
} catch (error) {
console.error('Error creating inventory:', error);
@@ -172,6 +318,19 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
});
}, [items, isCreating]);
// Auto-create inventory when step is completed (when user clicks Next)
useEffect(() => {
const hasValidStock = items.length > 0 && items.every(item =>
item.min_stock >= 0 && item.max_stock > item.min_stock
);
// If inventory is configured but not yet created in backend, create it automatically
if (hasValidStock && !data.inventoryCreated && !isCreating) {
console.log('InventorySetup - Auto-creating inventory on step completion');
handleCreateInventory();
}
}, [data.inventoryCreated, items, isCreating]);
const handleAddItem = () => {
const newItem: InventoryItem = {
id: Date.now().toString(),
@@ -220,9 +379,9 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
const getStockStatusColor = (status: string) => {
switch (status) {
case 'critical': return 'text-red-600 bg-red-50';
case 'warning': return 'text-yellow-600 bg-yellow-50';
default: return 'text-green-600 bg-green-50';
case 'critical': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
case 'warning': return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20';
default: return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
}
};
@@ -263,48 +422,48 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="p-6 text-center">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="p-6 text-center bg-[var(--bg-primary)] border-[var(--border-primary)]">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-2">{items.length}</div>
<div className="text-sm text-[var(--text-secondary)]">Elementos totales</div>
<div className="text-sm font-medium text-[var(--text-secondary)]">Elementos totales</div>
</Card>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-blue-600 mb-2">
<Card className="p-6 text-center bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<div className="text-3xl font-bold text-[var(--color-info)] mb-2">
{items.filter(item => item.category === 'ingredient').length}
</div>
<div className="text-sm text-[var(--text-secondary)]">Ingredientes</div>
<div className="text-sm font-medium text-[var(--color-info)]">Ingredientes</div>
</Card>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-green-600 mb-2">
<Card className="p-6 text-center bg-[var(--color-success)]/5 border-[var(--color-success)]/20">
<div className="text-3xl font-bold text-[var(--color-success)] mb-2">
{items.filter(item => item.category === 'finished_product').length}
</div>
<div className="text-sm text-[var(--text-secondary)]">Productos terminados</div>
<div className="text-sm font-medium text-[var(--color-success)]">Productos terminados</div>
</Card>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-red-600 mb-2">
<Card className="p-6 text-center bg-[var(--color-error)]/5 border-[var(--color-error)]/20">
<div className="text-3xl font-bold text-[var(--color-error)] mb-2">
{items.filter(item => getStockStatus(item) === 'critical').length}
</div>
<div className="text-sm text-[var(--text-secondary)]">Stock crítico</div>
<div className="text-sm font-medium text-[var(--color-error)]">Stock crítico</div>
</Card>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg">
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-center space-x-4">
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value as any)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-white"
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
<option value="all">Todos los elementos</option>
<option value="ingredient">Ingredientes</option>
<option value="finished_product">Productos terminados</option>
</select>
<Badge variant="outline" className="text-sm">
<Badge variant="outline" className="text-sm font-medium px-3 py-1 bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-secondary)]">
{filteredItems.length} elementos
</Badge>
</div>
@@ -313,72 +472,96 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
<Button
onClick={handleAddItem}
size="sm"
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white shadow-sm"
>
<Plus className="w-4 h-4 mr-1" />
Agregar elemento
</Button>
<Button
onClick={handleCreateInventory}
disabled={isCreating || items.length === 0 || data.inventoryConfigured}
disabled={isCreating || items.length === 0 || data.inventoryCreated}
size="sm"
variant="outline"
className="border-green-200 text-green-600 hover:bg-green-50"
className="border-[var(--color-success)]/30 text-[var(--color-success)] hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5 disabled:opacity-50 disabled:cursor-not-allowed"
>
<CheckCircle className="w-4 h-4 mr-1" />
{isCreating ? 'Creando...' : data.inventoryConfigured ? 'Inventario creado' : 'Crear inventario'}
{isCreating ? 'Creando...' : data.inventoryCreated ? 'Inventario creado automáticamente' : 'Crear inventario manualmente'}
</Button>
</div>
</div>
{/* Items List */}
<div className="space-y-3">
<div className="space-y-4">
{filteredItems.map((item) => (
<Card key={item.id} className="p-4 hover:shadow-md transition-shadow">
<Card key={item.id} className="p-6 hover:shadow-lg transition-all duration-200 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
{editingItem?.id === item.id ? (
<InventoryItemEditor
item={item}
onSave={handleSaveItem}
onCancel={handleCancelEdit}
showToast={showToast}
/>
) : (
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="font-semibold text-[var(--text-primary)]">{item.name}</h3>
<Badge variant="outline" className="text-xs">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center flex-wrap gap-2 mb-3">
<h3 className="font-semibold text-lg text-[var(--text-primary)] mr-2">{item.name}</h3>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getStockStatus(item) === 'critical' ? getStockStatusColor('critical') : getStockStatus(item) === 'warning' ? getStockStatusColor('warning') : getStockStatusColor('good')}`}>
Stock: {getStockStatus(item) === 'critical' ? '🔴 Crítico' : getStockStatus(item) === 'warning' ? '🟡 Bajo' : '🟢 Bueno'}
</Badge>
</div>
<div className="flex items-center flex-wrap gap-2 mb-3">
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-[var(--color-primary)]/20">
{item.category === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}
</Badge>
<Badge className={`text-xs ${getStockStatusColor(getStockStatus(item))}`}>
Stock: {getStockStatus(item)}
</Badge>
{item.requires_refrigeration && (
<Badge variant="outline" className="text-xs text-blue-600">
Refrigeración
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-info)]/10 text-[var(--color-info)] border border-[var(--color-info)]/20">
Refrigeración
</Badge>
)}
</div>
<div className="text-sm text-[var(--text-secondary)] space-y-1">
<div className="flex space-x-4">
<span>Stock actual: <span className="font-medium">{item.current_stock} {item.unit}</span></span>
<span>Mínimo: <span className="font-medium">{item.min_stock}</span></span>
<span>Máximo: <span className="font-medium">{item.max_stock}</span></span>
<div className="text-sm text-[var(--text-secondary)] space-y-2">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[100px]">Stock actual:</span>
<span className="font-bold text-[var(--text-primary)]">{item.current_stock} {item.unit}</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[70px]">Mínimo:</span>
<span className="font-medium text-[var(--text-primary)]">{item.min_stock}</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[70px]">Máximo:</span>
<span className="font-medium text-[var(--text-primary)]">{item.max_stock}</span>
</div>
</div>
{item.expiry_date && (
<div>Vence: <span className="font-medium">{item.expiry_date}</span></div>
)}
{item.supplier && (
<div>Proveedor: <span className="font-medium">{item.supplier}</span></div>
{(item.expiry_date || item.supplier) && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{item.expiry_date && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Vence:</span>
<span className="font-medium text-[var(--text-primary)]">{item.expiry_date}</span>
</div>
)}
{item.supplier && (
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[80px]">Proveedor:</span>
<span className="font-medium text-[var(--text-primary)]">{item.supplier}</span>
</div>
)}
</div>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex flex-col sm:flex-row gap-2 flex-shrink-0">
<Button
size="sm"
variant="outline"
onClick={() => setEditingItem(item)}
className="text-[var(--color-primary)] border-[var(--color-primary)]/30 hover:bg-[var(--color-primary)]/10 bg-[var(--color-primary)]/5"
>
<Edit className="w-4 h-4 mr-1" />
Editar
@@ -387,7 +570,7 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
size="sm"
variant="outline"
onClick={() => handleDeleteItem(item.id)}
className="text-red-600 border-red-200 hover:bg-red-50"
className="text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5"
>
<Trash2 className="w-4 h-4 mr-1" />
Eliminar
@@ -399,23 +582,21 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
))}
</div>
{/* Navigation */}
<div className="flex justify-between pt-6 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Anterior
</Button>
<Button
onClick={onNext}
disabled={!data.inventoryConfigured}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
>
{isLastStep ? 'Finalizar' : 'Siguiente'}
</Button>
</div>
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
📦 Configuración de Inventario:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Configure stock inicial</strong> - Establezca los niveles de stock actuales para cada producto</li>
<li> <strong>Defina límites</strong> - Establezca stock mínimo y máximo para recibir alertas automáticas</li>
<li> <strong>Agregue detalles</strong> - Incluya fechas de vencimiento, proveedores y unidades de medida</li>
<li> <strong>Marque refrigeración</strong> - Indique qué productos requieren condiciones especiales de almacenamiento</li>
<li> <strong>Edite elementos</strong> - Haga clic en "Editar" para modificar cualquier producto</li>
<li> <strong>Creación automática</strong> - El inventario se creará automáticamente al hacer clic en "Siguiente"</li>
</ul>
</Card>
</div>
);
};
@@ -425,39 +606,49 @@ const InventoryItemEditor: React.FC<{
item: InventoryItem;
onSave: (item: InventoryItem) => void;
onCancel: () => void;
}> = ({ item, onSave, onCancel }) => {
showToast: (toast: any) => void;
}> = ({ item, onSave, onCancel, showToast }) => {
const [editedItem, setEditedItem] = useState<InventoryItem>(item);
const handleSave = () => {
if (!editedItem.name.trim()) {
alert('El nombre es requerido');
showToast({
title: 'Error de validación',
message: 'El nombre es requerido',
type: 'error'
});
return;
}
if (editedItem.min_stock < 0 || editedItem.max_stock <= editedItem.min_stock) {
alert('Los niveles de stock deben ser válidos (máximo > mínimo >= 0)');
showToast({
title: 'Error de validación',
message: 'Los niveles de stock deben ser válidos (máximo > mínimo >= 0)',
type: 'error'
});
return;
}
onSave(editedItem);
};
return (
<div className="space-y-4 p-4 bg-gray-50 rounded-lg">
<div className="space-y-4 p-6 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-secondary)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Nombre</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Nombre</label>
<Input
value={editedItem.name}
onChange={(e) => setEditedItem({ ...editedItem, name: e.target.value })}
placeholder="Nombre del producto"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Categoría</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Categoría</label>
<select
value={editedItem.category}
onChange={(e) => setEditedItem({ ...editedItem, category: e.target.value as any })}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-white"
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
<option value="ingredient">Ingrediente</option>
<option value="finished_product">Producto terminado</option>
@@ -465,59 +656,65 @@ const InventoryItemEditor: React.FC<{
</div>
<div>
<label className="block text-sm font-medium mb-1">Stock actual</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock actual</label>
<Input
type="number"
value={editedItem.current_stock}
onChange={(e) => setEditedItem({ ...editedItem, current_stock: Number(e.target.value) })}
min="0"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Unidad</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Unidad</label>
<Input
value={editedItem.unit}
onChange={(e) => setEditedItem({ ...editedItem, unit: e.target.value })}
placeholder="kg, litros, unidades..."
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Stock mínimo</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock mínimo</label>
<Input
type="number"
value={editedItem.min_stock}
onChange={(e) => setEditedItem({ ...editedItem, min_stock: Number(e.target.value) })}
min="0"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Stock máximo</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Stock máximo</label>
<Input
type="number"
value={editedItem.max_stock}
onChange={(e) => setEditedItem({ ...editedItem, max_stock: Number(e.target.value) })}
min="1"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Fecha de vencimiento</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Fecha de vencimiento</label>
<Input
type="date"
value={editedItem.expiry_date || ''}
onChange={(e) => setEditedItem({ ...editedItem, expiry_date: e.target.value })}
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Proveedor</label>
<label className="block text-sm font-medium mb-2 text-[var(--text-primary)]">Proveedor</label>
<Input
value={editedItem.supplier || ''}
onChange={(e) => setEditedItem({ ...editedItem, supplier: e.target.value })}
placeholder="Nombre del proveedor"
className="bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-primary)]"
/>
</div>
</div>
@@ -528,17 +725,24 @@ const InventoryItemEditor: React.FC<{
type="checkbox"
checked={editedItem.requires_refrigeration}
onChange={(e) => setEditedItem({ ...editedItem, requires_refrigeration: e.target.checked })}
className="rounded"
className="rounded border-[var(--border-primary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
/>
<span className="text-sm">Requiere refrigeración</span>
<span className="text-sm font-medium text-[var(--text-primary)]">Requiere refrigeración</span>
</label>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onCancel}>
<div className="flex justify-end space-x-3 pt-4 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={onCancel}
className="text-[var(--text-secondary)] border-[var(--border-primary)] hover:bg-[var(--bg-tertiary)]"
>
Cancelar
</Button>
<Button onClick={handleSave}>
<Button
onClick={handleSave}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white"
>
Guardar
</Button>
</div>

View File

@@ -11,6 +11,7 @@ import {
TrainingCompletedMessage,
TrainingErrorMessage
} from '../../../../services/realtime/websocket.service';
import { trainingService } from '../../../../services/api/training.service';
interface TrainingMetrics {
accuracy: number;
@@ -35,44 +36,7 @@ interface TrainingJob {
metrics?: TrainingMetrics;
}
// Real training service using backend APIs
class TrainingApiService {
private async apiCall(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`/api${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`API call failed: ${response.statusText}`);
}
return response.json();
}
async startTrainingJob(tenantId: string, startDate?: string, endDate?: string): Promise<TrainingJob> {
return this.apiCall(`/tenants/${tenantId}/training/jobs`, {
method: 'POST',
body: JSON.stringify({
start_date: startDate,
end_date: endDate
}),
});
}
async getTrainingJob(tenantId: string, jobId: string): Promise<TrainingJob> {
return this.apiCall(`/tenants/${tenantId}/training/jobs/${jobId}`);
}
async getTrainingJobs(tenantId: string): Promise<TrainingJob[]> {
return this.apiCall(`/tenants/${tenantId}/training/jobs`);
}
}
const trainingService = new TrainingApiService();
// Using the proper training service from services/api/training.service.ts
export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
data,
@@ -114,13 +78,25 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
console.log('MLTrainingStep - dataProcessingData:', dataProcessingData);
console.log('MLTrainingStep - reviewData:', reviewData);
console.log('MLTrainingStep - inventoryData:', inventoryData);
console.log('MLTrainingStep - inventoryData.salesImportResult:', inventoryData?.salesImportResult);
// Check if sales data was processed
const hasProcessingResults = dataProcessingData?.processingResults &&
dataProcessingData.processingResults.is_valid &&
dataProcessingData.processingResults.total_records > 0;
// Check if sales data was imported (required for training)
const hasImportResults = inventoryData?.salesImportResult &&
(inventoryData.salesImportResult.records_created > 0 ||
inventoryData.salesImportResult.success === true ||
inventoryData.salesImportResult.imported === true);
if (!hasProcessingResults) {
missingItems.push('Datos de ventas validados');
}
// Sales data must be imported for ML training to work
if (!hasImportResults) {
missingItems.push('Datos de ventas importados');
}
@@ -152,6 +128,7 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
isValid: missingItems.length === 0,
missingItems,
hasProcessingResults,
hasImportResults,
hasApprovedProducts,
hasInventoryConfig
});
@@ -205,7 +182,11 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
try {
// Start training job
addLog('Iniciando trabajo de entrenamiento ML...', 'info');
const job = await trainingService.startTrainingJob(tenantId);
const response = await trainingService.createTrainingJob({
start_date: undefined,
end_date: undefined
});
const job = response.data;
setCurrentJob(job);
setTrainingStatus('training');

View File

@@ -227,16 +227,16 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 90) return 'text-green-600 bg-green-50';
if (confidence >= 75) return 'text-yellow-600 bg-yellow-50';
return 'text-red-600 bg-red-50';
if (confidence >= 90) return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
if (confidence >= 75) return 'text-[var(--color-warning)] bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20';
return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
};
const getStatusColor = (status: string) => {
switch (status) {
case 'approved': return 'text-green-600 bg-green-50';
case 'rejected': return 'text-red-600 bg-red-50';
default: return 'text-gray-600 bg-gray-50';
case 'approved': return 'text-[var(--color-success)] bg-[var(--color-success)]/10 border-[var(--color-success)]/20';
case 'rejected': return 'text-[var(--color-error)] bg-[var(--color-error)]/10 border-[var(--color-error)]/20';
default: return 'text-[var(--text-secondary)] bg-[var(--bg-secondary)] border-[var(--border-secondary)]';
}
};
@@ -267,35 +267,35 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
return (
<div className="space-y-8">
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<Card className="p-6 text-center">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-6 text-center bg-[var(--bg-primary)] border-[var(--border-primary)]">
<div className="text-3xl font-bold text-[var(--color-primary)] mb-2">{stats.total}</div>
<div className="text-sm text-[var(--text-secondary)]">Productos detectados</div>
<div className="text-sm font-medium text-[var(--text-secondary)]">Productos detectados</div>
</Card>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-green-600 mb-2">{stats.approved}</div>
<div className="text-sm text-[var(--text-secondary)]">Aprobados</div>
<Card className="p-6 text-center bg-[var(--color-success)]/5 border-[var(--color-success)]/20">
<div className="text-3xl font-bold text-[var(--color-success)] mb-2">{stats.approved}</div>
<div className="text-sm font-medium text-[var(--color-success)]">Aprobados</div>
</Card>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-red-600 mb-2">{stats.rejected}</div>
<div className="text-sm text-[var(--text-secondary)]">Rechazados</div>
<Card className="p-6 text-center bg-[var(--color-error)]/5 border-[var(--color-error)]/20">
<div className="text-3xl font-bold text-[var(--color-error)] mb-2">{stats.rejected}</div>
<div className="text-sm font-medium text-[var(--color-error)]">Rechazados</div>
</Card>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-yellow-600 mb-2">{stats.pending}</div>
<div className="text-sm text-[var(--text-secondary)]">Pendientes</div>
<Card className="p-6 text-center bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
<div className="text-3xl font-bold text-[var(--color-warning)] mb-2">{stats.pending}</div>
<div className="text-sm font-medium text-[var(--color-warning)]">Pendientes</div>
</Card>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg">
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center bg-[var(--bg-secondary)] p-4 rounded-lg border border-[var(--border-secondary)]">
<div className="flex items-center space-x-4">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-white"
className="px-3 py-2 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
{categories.map(category => (
<option key={category} value={category}>
@@ -304,7 +304,7 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
))}
</select>
<Badge variant="outline" className="text-sm">
<Badge variant="outline" className="text-sm font-medium px-3 py-1 bg-[var(--bg-primary)] border-[var(--border-primary)] text-[var(--text-secondary)]">
{getFilteredProducts().length} productos
</Badge>
</div>
@@ -314,7 +314,7 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-green-600 border-green-200 hover:bg-green-50"
className="text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5"
>
Aprobar todos
</Button>
@@ -322,7 +322,7 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-red-600 border-red-200 hover:bg-red-50"
className="text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5"
>
Rechazar todos
</Button>
@@ -330,48 +330,70 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
</div>
{/* Products List */}
<div className="space-y-3">
<div className="space-y-4">
{getFilteredProducts().map((product) => (
<Card key={product.id} className="p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="font-semibold text-[var(--text-primary)]">{product.name}</h3>
<Badge variant="outline" className="text-xs">
<Card key={product.id} className="p-6 hover:shadow-lg transition-all duration-200 border border-[var(--border-primary)] bg-[var(--bg-primary)]">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center flex-wrap gap-2 mb-3">
<h3 className="font-semibold text-lg text-[var(--text-primary)] mr-2">{product.name}</h3>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getStatusColor(product.status)}`}>
{product.status === 'approved' ? '✓ Aprobado' :
product.status === 'rejected' ? '✗ Rechazado' : '⏳ Pendiente'}
</Badge>
</div>
<div className="flex items-center flex-wrap gap-2 mb-3">
<Badge className="text-xs font-medium px-2 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-[var(--color-primary)]/20">
{product.category}
</Badge>
<Badge className={`text-xs ${getConfidenceColor(product.confidence)}`}>
<Badge className={`text-xs font-medium px-2 py-1 rounded-full border ${getConfidenceColor(product.confidence)}`}>
{product.confidence}% confianza
</Badge>
<Badge className={`text-xs ${getStatusColor(product.status)}`}>
{product.status === 'approved' ? 'Aprobado' :
product.status === 'rejected' ? 'Rechazado' : 'Pendiente'}
</Badge>
</div>
<div className="text-sm text-[var(--text-secondary)] space-y-1">
<div className="text-sm text-[var(--text-secondary)] space-y-2">
{product.original_name && product.original_name !== product.name && (
<div>Nombre original: <span className="font-medium">{product.original_name}</span></div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[120px]">Nombre original:</span>
<span className="font-medium text-[var(--text-primary)]">{product.original_name}</span>
</div>
)}
<div className="flex space-x-4">
<span>Tipo: {product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}</span>
<span>Unidad: {product.unit_of_measure}</span>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Tipo:</span>
<span className="font-medium text-[var(--text-primary)]">
{product.product_type === 'ingredient' ? 'Ingrediente' : 'Producto terminado'}
</span>
</div>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Unidad:</span>
<span className="font-medium text-[var(--text-primary)]">{product.unit_of_measure}</span>
</div>
{product.sales_data && (
<span>Ventas: {product.sales_data.total_quantity}</span>
<div className="flex items-center">
<span className="font-medium text-[var(--text-tertiary)] min-w-[60px]">Ventas:</span>
<span className="font-medium text-[var(--text-primary)]">{product.sales_data.total_quantity}</span>
</div>
)}
</div>
{product.notes && (
<div className="text-xs italic">Nota: {product.notes}</div>
<div className="text-xs italic bg-[var(--bg-secondary)] p-2 rounded border-l-4 border-[var(--color-primary)]/30">
<span className="font-medium text-[var(--text-tertiary)]">Nota:</span> {product.notes}
</div>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex flex-col sm:flex-row gap-2 flex-shrink-0">
<Button
size="sm"
variant={product.status === 'approved' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'approve')}
className={product.status === 'approved' ? 'bg-green-600 hover:bg-green-700' : 'text-green-600 border-green-200 hover:bg-green-50'}
className={product.status === 'approved'
? 'bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white shadow-sm'
: 'text-[var(--color-success)] border-[var(--color-success)]/30 hover:bg-[var(--color-success)]/10 bg-[var(--color-success)]/5'
}
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
@@ -380,7 +402,10 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
size="sm"
variant={product.status === 'rejected' ? 'default' : 'outline'}
onClick={() => handleProductAction(product.id, 'reject')}
className={product.status === 'rejected' ? 'bg-red-600 hover:bg-red-700' : 'text-red-600 border-red-200 hover:bg-red-50'}
className={product.status === 'rejected'
? 'bg-[var(--color-error)] hover:bg-[var(--color-error)]/90 text-white shadow-sm'
: 'text-[var(--color-error)] border-[var(--color-error)]/30 hover:bg-[var(--color-error)]/10 bg-[var(--color-error)]/5'
}
>
<AlertCircle className="w-4 h-4 mr-1" />
Rechazar
@@ -391,23 +416,21 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
))}
</div>
{/* Navigation */}
<div className="flex justify-between pt-6 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Anterior
</Button>
<Button
onClick={onNext}
disabled={!data.reviewCompleted || stats.approved === 0}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
>
{isLastStep ? 'Finalizar' : 'Siguiente'}
</Button>
</div>
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
📋 Revisión de Productos Detectados:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Revise cuidadosamente</strong> - Los productos fueron detectados automáticamente desde sus datos de ventas</li>
<li> <strong>Apruebe o rechace</strong> cada producto según sea correcto para su negocio</li>
<li> <strong>Verifique nombres</strong> - Compare el nombre original vs. el nombre sugerido</li>
<li> <strong>Revise clasificaciones</strong> - Confirme si son ingredientes o productos terminados</li>
<li> <strong>Use filtros</strong> - Filtre por categoría para revisar productos similares</li>
<li> <strong>Acciones masivas</strong> - Use "Aprobar todos" o "Rechazar todos" para agilizar el proceso</li>
</ul>
</Card>
</div>
);
};

View File

@@ -2,10 +2,12 @@ import React, { useState, useEffect } from 'react';
import { Truck, Phone, Mail, Plus, Edit, Trash2, MapPin, AlertCircle, Loader } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { procurementService, type Supplier } from '../../../../services/api/procurement.service';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
import { procurementService } from '../../../../services/api/procurement.service';
// Frontend supplier interface that matches the form needs
interface SupplierFormData {
@@ -46,8 +48,13 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { createAlert } = useAlertActions();
const { showToast } = useToast();
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
// Use modals for confirmations and editing
const deleteModal = useModal();
const editModal = useModal();
const [suppliers, setSuppliers] = useState<any[]>([]);
const [editingSupplier, setEditingSupplier] = useState<SupplierFormData | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all');
@@ -207,9 +214,17 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
};
const handleDeleteSupplier = async (id: string) => {
if (!window.confirm('¿Estás seguro de eliminar este proveedor? Esta acción no se puede deshacer.')) {
return;
}
deleteModal.openModal({
title: 'Confirmar eliminación',
message: '¿Estás seguro de eliminar este proveedor? Esta acción no se puede deshacer.',
onConfirm: () => performDelete(id),
onCancel: () => deleteModal.closeModal()
});
return;
};
const performDelete = async (id: string) => {
deleteModal.closeModal();
setDeleting(id);
try {
@@ -316,21 +331,6 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
{stats.total} proveedores configurados ({stats.active} activos)
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
onClick={handleAddSupplier}
disabled={creating}
>
{creating ? (
<Loader className="w-4 h-4 mr-2 animate-spin" />
) : (
<Plus className="w-4 h-4 mr-2" />
)}
Agregar Proveedor
</Button>
</div>
</div>
</Card>
@@ -533,20 +533,53 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
))}
{getFilteredSuppliers().length === 0 && !loading && (
<Card className="p-8 text-center">
<Truck className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)] mb-4">
<Card className="p-8 text-center bg-[var(--bg-primary)] border-2 border-dashed border-[var(--border-secondary)]">
<Truck className="w-16 h-16 text-[var(--color-primary)] mx-auto mb-4" />
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-2">
{filterStatus === 'all'
? 'No hay proveedores registrados'
? 'Comienza agregando tu primer proveedor'
: `No hay proveedores ${filterStatus === 'active' ? 'activos' : 'inactivos'}`
}
</h3>
<p className="text-[var(--text-secondary)] mb-6 max-w-md mx-auto">
{filterStatus === 'all'
? 'Los proveedores te ayudarán a gestionar tus compras y mantener un control de calidad en tu panadería.'
: 'Ajusta los filtros para ver otros proveedores o agrega uno nuevo.'
}
</p>
<Button onClick={handleAddSupplier} disabled={creating}>
<Plus className="w-4 h-4 mr-2" />
Agregar Primer Proveedor
<Button
onClick={handleAddSupplier}
disabled={creating}
size="lg"
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white px-8 py-3 text-base font-medium shadow-lg"
>
{creating ? (
<Loader className="w-5 h-5 mr-2 animate-spin" />
) : (
<Plus className="w-5 h-5 mr-2" />
)}
{filterStatus === 'all' ? 'Agregar Primer Proveedor' : 'Agregar Proveedor'}
</Button>
</Card>
)}
{/* Floating Action Button for when suppliers exist */}
{suppliers.length > 0 && !editingSupplier && (
<div className="fixed bottom-8 right-8 z-40">
<Button
onClick={handleAddSupplier}
disabled={creating}
className="w-14 h-14 rounded-full bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white shadow-2xl hover:shadow-3xl transition-all duration-200 hover:scale-105"
title="Agregar nuevo proveedor"
>
{creating ? (
<Loader className="w-6 h-6 animate-spin" />
) : (
<Plus className="w-6 h-6" />
)}
</Button>
</div>
)}
</div>
{/* Edit Modal */}
@@ -571,18 +604,20 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
</div>
)}
{/* Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
🚚 Gestión de Proveedores:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Este paso es opcional</strong> - puedes configurar proveedores más tarde</li>
<li> Define categorías de productos para facilitar la búsqueda</li>
<li> Establece días de entrega y términos de pago</li>
<li> Configura montos mínimos de pedido para optimizar compras</li>
</ul>
</Card>
{/* Information - Only show when there are suppliers */}
{suppliers.length > 0 && (
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-2">
🚚 Gestión de Proveedores:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Editar información</strong> - Actualiza datos de contacto y términos comerciales</li>
<li> <strong>Gestionar estado</strong> - Activa o pausa proveedores según necesidades</li>
<li> <strong>Revisar rendimiento</strong> - Evalúa entregas a tiempo y calidad de productos</li>
<li> <strong>Filtrar vista</strong> - Usa los filtros para encontrar proveedores específicos</li>
</ul>
</Card>
)}
</div>
);
@@ -610,11 +645,19 @@ const SupplierForm: React.FC<SupplierFormProps> = ({
e.preventDefault();
if (!formData.name.trim()) {
alert('El nombre de la empresa es requerido');
showToast({
title: 'Error de validación',
message: 'El nombre de la empresa es requerido',
type: 'error'
});
return;
}
if (!formData.address.trim()) {
alert('La dirección es requerida');
showToast({
title: 'Error de validación',
message: 'La dirección es requerida',
type: 'error'
});
return;
}