Start integrating the onboarding flow with backend 4
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user