Start integrating the onboarding flow with backend 2

This commit is contained in:
Urtzi Alfaro
2025-09-04 18:59:56 +02:00
parent a11fdfba24
commit 9eedc2e5f2
30 changed files with 3432 additions and 4735 deletions

View File

@@ -46,10 +46,15 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
const currentStep = steps[currentStepIndex];
const updateStepData = useCallback((stepId: string, data: any) => {
setStepData(prev => ({
...prev,
[stepId]: { ...prev[stepId], ...data }
}));
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;
});
// Clear validation error for this step
setValidationErrors(prev => {
@@ -414,7 +419,10 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
// Pass all step data to allow access to previous steps
allStepData: stepData
}}
onDataChange={(data) => updateStepData(currentStep.id, data)}
onDataChange={(data) => {
console.log(`OnboardingWizard - Step ${currentStep.id} calling onDataChange with:`, data);
updateStepData(currentStep.id, data);
}}
onNext={goToNextStep}
onPrevious={goToPreviousStep}
isFirstStep={currentStepIndex === 0}

View File

@@ -2,6 +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 { useAuthUser } from '../../../../stores/auth.store';
import { useAlertActions } from '../../../../stores/alerts.store';
interface CompletionStats {
totalProducts: number;
@@ -10,6 +13,8 @@ interface CompletionStats {
mlModelAccuracy: number;
estimatedTimeSaved: string;
completionScore: number;
salesImported: boolean;
salesImportRecords: number;
}
export const CompletionStep: React.FC<OnboardingStepProps> = ({
@@ -20,8 +25,73 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
isFirstStep,
isLastStep
}) => {
const user = useAuthUser();
const { createAlert } = useAlertActions();
const [showConfetti, setShowConfetti] = useState(false);
const [completionStats, setCompletionStats] = useState<CompletionStats | null>(null);
const [isImportingSales, setIsImportingSales] = useState(false);
// Handle final sales import
const handleFinalSalesImport = async () => {
if (!user?.tenant_id || !data.files?.salesData || !data.inventoryMapping) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error en importación final',
message: 'Faltan datos necesarios para importar las ventas.',
source: 'onboarding'
});
return;
}
setIsImportingSales(true);
try {
const result = await onboardingApiService.importSalesWithInventory(
user.tenant_id,
data.files.salesData,
data.inventoryMapping
);
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Importación completada',
message: `Se importaron ${result.successful_imports} registros de ventas exitosamente.`,
source: 'onboarding'
});
// Update completion stats
const updatedStats = {
...completionStats!,
salesImported: true,
salesImportRecords: result.successful_imports || 0
};
setCompletionStats(updatedStats);
onDataChange({
...data,
completionStats: updatedStats,
salesImportResult: result,
finalImportCompleted: true
});
} catch (error) {
console.error('Sales import error:', error);
const errorMessage = error instanceof Error ? error.message : 'Error al importar datos de ventas';
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error en importación',
message: errorMessage,
source: 'onboarding'
});
} finally {
setIsImportingSales(false);
}
};
useEffect(() => {
// Show confetti animation
@@ -35,7 +105,9 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
suppliersConfigured: data.suppliers?.length || 0,
mlModelAccuracy: data.trainingMetrics?.accuracy * 100 || 0,
estimatedTimeSaved: '15-20 horas',
completionScore: calculateCompletionScore()
completionScore: calculateCompletionScore(),
salesImported: data.finalImportCompleted || false,
salesImportRecords: data.salesImportResult?.successful_imports || 0
};
setCompletionStats(stats);
@@ -48,6 +120,11 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
completedAt: new Date().toISOString()
});
// Trigger final sales import if not already done
if (!data.finalImportCompleted && data.inventoryMapping && data.files?.salesData) {
handleFinalSalesImport();
}
return () => clearTimeout(timer);
}, []);
@@ -221,6 +298,10 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
<p className="text-2xl font-bold text-[var(--color-secondary)]">{completionStats.mlModelAccuracy.toFixed(1)}%</p>
<p className="text-xs text-[var(--text-secondary)]">Precisión IA</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">{completionStats.salesImported ? completionStats.salesImportRecords : '⏳'}</p>
<p className="text-xs text-[var(--text-secondary)]">Ventas {completionStats.salesImported ? 'Importadas' : 'Importando...'}</p>
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-lg font-bold text-[var(--color-warning)]">{completionStats.estimatedTimeSaved}</p>
<p className="text-xs text-[var(--text-secondary)]">Tiempo Ahorrado</p>

View File

@@ -2,6 +2,10 @@ 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 { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
import { useCurrentTenant, useTenantLoading } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
type ProcessingStage = 'upload' | 'validating' | 'analyzing' | 'completed' | 'error';
@@ -26,64 +30,70 @@ interface ProcessingResult {
recommendations: string[];
}
// Unified mock service that handles both validation and analysis
const mockDataProcessingService = {
processFile: async (file: File, onProgress: (progress: number, stage: string, message: string) => void) => {
return new Promise<ProcessingResult>((resolve, reject) => {
let progress = 0;
// 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);
const stages = [
{ threshold: 20, stage: 'validating', message: 'Validando estructura del archivo...' },
{ threshold: 40, stage: 'validating', message: 'Verificando integridad de datos...' },
{ threshold: 60, stage: 'analyzing', message: 'Identificando productos únicos...' },
{ threshold: 80, stage: 'analyzing', message: 'Analizando patrones de venta...' },
{ threshold: 90, stage: 'analyzing', message: 'Generando recomendaciones con IA...' },
{ threshold: 100, stage: 'completed', message: 'Procesamiento completado' }
];
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');
}
const interval = setInterval(() => {
if (progress < 100) {
progress += 10;
const currentStage = stages.find(s => progress <= s.threshold);
if (currentStage) {
onProgress(progress, currentStage.stage, currentStage.message);
}
}
if (progress >= 100) {
clearInterval(interval);
// Return combined validation + analysis results
resolve({
// Validation results
is_valid: true,
total_records: Math.floor(Math.random() * 1000) + 100,
unique_products: Math.floor(Math.random() * 50) + 10,
product_list: ['Pan Integral', 'Croissant', 'Baguette', 'Empanadas', 'Pan de Centeno', 'Medialunas'],
validation_errors: [],
validation_warnings: [
'Algunas fechas podrían tener formato inconsistente',
'3 productos sin categoría definida'
],
summary: {
date_range: '2024-01-01 to 2024-12-31',
total_sales: 15420.50,
average_daily_sales: 42.25
},
// Analysis results
productsIdentified: 15,
categoriesDetected: 4,
businessModel: 'artisan',
confidenceScore: 94,
recommendations: [
'Se detectó un modelo de panadería artesanal con producción propia',
'Los productos más vendidos son panes tradicionales y bollería',
'Recomendamos categorizar el inventario por tipo de producto',
'Considera ampliar la línea de productos de repostería'
]
});
}
}, 400);
});
// 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;
}
}
};
@@ -95,6 +105,34 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
isFirstStep,
isLastStep
}) => {
const user = useAuthUser();
const authLoading = useAuthLoading();
const currentTenant = useCurrentTenant();
const tenantLoading = useTenantLoading();
const { createAlert } = useAlertActions();
// Check if we're still loading user or tenant data
const isLoadingUserData = authLoading || tenantLoading;
// Get tenant ID from multiple sources with fallback
const getTenantId = (): string | null => {
const tenantId = currentTenant?.id || user?.tenant_id || null;
console.log('DataProcessingStep - getTenantId:', {
currentTenant: currentTenant?.id,
userTenantId: user?.tenant_id,
finalTenantId: tenantId,
isLoadingUserData,
authLoading,
tenantLoading,
user: user ? { id: user.id, email: user.email } : null
});
return tenantId;
};
// Check if tenant data is available (not loading and has ID)
const isTenantAvailable = (): boolean => {
return !isLoadingUserData && getTenantId() !== null;
};
const [stage, setStage] = useState<ProcessingStage>(data.processingStage || 'upload');
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
const [progress, setProgress] = useState(data.processingProgress || 0);
@@ -166,8 +204,41 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
setProgress(0);
try {
const result = await mockDataProcessingService.processFile(
// Wait for user data to load if still loading
if (!isTenantAvailable()) {
createAlert({
type: 'info',
category: 'system',
priority: 'low',
title: 'Cargando datos de usuario',
message: 'Por favor espere mientras cargamos su información...',
source: 'onboarding'
});
// Reset file state since we can't process it yet
setUploadedFile(null);
setStage('upload');
return;
}
const tenantId = getTenantId();
if (!tenantId) {
console.error('DataProcessingStep - No tenant ID available:', {
user,
currentTenant,
userTenantId: user?.tenant_id,
currentTenantId: currentTenant?.id,
isLoadingUserData,
authLoading,
tenantLoading
});
throw new Error('No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.');
}
console.log('DataProcessingStep - Starting file processing with tenant:', tenantId);
const result = await dataProcessingService.processFile(
file,
tenantId,
(newProgress, newStage, message) => {
setProgress(newProgress);
setStage(newStage as ProcessingStage);
@@ -177,32 +248,112 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
setResults(result);
setStage('completed');
// Store results for next steps
onDataChange({
...data,
files: { ...data.files, salesData: file },
processingResults: result,
processingStage: 'completed',
processingProgress: 100
});
console.log('DataProcessingStep - File processing completed:', result);
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Procesamiento completado',
message: `Se procesaron ${result.total_records} registros y se identificaron ${result.unique_products} productos únicos.`,
source: 'onboarding'
});
} catch (error) {
console.error('Processing error:', error);
console.error('DataProcessingStep - Processing error:', error);
console.error('DataProcessingStep - Error details:', {
errorMessage: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : null,
tenantInfo: {
user: user ? { id: user.id, tenant_id: user.tenant_id } : null,
currentTenant: currentTenant ? { id: currentTenant.id } : null
}
});
setStage('error');
setCurrentMessage('Error en el procesamiento de datos');
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
setCurrentMessage(errorMessage);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error en el procesamiento',
message: errorMessage,
source: 'onboarding'
});
}
};
const downloadTemplate = () => {
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
const downloadTemplate = async () => {
try {
if (!isTenantAvailable()) {
createAlert({
type: 'info',
category: 'system',
priority: 'low',
title: 'Cargando datos de usuario',
message: 'Por favor espere mientras cargamos su información...',
source: 'onboarding'
});
return;
}
const tenantId = getTenantId();
if (!tenantId) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: 'No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.',
source: 'onboarding'
});
return;
}
const templateData = await onboardingApiService.getSalesImportTemplate(tenantId, 'csv');
onboardingApiService.downloadTemplate(templateData, 'plantilla_ventas.csv', 'csv');
createAlert({
type: 'success',
category: 'system',
priority: 'low',
title: 'Plantilla descargada',
message: 'La plantilla de ventas se ha descargado correctamente',
source: 'onboarding'
});
} catch (error) {
console.error('Error downloading template:', error);
// Fallback to static template
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'plantilla_ventas.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'plantilla_ventas.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
const resetProcess = () => {
@@ -218,8 +369,23 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
return (
<div className="space-y-8">
{/* Loading state when tenant data is not available */}
{!isTenantAvailable() && (
<Card className="p-8 text-center">
<div className="w-16 h-16 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Activity className="w-8 h-8 text-[var(--color-info)] animate-pulse" />
</div>
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-3">
Cargando datos de usuario...
</h3>
<p className="text-[var(--text-secondary)]">
Por favor espere mientras cargamos su información de tenant
</p>
</Card>
)}
{/* Improved Upload Stage */}
{stage === 'upload' && (
{stage === 'upload' && isTenantAvailable() && (
<>
<div
className={`

View File

@@ -1,7 +1,10 @@
import React, { useState, useEffect } from 'react';
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2 } from 'lucide-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 { useAuthUser } from '../../../../stores/auth.store';
import { useAlertActions } from '../../../../stores/alerts.store';
interface InventoryItem {
id: string;
@@ -15,34 +18,35 @@ interface InventoryItem {
supplier?: string;
cost_per_unit?: number;
requires_refrigeration: boolean;
// API fields
suggestion_id?: string;
original_name?: string;
estimated_shelf_life_days?: number;
is_seasonal?: boolean;
}
// Mock inventory items based on approved products
const mockInventoryItems: InventoryItem[] = [
{
id: '1', name: 'Harina de Trigo', category: 'ingredient',
current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg',
expiry_date: '2024-12-31', supplier: 'Molinos del Sur',
cost_per_unit: 1.20, requires_refrigeration: false
},
{
id: '2', name: 'Levadura Fresca', category: 'ingredient',
current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg',
expiry_date: '2024-03-15', supplier: 'Levaduras Pro',
cost_per_unit: 3.50, requires_refrigeration: true
},
{
id: '3', name: 'Pan Integral', category: 'finished_product',
current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades',
expiry_date: '2024-01-25', requires_refrigeration: false
},
{
id: '4', name: 'Mantequilla', category: 'ingredient',
current_stock: 15, min_stock: 5, max_stock: 30, unit: 'kg',
expiry_date: '2024-02-28', supplier: 'Lácteos Premium',
cost_per_unit: 4.20, requires_refrigeration: true
}
];
// Convert approved products to inventory items
const convertProductsToInventory = (approvedProducts: any[]): InventoryItem[] => {
return approvedProducts.map((product, index) => ({
id: `inventory-${index}`,
name: product.suggested_name || product.name,
category: product.product_type || 'finished_product',
current_stock: 0, // To be configured by user
min_stock: 1, // Default minimum
max_stock: 100, // Default maximum
unit: product.unit_of_measure || 'unidad',
requires_refrigeration: product.requires_refrigeration || false,
// Store API data
suggestion_id: product.suggestion_id,
original_name: product.original_name,
estimated_shelf_life_days: product.estimated_shelf_life_days,
is_seasonal: product.is_seasonal,
// Optional fields to be filled by user
expiry_date: undefined,
supplier: product.suggested_supplier,
cost_per_unit: 0
}));
};
export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
data,
@@ -52,22 +56,121 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
isFirstStep,
isLastStep
}) => {
const [items, setItems] = useState<InventoryItem[]>(
data.inventoryItems || mockInventoryItems
);
const user = useAuthUser();
const { createAlert } = useAlertActions();
const [isCreating, setIsCreating] = useState(false);
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
// Generate inventory items from approved products
const generateInventoryFromProducts = (approvedProducts: any[]): InventoryItem[] => {
if (!approvedProducts || approvedProducts.length === 0) {
createAlert({
type: 'warning',
category: 'system',
priority: 'medium',
title: 'Sin productos aprobados',
message: 'No hay productos aprobados para crear inventario. Regrese al paso anterior.',
source: 'onboarding'
});
return [];
}
return convertProductsToInventory(approvedProducts);
};
const [items, setItems] = useState<InventoryItem[]>(() => {
if (data.inventoryItems) {
return data.inventoryItems;
}
// Try to get approved products from current step data first, then from review step data
const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts;
return generateInventoryFromProducts(approvedProducts || []);
});
// Update items when approved products become available (for when component is already mounted)
useEffect(() => {
const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts;
if (approvedProducts && approvedProducts.length > 0 && items.length === 0) {
const newItems = generateInventoryFromProducts(approvedProducts);
setItems(newItems);
}
}, [data.approvedProducts, data.allStepData]);
const [filterCategory, setFilterCategory] = useState<'all' | 'ingredient' | 'finished_product'>('all');
const filteredItems = filterCategory === 'all'
? items
: items.filter(item => item.category === filterCategory);
// Create inventory items via API
const handleCreateInventory = async () => {
const approvedProducts = data.approvedProducts || data.allStepData?.['review']?.approvedProducts;
if (!user?.tenant_id || !approvedProducts || approvedProducts.length === 0) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: 'No se pueden crear elementos de inventario sin productos aprobados.',
source: 'onboarding'
});
return;
}
setIsCreating(true);
try {
const result = await onboardingApiService.createInventoryFromSuggestions(
user.tenant_id,
approvedProducts
);
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Inventario creado',
message: `Se crearon ${result.created_items.length} elementos de inventario exitosamente.`,
source: 'onboarding'
});
// Update the step data with created inventory
onDataChange({
...data,
inventoryItems: items,
inventoryConfigured: true,
inventoryMapping: result.inventory_mapping,
createdInventoryItems: result.created_items
});
} catch (error) {
console.error('Error creating inventory:', error);
const errorMessage = error instanceof Error ? error.message : 'Error al crear inventario';
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al crear inventario',
message: errorMessage,
source: 'onboarding'
});
} finally {
setIsCreating(false);
}
};
useEffect(() => {
const hasValidStock = items.length > 0 && items.every(item =>
item.min_stock >= 0 && item.max_stock > item.min_stock
);
onDataChange({
...data,
inventoryItems: items,
inventoryConfigured: items.length > 0 && items.every(item =>
item.min_stock > 0 && item.max_stock > item.min_stock
)
inventoryConfigured: hasValidStock && !isCreating
});
}, [items]);
}, [items, isCreating]);
const handleAddItem = () => {
const newItem: InventoryItem = {
@@ -75,491 +178,370 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
name: '',
category: 'ingredient',
current_stock: 0,
min_stock: 0,
max_stock: 0,
unit: 'kg',
min_stock: 1,
max_stock: 10,
unit: 'unidad',
requires_refrigeration: false
};
setItems([...items, newItem]);
setEditingItem(newItem);
setIsAddingNew(true);
};
const handleSaveItem = (item: InventoryItem) => {
if (isAddingNew) {
setItems(prev => [...prev, item]);
} else {
setItems(prev => prev.map(i => i.id === item.id ? item : i));
}
const handleSaveItem = (updatedItem: InventoryItem) => {
setItems(items.map(item =>
item.id === updatedItem.id ? updatedItem : item
));
setEditingItem(null);
setIsAddingNew(false);
};
const handleDeleteItem = (id: string) => {
if (window.confirm('¿Estás seguro de eliminar este elemento del inventario?')) {
setItems(prev => prev.filter(item => item.id !== id));
setItems(items.filter(item => item.id !== id));
if (editingItem?.id === id) {
setEditingItem(null);
setIsAddingNew(false);
}
};
const handleQuickSetup = () => {
// Auto-configure basic inventory based on approved products
const autoItems = data.detectedProducts
?.filter((p: any) => p.status === 'approved')
.map((product: any, index: number) => ({
id: `auto_${index}`,
name: product.name,
category: 'finished_product' as const,
current_stock: Math.floor(Math.random() * 20) + 5,
min_stock: 5,
max_stock: 50,
unit: 'unidades',
requires_refrigeration: product.category === 'Repostería' || product.category === 'Salados'
})) || [];
setItems(prev => [...prev, ...autoItems]);
};
const getFilteredItems = () => {
return filterCategory === 'all'
? items
: items.filter(item => item.category === filterCategory);
const handleCancelEdit = () => {
if (isAddingNew && editingItem) {
setItems(items.filter(item => item.id !== editingItem.id));
}
setEditingItem(null);
setIsAddingNew(false);
};
const getStockStatus = (item: InventoryItem) => {
if (item.current_stock <= item.min_stock) return { status: 'low', color: 'red', text: 'Stock Bajo' };
if (item.current_stock >= item.max_stock) return { status: 'high', color: 'blue', text: 'Stock Alto' };
return { status: 'normal', color: 'green', text: 'Normal' };
if (item.current_stock <= item.min_stock) return 'critical';
if (item.current_stock <= item.min_stock * 1.5) return 'warning';
return 'good';
};
const isNearExpiry = (expiryDate?: string) => {
if (!expiryDate) return false;
const expiry = new Date(expiryDate);
const today = new Date();
const diffDays = (expiry.getTime() - today.getTime()) / (1000 * 3600 * 24);
return diffDays <= 7;
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';
}
};
const stats = {
total: items.length,
ingredients: items.filter(i => i.category === 'ingredient').length,
products: items.filter(i => i.category === 'finished_product').length,
lowStock: items.filter(i => i.current_stock <= i.min_stock).length,
nearExpiry: items.filter(i => isNearExpiry(i.expiry_date)).length,
refrigerated: items.filter(i => i.requires_refrigeration).length
};
if (items.length === 0) {
return (
<div className="space-y-8">
<div className="text-center py-16">
<Package className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-gray-600 mb-2">
Sin productos para inventario
</h3>
<p className="text-gray-500 mb-6 max-w-md mx-auto">
No hay productos aprobados para crear inventario.
Regrese al paso anterior para aprobar productos.
</p>
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Volver al paso anterior
</Button>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Quick Actions */}
<Card className="p-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<p className="text-sm text-[var(--text-secondary)]">
{stats.total} elementos configurados
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleQuickSetup}
className="text-[var(--color-info)]"
>
Auto-configurar Productos
</Button>
<Button
size="sm"
onClick={handleAddItem}
>
<Plus className="w-4 h-4 mr-2" />
Agregar Elemento
</Button>
</div>
</div>
</Card>
<div className="space-y-8">
{/* Header */}
<div className="text-center">
<h2 className="text-3xl font-bold text-[var(--text-primary)] mb-4">
Configuración de Inventario
</h2>
<p className="text-[var(--text-secondary)] text-lg max-w-2xl mx-auto">
Configure los niveles de stock, fechas de vencimiento y otros detalles para sus productos.
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
<p className="text-xs text-[var(--text-secondary)]">Total</p>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="p-6 text-center">
<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>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.ingredients}</p>
<p className="text-xs text-[var(--text-secondary)]">Ingredientes</p>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-blue-600 mb-2">
{items.filter(item => item.category === 'ingredient').length}
</div>
<div className="text-sm text-[var(--text-secondary)]">Ingredientes</div>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.products}</p>
<p className="text-xs text-[var(--text-secondary)]">Productos</p>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-green-600 mb-2">
{items.filter(item => item.category === 'finished_product').length}
</div>
<div className="text-sm text-[var(--text-secondary)]">Productos terminados</div>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-error)]">{stats.lowStock}</p>
<p className="text-xs text-[var(--text-secondary)]">Stock Bajo</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.nearExpiry}</p>
<p className="text-xs text-[var(--text-secondary)]">Por Vencer</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.refrigerated}</p>
<p className="text-xs text-[var(--text-secondary)]">Refrigerado</p>
<Card className="p-6 text-center">
<div className="text-3xl font-bold text-red-600 mb-2">
{items.filter(item => getStockStatus(item) === 'critical').length}
</div>
<div className="text-sm text-[var(--text-secondary)]">Stock crítico</div>
</Card>
</div>
{/* Filters */}
<Card className="p-4">
{/* 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 items-center space-x-4">
<label className="text-sm font-medium text-[var(--text-secondary)]">Filtrar:</label>
<div className="flex space-x-2">
{[
{ value: 'all', label: 'Todos' },
{ value: 'ingredient', label: 'Ingredientes' },
{ value: 'finished_product', label: 'Productos' }
].map(filter => (
<button
key={filter.value}
onClick={() => setFilterCategory(filter.value as any)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filterCategory === filter.value
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
}`}
>
{filter.label}
</button>
))}
</div>
</div>
</Card>
{/* Inventory Items */}
<div className="space-y-4">
{getFilteredItems().map((item) => {
const stockStatus = getStockStatus(item);
const nearExpiry = isNearExpiry(item.expiry_date);
<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"
>
<option value="all">Todos los elementos</option>
<option value="ingredient">Ingredientes</option>
<option value="finished_product">Productos terminados</option>
</select>
return (
<Card key={item.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Category Icon */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
item.category === 'ingredient'
? 'bg-[var(--color-primary)]/10'
: 'bg-[var(--color-success)]/10'
}`}>
<Package className={`w-4 h-4 ${
item.category === 'ingredient'
? 'text-[var(--color-primary)]'
: 'text-[var(--color-success)]'
}`} />
</div>
<Badge variant="outline" className="text-sm">
{filteredItems.length} elementos
</Badge>
</div>
<div className="flex space-x-2">
<Button
onClick={handleAddItem}
size="sm"
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
>
<Plus className="w-4 h-4 mr-1" />
Agregar elemento
</Button>
<Button
onClick={handleCreateInventory}
disabled={isCreating || items.length === 0 || data.inventoryConfigured}
size="sm"
variant="outline"
className="border-green-200 text-green-600 hover:bg-green-50"
>
<CheckCircle className="w-4 h-4 mr-1" />
{isCreating ? 'Creando...' : data.inventoryConfigured ? 'Inventario creado' : 'Crear inventario'}
</Button>
</div>
</div>
{/* Item Info */}
<div className="flex-1">
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{item.name}</h4>
<div className="flex items-center gap-2 mb-3">
<Badge variant={item.category === 'ingredient' ? 'blue' : 'green'}>
{item.category === 'ingredient' ? 'Ingrediente' : 'Producto'}
{/* Items List */}
<div className="space-y-3">
{filteredItems.map((item) => (
<Card key={item.id} className="p-4 hover:shadow-md transition-shadow">
{editingItem?.id === item.id ? (
<InventoryItemEditor
item={item}
onSave={handleSaveItem}
onCancel={handleCancelEdit}
/>
) : (
<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">
{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>
{item.requires_refrigeration && (
<Badge variant="gray"> Refrigeración</Badge>
)}
<Badge variant={stockStatus.color}>
{stockStatus.text}
</Badge>
{nearExpiry && (
<Badge variant="red">Vence Pronto</Badge>
)}
</div>
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)]">
<div>
<span className="text-[var(--text-tertiary)]">Stock Actual: </span>
<span className={`font-medium ${stockStatus.status === 'low' ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
{item.current_stock} {item.unit}
</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Rango: </span>
<span className="font-medium text-[var(--text-primary)]">
{item.min_stock} - {item.max_stock} {item.unit}
</span>
</div>
{item.expiry_date && (
<div>
<span className="text-[var(--text-tertiary)]">Vencimiento: </span>
<span className={`font-medium ${nearExpiry ? 'text-[var(--color-error)]' : 'text-[var(--text-primary)]'}`}>
{new Date(item.expiry_date).toLocaleDateString()}
</span>
</div>
)}
{item.cost_per_unit && (
<div>
<span className="text-[var(--text-tertiary)]">Costo/Unidad: </span>
<span className="font-medium text-[var(--text-primary)]">
${item.cost_per_unit.toFixed(2)}
</span>
</div>
)}
)}
</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>
{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>
)}
</div>
</div>
{/* Actions */}
<div className="flex gap-2 ml-4 mt-1">
<Button
size="sm"
<div className="flex items-center space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => setEditingItem(item)}
>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
<Button
size="sm"
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteItem(item.id)}
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
className="text-red-600 border-red-200 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
<Trash2 className="w-4 h-4 mr-1" />
Eliminar
</Button>
</div>
</div>
</Card>
);
})}
{getFilteredItems().length === 0 && (
<Card className="p-8 text-center">
<Package className="w-12 h-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">No hay elementos en esta categoría</p>
)}
</Card>
)}
))}
</div>
{/* Warnings */}
{(stats.lowStock > 0 || stats.nearExpiry > 0) && (
<Card className="p-4 bg-[var(--color-warning-50)] border-[var(--color-warning-200)]">
<div className="flex items-start space-x-3">
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)] flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-medium text-[var(--color-warning-800)] mb-1">Advertencias de Inventario</h4>
{stats.lowStock > 0 && (
<p className="text-sm text-[var(--color-warning-700)] mb-1">
{stats.lowStock} elemento(s) con stock bajo
</p>
)}
{stats.nearExpiry > 0 && (
<p className="text-sm text-[var(--color-warning-700)]">
{stats.nearExpiry} elemento(s) próximos a vencer
</p>
)}
</div>
</div>
</Card>
)}
{/* Edit Modal */}
{editingItem && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
{isAddingNew ? 'Agregar Elemento' : 'Editar Elemento'}
</h3>
<InventoryItemForm
item={editingItem}
onSave={handleSaveItem}
onCancel={() => {
setEditingItem(null);
setIsAddingNew(false);
}}
/>
</Card>
</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>Stock Mínimo:</strong> Nivel que dispara alertas de reabastecimiento</li>
<li> <strong>Stock Máximo:</strong> Capacidad máxima de almacenamiento</li>
<li> <strong>Fechas de Vencimiento:</strong> Control automático de productos perecederos</li>
<li> <strong>Refrigeración:</strong> Identifica productos que requieren frío</li>
</ul>
</Card>
{/* 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>
</div>
);
};
// Component for editing inventory items
interface InventoryItemFormProps {
// Inventory Item Editor Component
const InventoryItemEditor: React.FC<{
item: InventoryItem;
onSave: (item: InventoryItem) => void;
onCancel: () => void;
}
}> = ({ item, onSave, onCancel }) => {
const [editedItem, setEditedItem] = useState<InventoryItem>(item);
const InventoryItemForm: React.FC<InventoryItemFormProps> = ({ item, onSave, onCancel }) => {
const [formData, setFormData] = useState(item);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
const handleSave = () => {
if (!editedItem.name.trim()) {
alert('El nombre es requerido');
return;
}
if (formData.min_stock >= formData.max_stock) {
alert('El stock máximo debe ser mayor al mínimo');
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)');
return;
}
onSave(formData);
onSave(editedItem);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Nombre *
</label>
<Input
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Nombre del producto/ingrediente"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-4 p-4 bg-gray-50 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Categoría *
</label>
<select
value={formData.category}
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as any }))}
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
<label className="block text-sm font-medium mb-1">Nombre</label>
<Input
value={editedItem.name}
onChange={(e) => setEditedItem({ ...editedItem, name: e.target.value })}
placeholder="Nombre del producto"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">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"
>
<option value="ingredient">Ingrediente</option>
<option value="finished_product">Producto Terminado</option>
<option value="finished_product">Producto terminado</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Unidad *
</label>
<Input
value={formData.unit}
onChange={(e) => setFormData(prev => ({ ...prev, unit: e.target.value }))}
placeholder="kg, unidades, litros..."
/>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Stock Actual
</label>
<label className="block text-sm font-medium mb-1">Stock actual</label>
<Input
type="number"
value={formData.current_stock}
onChange={(e) => setFormData(prev => ({ ...prev, current_stock: Number(e.target.value) }))}
value={editedItem.current_stock}
onChange={(e) => setEditedItem({ ...editedItem, current_stock: Number(e.target.value) })}
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Stock Mín. *
</label>
<label className="block text-sm font-medium mb-1">Unidad</label>
<Input
value={editedItem.unit}
onChange={(e) => setEditedItem({ ...editedItem, unit: e.target.value })}
placeholder="kg, litros, unidades..."
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Stock mínimo</label>
<Input
type="number"
value={formData.min_stock}
onChange={(e) => setFormData(prev => ({ ...prev, min_stock: Number(e.target.value) }))}
value={editedItem.min_stock}
onChange={(e) => setEditedItem({ ...editedItem, min_stock: Number(e.target.value) })}
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Stock Máx. *
</label>
<label className="block text-sm font-medium mb-1">Stock máximo</label>
<Input
type="number"
value={formData.max_stock}
onChange={(e) => setFormData(prev => ({ ...prev, max_stock: Number(e.target.value) }))}
value={editedItem.max_stock}
onChange={(e) => setEditedItem({ ...editedItem, max_stock: Number(e.target.value) })}
min="1"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Fecha Vencimiento
</label>
<label className="block text-sm font-medium mb-1">Fecha de vencimiento</label>
<Input
type="date"
value={formData.expiry_date || ''}
onChange={(e) => setFormData(prev => ({ ...prev, expiry_date: e.target.value }))}
value={editedItem.expiry_date || ''}
onChange={(e) => setEditedItem({ ...editedItem, expiry_date: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Costo por Unidad
</label>
<label className="block text-sm font-medium mb-1">Proveedor</label>
<Input
type="number"
step="0.01"
value={formData.cost_per_unit || ''}
onChange={(e) => setFormData(prev => ({ ...prev, cost_per_unit: Number(e.target.value) }))}
min="0"
value={editedItem.supplier || ''}
onChange={(e) => setEditedItem({ ...editedItem, supplier: e.target.value })}
placeholder="Nombre del proveedor"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Proveedor
</label>
<Input
value={formData.supplier || ''}
onChange={(e) => setFormData(prev => ({ ...prev, supplier: e.target.value }))}
placeholder="Nombre del proveedor"
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="refrigeration"
checked={formData.requires_refrigeration}
onChange={(e) => setFormData(prev => ({ ...prev, requires_refrigeration: e.target.checked }))}
className="rounded"
/>
<label htmlFor="refrigeration" className="text-sm text-[var(--text-primary)]">
Requiere refrigeración
<div className="flex items-center space-x-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editedItem.requires_refrigeration}
onChange={(e) => setEditedItem({ ...editedItem, requires_refrigeration: e.target.checked })}
className="rounded"
/>
<span className="text-sm">Requiere refrigeración</span>
</label>
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">
<Button onClick={handleSave}>
Guardar
</Button>
</div>
</form>
</div>
);
};

View File

@@ -1,30 +1,68 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Eye, CheckCircle, AlertCircle, Edit, Trash2 } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useAlertActions } from '../../../../stores/alerts.store';
interface Product {
id: string;
name: string;
category: string;
confidence: number;
sales_count: number;
estimated_price: number;
sales_count?: number;
estimated_price?: number;
status: 'approved' | 'rejected' | 'pending';
notes?: string;
// Fields from API suggestion
suggestion_id?: string;
original_name: string;
suggested_name: string;
product_type: 'ingredient' | 'finished_product';
unit_of_measure: string;
estimated_shelf_life_days: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
is_seasonal: boolean;
suggested_supplier?: string;
sales_data?: {
total_quantity: number;
average_daily_sales: number;
peak_day: string;
frequency: number;
};
}
// Mock detected products
const mockDetectedProducts: Product[] = [
{ id: '1', name: 'Pan Integral', category: 'Panadería', confidence: 95, sales_count: 45, estimated_price: 2.50, status: 'pending' },
{ id: '2', name: 'Croissant', category: 'Bollería', confidence: 92, sales_count: 38, estimated_price: 1.80, status: 'pending' },
{ id: '3', name: 'Baguette', category: 'Panadería', confidence: 88, sales_count: 22, estimated_price: 3.00, status: 'pending' },
{ id: '4', name: 'Empanada de Pollo', category: 'Salados', confidence: 85, sales_count: 31, estimated_price: 4.50, status: 'pending' },
{ id: '5', name: 'Tarta de Manzana', category: 'Repostería', confidence: 78, sales_count: 12, estimated_price: 15.00, status: 'pending' },
{ id: '6', name: 'Pan de Centeno', category: 'Panadería', confidence: 91, sales_count: 18, estimated_price: 2.80, status: 'pending' },
{ id: '7', name: 'Medialunas', category: 'Bollería', confidence: 87, sales_count: 29, estimated_price: 1.20, status: 'pending' },
{ id: '8', name: 'Sandwich Mixto', category: 'Salados', confidence: 82, sales_count: 25, estimated_price: 5.50, status: 'pending' }
];
// Convert API suggestions to Product interface
const convertSuggestionsToProducts = (suggestions: any[]): Product[] => {
console.log('ReviewStep - convertSuggestionsToProducts called with:', suggestions);
const products = suggestions.map((suggestion, index) => ({
id: suggestion.suggestion_id || `product-${index}`,
name: suggestion.suggested_name,
category: suggestion.category,
confidence: Math.round(suggestion.confidence_score * 100),
status: 'pending' as const,
// Store original API data
suggestion_id: suggestion.suggestion_id,
original_name: suggestion.original_name,
suggested_name: suggestion.suggested_name,
product_type: suggestion.product_type,
unit_of_measure: suggestion.unit_of_measure,
estimated_shelf_life_days: suggestion.estimated_shelf_life_days,
requires_refrigeration: suggestion.requires_refrigeration,
requires_freezing: suggestion.requires_freezing,
is_seasonal: suggestion.is_seasonal,
suggested_supplier: suggestion.suggested_supplier,
notes: suggestion.notes,
sales_data: suggestion.sales_data,
// Legacy fields for display
sales_count: suggestion.sales_data?.total_quantity || 0,
estimated_price: 0 // Price estimation not provided by current API
}));
console.log('ReviewStep - Converted products:', products);
return products;
};
export const ReviewStep: React.FC<OnboardingStepProps> = ({
data,
@@ -34,35 +72,128 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
isFirstStep,
isLastStep
}) => {
// Generate products from processing results or use mock data
const { createAlert } = useAlertActions();
// Generate products from AI suggestions in processing results
const generateProductsFromResults = (results: any) => {
if (!results?.product_list) return mockDetectedProducts;
console.log('ReviewStep - generateProductsFromResults called with:', results);
console.log('ReviewStep - results keys:', Object.keys(results || {}));
console.log('ReviewStep - results.aiSuggestions:', results?.aiSuggestions);
console.log('ReviewStep - aiSuggestions length:', results?.aiSuggestions?.length);
console.log('ReviewStep - aiSuggestions type:', typeof results?.aiSuggestions);
console.log('ReviewStep - aiSuggestions is array:', Array.isArray(results?.aiSuggestions));
return results.product_list.map((name: string, index: number) => ({
id: (index + 1).toString(),
name,
category: index < 3 ? 'Panadería' : index < 5 ? 'Bollería' : 'Salados',
confidence: Math.max(75, results.confidenceScore - Math.random() * 15),
sales_count: Math.floor(Math.random() * 50) + 10,
estimated_price: Math.random() * 5 + 1.5,
status: 'pending' as const
}));
if (results?.aiSuggestions && results.aiSuggestions.length > 0) {
console.log('ReviewStep - Using AI suggestions:', results.aiSuggestions);
return convertSuggestionsToProducts(results.aiSuggestions);
}
// Fallback: create products from product list if no AI suggestions
if (results?.product_list) {
console.log('ReviewStep - Using fallback product list:', results.product_list);
return results.product_list.map((name: string, index: number) => ({
id: `fallback-${index}`,
name,
original_name: name,
suggested_name: name,
category: 'Sin clasificar',
confidence: 50,
status: 'pending' as const,
product_type: 'finished_product' as const,
unit_of_measure: 'unidad',
estimated_shelf_life_days: 7,
requires_refrigeration: false,
requires_freezing: false,
is_seasonal: false
}));
}
return [];
};
const [products, setProducts] = useState<Product[]>(
data.detectedProducts || generateProductsFromResults(data.processingResults)
);
const [products, setProducts] = useState<Product[]>(() => {
if (data.detectedProducts) {
return data.detectedProducts;
}
// Try to get processing results from current step data first, then from previous step data
const processingResults = data.processingResults || data.allStepData?.['data-processing']?.processingResults;
console.log('ReviewStep - Initializing with processingResults:', processingResults);
return generateProductsFromResults(processingResults);
});
// Check for empty products and show alert after component mounts
useEffect(() => {
const processingResults = data.processingResults || data.allStepData?.['data-processing']?.processingResults;
if (products.length === 0 && processingResults) {
createAlert({
type: 'warning',
category: 'system',
priority: 'medium',
title: 'Sin productos detectados',
message: 'No se encontraron productos en los datos procesados. Verifique el archivo de ventas.',
source: 'onboarding'
});
}
}, [products.length, data.processingResults, data.allStepData, createAlert]);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))];
// Memoize computed values to avoid unnecessary recalculations
const approvedProducts = useMemo(() =>
products.filter(p => p.status === 'approved'),
[products]
);
const reviewCompleted = useMemo(() =>
products.length > 0 && products.every(p => p.status !== 'pending'),
[products]
);
const [lastReviewCompleted, setLastReviewCompleted] = useState(false);
const dataChangeRef = useRef({ products: [], approvedProducts: [], reviewCompleted: false });
// Update parent data when products change
useEffect(() => {
onDataChange({
...data,
detectedProducts: products,
reviewCompleted: products.every(p => p.status !== 'pending')
});
}, [products]);
const currentState = { products, approvedProducts, reviewCompleted };
const lastState = dataChangeRef.current;
// Only call onDataChange if the state actually changed
if (JSON.stringify(currentState) !== JSON.stringify(lastState)) {
console.log('ReviewStep - Updating parent data with:', {
detectedProducts: products,
approvedProducts,
reviewCompleted,
approvedProductsCount: approvedProducts.length
});
onDataChange({
...data,
detectedProducts: products,
approvedProducts,
reviewCompleted
});
dataChangeRef.current = currentState;
}
}, [products, approvedProducts, reviewCompleted]);
// Handle review completion alert separately
useEffect(() => {
if (reviewCompleted && approvedProducts.length > 0 && !lastReviewCompleted) {
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Revisión completada',
message: `Se aprobaron ${approvedProducts.length} de ${products.length} productos detectados.`,
source: 'onboarding'
});
setLastReviewCompleted(true);
}
if (!reviewCompleted && lastReviewCompleted) {
setLastReviewCompleted(false);
}
}, [reviewCompleted, approvedProducts.length, products.length, lastReviewCompleted, createAlert]);
const handleProductAction = (productId: string, action: 'approve' | 'reject') => {
setProducts(prev => prev.map(product =>
@@ -95,191 +226,188 @@ export const ReviewStep: React.FC<OnboardingStepProps> = ({
pending: products.filter(p => p.status === 'pending').length
};
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';
};
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';
}
};
if (products.length === 0) {
return (
<div className="space-y-8">
<div className="text-center py-16">
<AlertCircle className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-gray-600 mb-2">
No se encontraron productos
</h3>
<p className="text-gray-500 mb-6 max-w-md mx-auto">
No se pudieron detectar productos en el archivo procesado.
Verifique que el archivo contenga datos de ventas válidos.
</p>
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Volver al paso anterior
</Button>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.total}</p>
<p className="text-sm text-[var(--text-secondary)]">Total</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<Card className="p-6 text-center">
<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>
</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>
<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>
<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>
</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 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"
>
{categories.map(category => (
<option key={category} value={category}>
{category === 'all' ? 'Todas las categorías' : category}
</option>
))}
</select>
<Badge variant="outline" className="text-sm">
{getFilteredProducts().length} productos
</Badge>
</div>
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.approved}</p>
<p className="text-sm text-[var(--text-secondary)]">Aprobados</p>
</div>
<div className="text-center p-4 bg-[var(--color-error)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-error)]">{stats.rejected}</p>
<p className="text-sm text-[var(--text-secondary)]">Rechazados</p>
</div>
<div className="text-center p-4 bg-[var(--color-warning)]/10 rounded-lg">
<p className="text-2xl font-bold text-[var(--color-warning)]">{stats.pending}</p>
<p className="text-sm text-[var(--text-secondary)]">Pendientes</p>
<div className="flex space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-green-600 border-green-200 hover:bg-green-50"
>
Aprobar todos
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-red-600 border-red-200 hover:bg-red-50"
>
Rechazar todos
</Button>
</div>
</div>
{/* Filters and Actions */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div>
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-2">
Filtrar por categoría:
</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="border border-[var(--border-secondary)] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)]"
>
{categories.map(cat => (
<option key={cat} value={cat}>
{cat === 'all' ? 'Todas las categorías' : cat}
</option>
))}
</select>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('approve')}
className="text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar Visibles
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleBulkAction('reject')}
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
>
<Trash2 className="w-4 h-4 mr-1" />
Rechazar Visibles
</Button>
</div>
</div>
</Card>
{/* Products List */}
<div className="space-y-4">
<div className="space-y-3">
{getFilteredProducts().map((product) => (
<Card key={product.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Status Icon */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
product.status === 'approved'
? 'bg-[var(--color-success)]'
: product.status === 'rejected'
? 'bg-[var(--color-error)]'
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
}`}>
{product.status === 'approved' ? (
<CheckCircle className="w-4 h-4 text-white" />
) : product.status === 'rejected' ? (
<Trash2 className="w-4 h-4 text-white" />
) : (
<Eye className="w-4 h-4 text-[var(--text-tertiary)]" />
)}
<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">
{product.category}
</Badge>
<Badge className={`text-xs ${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>
{/* Product Info */}
<div className="flex-1">
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{product.name}</h4>
<div className="flex items-center gap-2 mb-3">
<Badge variant="gray">{product.category}</Badge>
{product.status !== 'pending' && (
<Badge variant={product.status === 'approved' ? 'green' : 'red'}>
{product.status === 'approved' ? 'Aprobado' : 'Rechazado'}
</Badge>
<div className="text-sm text-[var(--text-secondary)] space-y-1">
{product.original_name && product.original_name !== product.name && (
<div>Nombre original: <span className="font-medium">{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>
{product.sales_data && (
<span>Ventas: {product.sales_data.total_quantity}</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
<span className={`px-2 py-1 rounded text-xs ${
product.confidence >= 90
? 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
: product.confidence >= 75
? 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
: 'bg-[var(--color-error)]/10 text-[var(--color-error)]'
}`}>
{product.confidence}% confianza
</span>
<span>{product.sales_count} ventas</span>
<span>${product.estimated_price.toFixed(2)}</span>
</div>
{product.notes && (
<div className="text-xs italic">Nota: {product.notes}</div>
)}
</div>
</div>
{/* Actions */}
<div className="flex gap-2 ml-4 mt-1">
{product.status === 'pending' ? (
<>
<Button
size="sm"
onClick={() => handleProductAction(product.id, 'approve')}
className="bg-[var(--color-success)] hover:bg-[var(--color-success)]/90 text-white"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleProductAction(product.id, 'reject')}
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
>
<Trash2 className="w-4 h-4 mr-1" />
Rechazar
</Button>
</>
) : (
<Button
size="sm"
variant="outline"
onClick={() => setProducts(prev => prev.map(p => p.id === product.id ? {...p, status: 'pending'} : p))}
className="text-[var(--text-secondary)] hover:text-[var(--color-primary)]"
>
<Edit className="w-4 h-4 mr-1" />
Modificar
</Button>
)}
<div className="flex items-center space-x-2">
<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'}
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprobar
</Button>
<Button
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'}
>
<AlertCircle className="w-4 h-4 mr-1" />
Rechazar
</Button>
</div>
</div>
</Card>
))}
</div>
{/* Progress Indicator */}
{stats.pending > 0 && (
<Card className="p-4 bg-[var(--color-warning)]/5 border-[var(--color-warning)]/20">
<div className="flex items-center space-x-3">
<AlertCircle className="w-5 h-5 text-[var(--color-warning)]" />
<div>
<p className="font-medium text-[var(--text-primary)]">
{stats.pending} productos pendientes de revisión
</p>
<p className="text-sm text-[var(--text-secondary)]">
Revisa todos los productos antes de continuar al siguiente paso
</p>
</div>
</div>
</Card>
)}
{/* Help Information */}
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
<h4 className="font-medium text-[var(--color-info)] mb-3">
💡 Consejos para la revisión:
</h4>
<ul className="text-sm text-[var(--color-info)] space-y-1">
<li> <strong>Confianza alta (90%+):</strong> Productos identificados con alta precisión</li>
<li> <strong>Confianza media (75-89%):</strong> Revisar nombres y categorías</li>
<li> <strong>Confianza baja (&lt;75%):</strong> Verificar que corresponden a tu catálogo</li>
<li> Usa las acciones masivas para aprobar/rechazar por categoría completa</li>
</ul>
</Card>
{/* 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>
</div>
);
};

View File

@@ -1,66 +1,40 @@
import React, { useState, useEffect } from 'react';
import { Truck, Phone, Mail, Plus, Edit, Trash2, MapPin } from 'lucide-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 { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAlertActions } from '../../../../stores/alerts.store';
interface Supplier {
id: string;
// Frontend supplier interface that matches the form needs
interface SupplierFormData {
id?: string;
name: string;
contact_person: string;
contact_name: string;
phone: string;
email: string;
address: string;
categories: string[];
payment_terms: string;
delivery_days: string[];
min_order_amount?: number;
notes?: string;
status: 'active' | 'inactive';
created_at: string;
delivery_terms: string;
tax_id?: string;
is_active: boolean;
}
// Mock suppliers
const mockSuppliers: Supplier[] = [
{
id: '1',
name: 'Molinos del Sur',
contact_person: 'Juan Pérez',
phone: '+1 555-0123',
email: 'ventas@molinosdelsur.com',
address: 'Av. Industrial 123, Zona Sur',
categories: ['Harinas', 'Granos'],
payment_terms: '30 días',
delivery_days: ['Lunes', 'Miércoles', 'Viernes'],
min_order_amount: 200,
notes: 'Proveedor principal de harinas, muy confiable',
status: 'active',
created_at: '2024-01-15'
},
{
id: '2',
name: 'Lácteos Premium',
contact_person: 'María González',
phone: '+1 555-0456',
email: 'pedidos@lacteospremium.com',
address: 'Calle Central 456, Centro',
categories: ['Lácteos', 'Mantequillas', 'Quesos'],
payment_terms: '15 días',
delivery_days: ['Martes', 'Jueves', 'Sábado'],
min_order_amount: 150,
status: 'active',
created_at: '2024-01-20'
}
];
const daysOfWeek = [
'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'
];
const commonCategories = [
'Harinas', 'Lácteos', 'Levaduras', 'Azúcares', 'Grasas', 'Huevos',
'Frutas', 'Chocolates', 'Frutos Secos', 'Especias', 'Conservantes'
];
const paymentTermsOptions = [
'Inmediato',
'15 días',
'30 días',
'45 días',
'60 días',
'90 días'
];
export const SuppliersStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
@@ -69,13 +43,57 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
isFirstStep,
isLastStep
}) => {
const [suppliers, setSuppliers] = useState<Supplier[]>(
data.suppliers || mockSuppliers
);
const [editingSupplier, setEditingSupplier] = useState<Supplier | null>(null);
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const { createAlert } = useAlertActions();
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [editingSupplier, setEditingSupplier] = useState<SupplierFormData | null>(null);
const [isAddingNew, setIsAddingNew] = useState(false);
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'inactive'>('all');
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [updating, setUpdating] = useState(false);
const [deleting, setDeleting] = useState<string | null>(null);
// Load suppliers from backend on component mount
useEffect(() => {
const loadSuppliers = async () => {
// Check if we already have suppliers loaded
if (data.suppliers && Array.isArray(data.suppliers) && data.suppliers.length > 0) {
setSuppliers(data.suppliers);
return;
}
if (!currentTenant?.id) {
return;
}
setLoading(true);
try {
const response = await procurementService.getSuppliers({ size: 100 });
if (response.success && response.data) {
setSuppliers(response.data.items);
}
} catch (error) {
console.error('Failed to load suppliers:', error);
createAlert({
type: 'error',
category: 'system',
priority: 'medium',
title: 'Error al cargar proveedores',
message: 'No se pudieron cargar los proveedores existentes.',
source: 'onboarding'
});
} finally {
setLoading(false);
}
};
loadSuppliers();
}, [currentTenant?.id]);
// Update parent data when suppliers change
useEffect(() => {
onDataChange({
...data,
@@ -85,60 +103,202 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
}, [suppliers]);
const handleAddSupplier = () => {
const newSupplier: Supplier = {
id: Date.now().toString(),
const newSupplier: SupplierFormData = {
name: '',
contact_person: '',
contact_name: '',
phone: '',
email: '',
address: '',
categories: [],
payment_terms: '30 días',
delivery_days: [],
status: 'active',
created_at: new Date().toISOString().split('T')[0]
delivery_terms: 'Recoger en tienda',
tax_id: '',
is_active: true
};
setEditingSupplier(newSupplier);
setIsAddingNew(true);
};
const handleSaveSupplier = (supplier: Supplier) => {
const handleSaveSupplier = async (supplierData: SupplierFormData) => {
if (isAddingNew) {
setSuppliers(prev => [...prev, supplier]);
setCreating(true);
try {
const response = await procurementService.createSupplier({
name: supplierData.name,
contact_name: supplierData.contact_name,
phone: supplierData.phone,
email: supplierData.email,
address: supplierData.address,
payment_terms: supplierData.payment_terms,
delivery_terms: supplierData.delivery_terms,
tax_id: supplierData.tax_id,
is_active: supplierData.is_active
});
if (response.success && response.data) {
setSuppliers(prev => [...prev, response.data]);
createAlert({
type: 'success',
category: 'system',
priority: 'low',
title: 'Proveedor creado',
message: `El proveedor ${response.data.name} se ha creado exitosamente.`,
source: 'onboarding'
});
}
} catch (error) {
console.error('Failed to create supplier:', error);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al crear proveedor',
message: 'No se pudo crear el proveedor. Intente nuevamente.',
source: 'onboarding'
});
} finally {
setCreating(false);
}
} else {
setSuppliers(prev => prev.map(s => s.id === supplier.id ? supplier : s));
// Update existing supplier
if (!supplierData.id) return;
setUpdating(true);
try {
const response = await procurementService.updateSupplier(supplierData.id, {
name: supplierData.name,
contact_name: supplierData.contact_name,
phone: supplierData.phone,
email: supplierData.email,
address: supplierData.address,
payment_terms: supplierData.payment_terms,
delivery_terms: supplierData.delivery_terms,
tax_id: supplierData.tax_id,
is_active: supplierData.is_active
});
if (response.success && response.data) {
setSuppliers(prev => prev.map(s => s.id === response.data.id ? response.data : s));
createAlert({
type: 'success',
category: 'system',
priority: 'low',
title: 'Proveedor actualizado',
message: `El proveedor ${response.data.name} se ha actualizado exitosamente.`,
source: 'onboarding'
});
}
} catch (error) {
console.error('Failed to update supplier:', error);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al actualizar proveedor',
message: 'No se pudo actualizar el proveedor. Intente nuevamente.',
source: 'onboarding'
});
} finally {
setUpdating(false);
}
}
setEditingSupplier(null);
setIsAddingNew(false);
};
const handleDeleteSupplier = (id: string) => {
if (window.confirm('¿Estás seguro de eliminar este proveedor?')) {
setSuppliers(prev => prev.filter(s => s.id !== id));
const handleDeleteSupplier = async (id: string) => {
if (!window.confirm('¿Estás seguro de eliminar este proveedor? Esta acción no se puede deshacer.')) {
return;
}
setDeleting(id);
try {
const response = await procurementService.deleteSupplier(id);
if (response.success) {
setSuppliers(prev => prev.filter(s => s.id !== id));
createAlert({
type: 'success',
category: 'system',
priority: 'low',
title: 'Proveedor eliminado',
message: 'El proveedor se ha eliminado exitosamente.',
source: 'onboarding'
});
}
} catch (error) {
console.error('Failed to delete supplier:', error);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al eliminar proveedor',
message: 'No se pudo eliminar el proveedor. Intente nuevamente.',
source: 'onboarding'
});
} finally {
setDeleting(null);
}
};
const toggleSupplierStatus = (id: string) => {
setSuppliers(prev => prev.map(s =>
s.id === id
? { ...s, status: s.status === 'active' ? 'inactive' : 'active' }
: s
));
const toggleSupplierStatus = async (id: string, currentStatus: boolean) => {
try {
const response = await procurementService.updateSupplier(id, {
is_active: !currentStatus
});
if (response.success && response.data) {
setSuppliers(prev => prev.map(s =>
s.id === id ? response.data : s
));
createAlert({
type: 'success',
category: 'system',
priority: 'low',
title: 'Estado actualizado',
message: `El proveedor se ha ${!currentStatus ? 'activado' : 'desactivado'} exitosamente.`,
source: 'onboarding'
});
}
} catch (error) {
console.error('Failed to toggle supplier status:', error);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al cambiar estado',
message: 'No se pudo cambiar el estado del proveedor.',
source: 'onboarding'
});
}
};
const getFilteredSuppliers = () => {
return filterStatus === 'all'
? suppliers
: suppliers.filter(s => s.status === filterStatus);
if (filterStatus === 'all') {
return suppliers;
}
return suppliers.filter(s =>
filterStatus === 'active' ? s.is_active : !s.is_active
);
};
const stats = {
total: suppliers.length,
active: suppliers.filter(s => s.status === 'active').length,
inactive: suppliers.filter(s => s.status === 'inactive').length,
categories: Array.from(new Set(suppliers.flatMap(s => s.categories))).length
active: suppliers.filter(s => s.is_active).length,
inactive: suppliers.filter(s => !s.is_active).length,
totalOrders: suppliers.reduce((sum, s) => sum + s.performance_metrics.total_orders, 0)
};
if (loading) {
return (
<div className="flex items-center justify-center py-16">
<div className="text-center">
<Loader className="w-8 h-8 animate-spin text-[var(--color-primary)] mx-auto mb-4" />
<p className="text-[var(--text-secondary)]">Cargando proveedores...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Optional Step Notice */}
@@ -161,8 +321,13 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
<Button
size="sm"
onClick={handleAddSupplier}
disabled={creating}
>
<Plus className="w-4 h-4 mr-2" />
{creating ? (
<Loader className="w-4 h-4 mr-2 animate-spin" />
) : (
<Plus className="w-4 h-4 mr-2" />
)}
Agregar Proveedor
</Button>
</div>
@@ -184,8 +349,8 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
<p className="text-xs text-[var(--text-secondary)]">Inactivos</p>
</Card>
<Card className="p-4 text-center">
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.categories}</p>
<p className="text-xs text-[var(--text-secondary)]">Categorías</p>
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.totalOrders}</p>
<p className="text-xs text-[var(--text-secondary)]">Órdenes</p>
</Card>
</div>
@@ -223,12 +388,12 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
<div className="flex items-start space-x-4 flex-1">
{/* Status Icon */}
<div className={`w-8 h-8 rounded-full flex items-center justify-center mt-1 ${
supplier.status === 'active'
supplier.is_active
? 'bg-[var(--color-success)]/10'
: 'bg-[var(--bg-secondary)] border border-[var(--border-secondary)]'
}`}>
<Truck className={`w-4 h-4 ${
supplier.status === 'active'
supplier.is_active
? 'text-[var(--color-success)]'
: 'text-[var(--text-tertiary)]'
}`} />
@@ -239,63 +404,70 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
<h4 className="font-semibold text-[var(--text-primary)] mb-2">{supplier.name}</h4>
<div className="flex items-center gap-2 mb-3">
<Badge variant={supplier.status === 'active' ? 'green' : 'gray'}>
{supplier.status === 'active' ? 'Activo' : 'Inactivo'}
<Badge variant={supplier.is_active ? 'green' : 'gray'}>
{supplier.is_active ? 'Activo' : 'Inactivo'}
</Badge>
{supplier.categories.slice(0, 2).map((cat, idx) => (
<Badge key={idx} variant="blue">{cat}</Badge>
))}
{supplier.categories.length > 2 && (
<Badge variant="gray">+{supplier.categories.length - 2}</Badge>
{supplier.rating && (
<Badge variant="blue"> {supplier.rating.toFixed(1)}</Badge>
)}
{supplier.performance_metrics.total_orders > 0 && (
<Badge variant="purple">{supplier.performance_metrics.total_orders} órdenes</Badge>
)}
</div>
<div className="flex items-center gap-6 text-sm text-[var(--text-secondary)] mb-2">
<div>
<span className="text-[var(--text-tertiary)]">Contacto: </span>
<span className="font-medium text-[var(--text-primary)]">{supplier.contact_person}</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Entrega: </span>
<span className="font-medium text-[var(--text-primary)]">
{supplier.delivery_days.slice(0, 2).join(', ')}
{supplier.delivery_days.length > 2 && ` +${supplier.delivery_days.length - 2}`}
</span>
</div>
<div>
<span className="text-[var(--text-tertiary)]">Pago: </span>
<span className="font-medium text-[var(--text-primary)]">{supplier.payment_terms}</span>
</div>
{supplier.min_order_amount && (
{supplier.contact_name && (
<div>
<span className="text-[var(--text-tertiary)]">Mín: </span>
<span className="font-medium text-[var(--text-primary)]">${supplier.min_order_amount}</span>
<span className="text-[var(--text-tertiary)]">Contacto: </span>
<span className="font-medium text-[var(--text-primary)]">{supplier.contact_name}</span>
</div>
)}
{supplier.delivery_terms && (
<div>
<span className="text-[var(--text-tertiary)]">Entrega: </span>
<span className="font-medium text-[var(--text-primary)]">{supplier.delivery_terms}</span>
</div>
)}
{supplier.payment_terms && (
<div>
<span className="text-[var(--text-tertiary)]">Pago: </span>
<span className="font-medium text-[var(--text-primary)]">{supplier.payment_terms}</span>
</div>
)}
</div>
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
<div className="flex items-center gap-1">
<Phone className="w-3 h-3" />
<span>{supplier.phone}</span>
</div>
<div className="flex items-center gap-1">
<Mail className="w-3 h-3" />
<span>{supplier.email}</span>
</div>
{supplier.phone && (
<div className="flex items-center gap-1">
<Phone className="w-3 h-3" />
<span>{supplier.phone}</span>
</div>
)}
{supplier.email && (
<div className="flex items-center gap-1">
<Mail className="w-3 h-3" />
<span>{supplier.email}</span>
</div>
)}
<div className="flex items-center gap-1">
<MapPin className="w-3 h-3" />
<span>{supplier.address}</span>
</div>
</div>
{supplier.notes && (
{supplier.performance_metrics.on_time_delivery_rate > 0 && (
<div className="mt-3 p-2 bg-[var(--bg-secondary)] rounded text-sm">
<span className="text-[var(--text-tertiary)]">Notas: </span>
<span className="text-[var(--text-primary)]">{supplier.notes}</span>
<span className="text-[var(--text-tertiary)]">Rendimiento: </span>
<span className="text-[var(--text-primary)]">
{supplier.performance_metrics.on_time_delivery_rate}% entregas a tiempo
</span>
{supplier.performance_metrics.quality_score > 0 && (
<span className="text-[var(--text-primary)]">
, {supplier.performance_metrics.quality_score}/5 calidad
</span>
)}
</div>
)}
</div>
@@ -306,7 +478,24 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
<Button
size="sm"
variant="outline"
onClick={() => setEditingSupplier(supplier)}
onClick={() => {
// Convert backend supplier to form data
const formData: SupplierFormData = {
id: supplier.id,
name: supplier.name,
contact_name: supplier.contact_name || '',
phone: supplier.phone || '',
email: supplier.email || '',
address: supplier.address,
payment_terms: supplier.payment_terms || '30 días',
delivery_terms: supplier.delivery_terms || 'Recoger en tienda',
tax_id: supplier.tax_id || '',
is_active: supplier.is_active
};
setEditingSupplier(formData);
setIsAddingNew(false);
}}
disabled={updating}
>
<Edit className="w-4 h-4 mr-1" />
Editar
@@ -315,13 +504,14 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
<Button
size="sm"
variant="outline"
onClick={() => toggleSupplierStatus(supplier.id)}
className={supplier.status === 'active'
onClick={() => toggleSupplierStatus(supplier.id, supplier.is_active)}
className={supplier.is_active
? 'text-[var(--color-warning)] border-[var(--color-warning)] hover:bg-[var(--color-warning)]/10'
: 'text-[var(--color-success)] border-[var(--color-success)] hover:bg-[var(--color-success)]/10'
}
disabled={updating}
>
{supplier.status === 'active' ? 'Pausar' : 'Activar'}
{supplier.is_active ? 'Pausar' : 'Activar'}
</Button>
<Button
@@ -329,19 +519,29 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
variant="outline"
onClick={() => handleDeleteSupplier(supplier.id)}
className="text-[var(--color-error)] border-[var(--color-error)] hover:bg-[var(--color-error)]/10"
disabled={deleting === supplier.id}
>
<Trash2 className="w-4 h-4" />
{deleting === supplier.id ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</Button>
</div>
</div>
</Card>
))}
{getFilteredSuppliers().length === 0 && (
{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">No hay proveedores en esta categoría</p>
<Button onClick={handleAddSupplier}>
<p className="text-[var(--text-secondary)] mb-4">
{filterStatus === 'all'
? 'No hay proveedores registrados'
: `No hay proveedores ${filterStatus === 'active' ? 'activos' : 'inactivos'}`
}
</p>
<Button onClick={handleAddSupplier} disabled={creating}>
<Plus className="w-4 h-4 mr-2" />
Agregar Primer Proveedor
</Button>
@@ -364,6 +564,8 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
setEditingSupplier(null);
setIsAddingNew(false);
}}
isCreating={creating}
isUpdating={updating}
/>
</Card>
</div>
@@ -388,45 +590,37 @@ export const SuppliersStep: React.FC<OnboardingStepProps> = ({
// Component for editing suppliers
interface SupplierFormProps {
supplier: Supplier;
onSave: (supplier: Supplier) => void;
supplier: SupplierFormData;
onSave: (supplier: SupplierFormData) => void;
onCancel: () => void;
isCreating: boolean;
isUpdating: boolean;
}
const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel }) => {
const SupplierForm: React.FC<SupplierFormProps> = ({
supplier,
onSave,
onCancel,
isCreating,
isUpdating
}) => {
const [formData, setFormData] = useState(supplier);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
alert('El nombre es requerido');
alert('El nombre de la empresa es requerido');
return;
}
if (!formData.contact_person.trim()) {
alert('El contacto es requerido');
if (!formData.address.trim()) {
alert('La dirección es requerida');
return;
}
onSave(formData);
};
const toggleCategory = (category: string) => {
setFormData(prev => ({
...prev,
categories: prev.categories.includes(category)
? prev.categories.filter(c => c !== category)
: [...prev.categories, category]
}));
};
const toggleDeliveryDay = (day: string) => {
setFormData(prev => ({
...prev,
delivery_days: prev.delivery_days.includes(day)
? prev.delivery_days.filter(d => d !== day)
: [...prev.delivery_days, day]
}));
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -438,17 +632,19 @@ const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Molinos del Sur"
disabled={isCreating || isUpdating}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Persona de Contacto *
Persona de Contacto
</label>
<Input
value={formData.contact_person}
onChange={(e) => setFormData(prev => ({ ...prev, contact_person: e.target.value }))}
value={formData.contact_name}
onChange={(e) => setFormData(prev => ({ ...prev, contact_name: e.target.value }))}
placeholder="Juan Pérez"
disabled={isCreating || isUpdating}
/>
</div>
</div>
@@ -462,6 +658,7 @@ const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel
value={formData.phone}
onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
placeholder="+1 555-0123"
disabled={isCreating || isUpdating}
/>
</div>
@@ -474,40 +671,23 @@ const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="ventas@proveedor.com"
disabled={isCreating || isUpdating}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Dirección
Dirección *
</label>
<Input
value={formData.address}
onChange={(e) => setFormData(prev => ({ ...prev, address: e.target.value }))}
placeholder="Av. Industrial 123, Zona Sur"
disabled={isCreating || isUpdating}
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Categorías de Productos
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{commonCategories.map(category => (
<label key={category} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={formData.categories.includes(category)}
onChange={() => toggleCategory(category)}
className="rounded"
/>
<span className="text-sm text-[var(--text-primary)]">{category}</span>
</label>
))}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
@@ -517,67 +697,74 @@ const SupplierForm: React.FC<SupplierFormProps> = ({ supplier, onSave, onCancel
value={formData.payment_terms}
onChange={(e) => setFormData(prev => ({ ...prev, payment_terms: e.target.value }))}
className="w-full p-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
disabled={isCreating || isUpdating}
>
<option value="Inmediato">Inmediato</option>
<option value="15 días">15 días</option>
<option value="30 días">30 días</option>
<option value="45 días">45 días</option>
<option value="60 días">60 días</option>
{paymentTermsOptions.map(term => (
<option key={term} value={term}>{term}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Pedido Mínimo
Términos de Entrega
</label>
<Input
type="number"
value={formData.min_order_amount || ''}
onChange={(e) => setFormData(prev => ({ ...prev, min_order_amount: Number(e.target.value) }))}
placeholder="200"
min="0"
value={formData.delivery_terms}
onChange={(e) => setFormData(prev => ({ ...prev, delivery_terms: e.target.value }))}
placeholder="Recoger en tienda"
disabled={isCreating || isUpdating}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Días de Entrega
</label>
<div className="flex flex-wrap gap-2">
{daysOfWeek.map(day => (
<label key={day} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={formData.delivery_days.includes(day)}
onChange={() => toggleDeliveryDay(day)}
className="rounded"
/>
<span className="text-sm text-[var(--text-primary)]">{day}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Notas
RUT/NIT (Opcional)
</label>
<textarea
value={formData.notes || ''}
onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
placeholder="Información adicional sobre el proveedor..."
className="w-full p-2 border border-[var(--border-secondary)] rounded resize-none focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
rows={3}
<Input
value={formData.tax_id || ''}
onChange={(e) => setFormData(prev => ({ ...prev, tax_id: e.target.value }))}
placeholder="12345678-9"
disabled={isCreating || isUpdating}
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="is_active"
checked={formData.is_active}
onChange={(e) => setFormData(prev => ({ ...prev, is_active: e.target.checked }))}
disabled={isCreating || isUpdating}
className="rounded"
/>
<label htmlFor="is_active" className="text-sm text-[var(--text-primary)]">
Proveedor activo
</label>
</div>
<div className="flex justify-end space-x-3 pt-4 border-t">
<Button type="button" variant="outline" onClick={onCancel}>
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isCreating || isUpdating}
>
Cancelar
</Button>
<Button type="submit">
Guardar Proveedor
<Button
type="submit"
disabled={isCreating || isUpdating}
>
{isCreating || isUpdating ? (
<>
<Loader className="w-4 h-4 mr-2 animate-spin" />
{isCreating ? 'Creando...' : 'Actualizando...'}
</>
) : (
'Guardar Proveedor'
)}
</Button>
</div>
</form>

View File

@@ -1,85 +1,17 @@
import { apiClient, ApiResponse } from './client';
// Request/Response Types based on backend schemas
export interface UserRegistration {
email: string;
password: string;
full_name: string;
tenant_name?: string;
role?: 'user' | 'admin' | 'manager';
}
export interface UserLogin {
email: string;
password: string;
}
export interface UserData {
id: string;
email: string;
full_name: string;
is_active: boolean;
is_verified: boolean;
created_at: string;
tenant_id?: string;
role?: string;
}
export interface TokenResponse {
access_token: string;
refresh_token?: string;
token_type: string;
expires_in: number;
user?: UserData;
}
export interface RefreshTokenRequest {
refresh_token: string;
}
export interface PasswordChange {
current_password: string;
new_password: string;
}
export interface PasswordReset {
email: string;
}
export interface PasswordResetConfirm {
token: string;
new_password: string;
}
export interface TokenVerification {
valid: boolean;
user_id?: string;
email?: string;
exp?: number;
message?: string;
}
export interface UserResponse {
id: string;
email: string;
full_name: string;
is_active: boolean;
is_verified: boolean;
created_at: string;
last_login?: string;
phone?: string;
language?: string;
timezone?: string;
tenant_id?: string;
role?: string;
}
export interface UserUpdate {
full_name?: string;
phone?: string;
language?: string;
timezone?: string;
}
import {
UserRegistration,
UserLogin,
UserData,
TokenResponse,
RefreshTokenRequest,
PasswordChange,
PasswordReset,
PasswordResetConfirm,
TokenVerification,
UserResponse,
UserUpdate
} from '../../types/auth.types';
class AuthService {
private readonly baseUrl = '/auth';

View File

@@ -1,4 +1,5 @@
import axios, { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios';
import { ApiResponse, ApiError } from '../../types/api.types';
// Utility functions to access auth and tenant store data from localStorage
const getAuthData = () => {
@@ -27,22 +28,13 @@ const clearAuthData = () => {
localStorage.removeItem('auth-storage');
};
export interface ApiResponse<T = any> {
data: T;
// Client-specific error interface
interface ClientError {
success: boolean;
message?: string;
error?: string;
}
export interface ErrorDetail {
message: string;
code?: string;
field?: string;
}
export interface ApiError {
success: boolean;
error: ErrorDetail;
error: {
message: string;
code?: string;
};
timestamp: string;
}
@@ -116,7 +108,7 @@ class ApiClient {
// Handle network errors
if (!error.response) {
const networkError: ApiError = {
const networkError: ClientError = {
success: false,
error: {
message: 'Network error - please check your connection',

View File

@@ -1,67 +1,27 @@
import { apiClient, ApiResponse } from './client';
// External data types
export interface WeatherData {
id: string;
tenant_id: string;
location_id: string;
date: string;
temperature_avg: number;
temperature_min: number;
temperature_max: number;
humidity: number;
precipitation: number;
wind_speed: number;
condition: string;
description: string;
created_at: string;
}
export interface TrafficData {
id: string;
tenant_id: string;
location_id: string;
date: string;
hour: number;
traffic_level: number;
congestion_index: number;
average_speed: number;
incident_count: number;
created_at: string;
}
export interface EventData {
id: string;
tenant_id: string;
location_id: string;
event_name: string;
event_type: string;
start_date: string;
end_date: string;
expected_attendance?: number;
impact_radius_km?: number;
impact_score: number;
created_at: string;
}
export interface LocationConfig {
id: string;
tenant_id: string;
name: string;
latitude: number;
longitude: number;
address: string;
city: string;
country: string;
is_primary: boolean;
data_sources: {
weather_enabled: boolean;
traffic_enabled: boolean;
events_enabled: boolean;
};
created_at: string;
updated_at: string;
}
import {
WeatherData,
WeatherDataParams,
TrafficData,
TrafficDataParams,
TrafficPatternsParams,
TrafficPattern,
EventData,
EventsParams,
CustomEventCreate,
LocationConfig,
LocationCreate,
ExternalFactorsImpact,
ExternalFactorsParams,
DataQualityReport,
DataSettings,
DataSettingsUpdate,
RefreshDataResponse,
DeleteResponse,
WeatherCondition,
EventType,
RefreshInterval
} from '../../types/data.types';
class DataService {
private readonly baseUrl = '/data';
@@ -75,16 +35,7 @@ class DataService {
return apiClient.get(`${this.baseUrl}/locations/${locationId}`);
}
async createLocation(locationData: {
name: string;
latitude: number;
longitude: number;
address: string;
city: string;
country?: string;
is_primary?: boolean;
data_sources?: LocationConfig['data_sources'];
}): Promise<ApiResponse<LocationConfig>> {
async createLocation(locationData: LocationCreate): Promise<ApiResponse<LocationConfig>> {
return apiClient.post(`${this.baseUrl}/locations`, locationData);
}
@@ -92,18 +43,12 @@ class DataService {
return apiClient.put(`${this.baseUrl}/locations/${locationId}`, locationData);
}
async deleteLocation(locationId: string): Promise<ApiResponse<{ message: string }>> {
async deleteLocation(locationId: string): Promise<ApiResponse<DeleteResponse>> {
return apiClient.delete(`${this.baseUrl}/locations/${locationId}`);
}
// Weather data
async getWeatherData(params?: {
location_id?: string;
start_date?: string;
end_date?: string;
page?: number;
size?: number;
}): Promise<ApiResponse<{ items: WeatherData[]; total: number; page: number; size: number; pages: number }>> {
async getWeatherData(params?: WeatherDataParams): Promise<ApiResponse<{ items: WeatherData[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
@@ -129,7 +74,7 @@ class DataService {
return apiClient.get(`${this.baseUrl}/weather/forecast/${locationId}?days=${days}`);
}
async refreshWeatherData(locationId?: string): Promise<ApiResponse<{ message: string; updated_records: number }>> {
async refreshWeatherData(locationId?: string): Promise<ApiResponse<RefreshDataResponse>> {
const url = locationId
? `${this.baseUrl}/weather/refresh/${locationId}`
: `${this.baseUrl}/weather/refresh`;
@@ -138,14 +83,7 @@ class DataService {
}
// Traffic data
async getTrafficData(params?: {
location_id?: string;
start_date?: string;
end_date?: string;
hour?: number;
page?: number;
size?: number;
}): Promise<ApiResponse<{ items: TrafficData[]; total: number; page: number; size: number; pages: number }>> {
async getTrafficData(params?: TrafficDataParams): Promise<ApiResponse<{ items: TrafficData[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
@@ -167,15 +105,7 @@ class DataService {
return apiClient.get(`${this.baseUrl}/traffic/current/${locationId}`);
}
async getTrafficPatterns(locationId: string, params?: {
days_back?: number;
granularity?: 'hourly' | 'daily';
}): Promise<ApiResponse<Array<{
period: string;
average_traffic_level: number;
peak_hours: number[];
congestion_patterns: Record<string, number>;
}>>> {
async getTrafficPatterns(locationId: string, params?: TrafficPatternsParams): Promise<ApiResponse<TrafficPattern[]>> {
const queryParams = new URLSearchParams();
if (params) {
@@ -193,7 +123,7 @@ class DataService {
return apiClient.get(url);
}
async refreshTrafficData(locationId?: string): Promise<ApiResponse<{ message: string; updated_records: number }>> {
async refreshTrafficData(locationId?: string): Promise<ApiResponse<RefreshDataResponse>> {
const url = locationId
? `${this.baseUrl}/traffic/refresh/${locationId}`
: `${this.baseUrl}/traffic/refresh`;
@@ -202,14 +132,7 @@ class DataService {
}
// Events data
async getEvents(params?: {
location_id?: string;
start_date?: string;
end_date?: string;
event_type?: string;
page?: number;
size?: number;
}): Promise<ApiResponse<{ items: EventData[]; total: number; page: number; size: number; pages: number }>> {
async getEvents(params?: EventsParams): Promise<ApiResponse<{ items: EventData[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
@@ -231,16 +154,7 @@ class DataService {
return apiClient.get(`${this.baseUrl}/events/upcoming/${locationId}?days=${days}`);
}
async createCustomEvent(eventData: {
location_id: string;
event_name: string;
event_type: string;
start_date: string;
end_date: string;
expected_attendance?: number;
impact_radius_km?: number;
impact_score?: number;
}): Promise<ApiResponse<EventData>> {
async createCustomEvent(eventData: CustomEventCreate): Promise<ApiResponse<EventData>> {
return apiClient.post(`${this.baseUrl}/events`, eventData);
}
@@ -248,11 +162,11 @@ class DataService {
return apiClient.put(`${this.baseUrl}/events/${eventId}`, eventData);
}
async deleteEvent(eventId: string): Promise<ApiResponse<{ message: string }>> {
async deleteEvent(eventId: string): Promise<ApiResponse<DeleteResponse>> {
return apiClient.delete(`${this.baseUrl}/events/${eventId}`);
}
async refreshEventsData(locationId?: string): Promise<ApiResponse<{ message: string; updated_records: number }>> {
async refreshEventsData(locationId?: string): Promise<ApiResponse<RefreshDataResponse>> {
const url = locationId
? `${this.baseUrl}/events/refresh/${locationId}`
: `${this.baseUrl}/events/refresh`;
@@ -261,27 +175,7 @@ class DataService {
}
// Combined analytics
async getExternalFactorsImpact(params?: {
location_id?: string;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<{
weather_impact: {
temperature_correlation: number;
precipitation_impact: number;
most_favorable_conditions: string;
};
traffic_impact: {
congestion_correlation: number;
peak_traffic_effect: number;
optimal_traffic_levels: number[];
};
events_impact: {
positive_events: EventData[];
negative_events: EventData[];
average_event_boost: number;
};
}>> {
async getExternalFactorsImpact(params?: ExternalFactorsParams): Promise<ApiResponse<ExternalFactorsImpact>> {
const queryParams = new URLSearchParams();
if (params) {
@@ -299,64 +193,21 @@ class DataService {
return apiClient.get(url);
}
async getDataQualityReport(): Promise<ApiResponse<{
overall_score: number;
data_sources: Array<{
source: 'weather' | 'traffic' | 'events';
completeness: number;
freshness_hours: number;
reliability_score: number;
last_update: string;
}>;
recommendations: Array<{
priority: 'high' | 'medium' | 'low';
message: string;
action: string;
}>;
}>> {
async getDataQualityReport(): Promise<ApiResponse<DataQualityReport>> {
return apiClient.get(`${this.baseUrl}/quality-report`);
}
// Data configuration
async getDataSettings(): Promise<ApiResponse<{
auto_refresh_enabled: boolean;
refresh_intervals: {
weather_minutes: number;
traffic_minutes: number;
events_hours: number;
};
data_retention_days: {
weather: number;
traffic: number;
events: number;
};
external_apis: {
weather_provider: string;
traffic_provider: string;
events_provider: string;
};
}>> {
async getDataSettings(): Promise<ApiResponse<DataSettings>> {
return apiClient.get(`${this.baseUrl}/settings`);
}
async updateDataSettings(settings: {
auto_refresh_enabled?: boolean;
refresh_intervals?: {
weather_minutes?: number;
traffic_minutes?: number;
events_hours?: number;
};
data_retention_days?: {
weather?: number;
traffic?: number;
events?: number;
};
}): Promise<ApiResponse<any>> {
async updateDataSettings(settings: DataSettingsUpdate): Promise<ApiResponse<DataSettings>> {
return apiClient.put(`${this.baseUrl}/settings`, settings);
}
// Utility methods
getWeatherConditions(): { value: string; label: string; impact: 'positive' | 'negative' | 'neutral' }[] {
getWeatherConditions(): WeatherCondition[] {
return [
{ value: 'sunny', label: 'Sunny', impact: 'positive' },
{ value: 'cloudy', label: 'Cloudy', impact: 'neutral' },
@@ -367,7 +218,7 @@ class DataService {
];
}
getEventTypes(): { value: string; label: string; typical_impact: 'positive' | 'negative' | 'neutral' }[] {
getEventTypes(): EventType[] {
return [
{ value: 'festival', label: 'Festival', typical_impact: 'positive' },
{ value: 'concert', label: 'Concert', typical_impact: 'positive' },
@@ -380,7 +231,7 @@ class DataService {
];
}
getRefreshIntervals(): { value: number; label: string; suitable_for: string[] }[] {
getRefreshIntervals(): RefreshInterval[] {
return [
{ value: 5, label: '5 minutes', suitable_for: ['traffic'] },
{ value: 15, label: '15 minutes', suitable_for: ['traffic'] },

View File

@@ -1,68 +1,10 @@
import { apiClient, ApiResponse } from './client';
// Request/Response Types
export interface ForecastRequest {
product_name: string;
days_ahead: number;
start_date?: string;
include_confidence_intervals?: boolean;
external_factors?: {
weather?: string[];
events?: string[];
holidays?: boolean;
};
}
export interface ForecastResponse {
id: string;
tenant_id: string;
product_name: string;
forecast_date: string;
predicted_demand: number;
confidence_lower: number;
confidence_upper: number;
confidence_level: number;
external_factors: Record<string, any>;
model_version: string;
created_at: string;
actual_demand?: number;
accuracy_score?: number;
}
export interface PredictionBatch {
id: string;
tenant_id: string;
name: string;
description?: string;
parameters: Record<string, any>;
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
total_predictions: number;
completed_predictions: number;
failed_predictions: number;
created_at: string;
completed_at?: string;
error_message?: string;
}
export interface ModelPerformance {
model_id: string;
model_name: string;
version: string;
accuracy_metrics: {
mape: number; // Mean Absolute Percentage Error
rmse: number; // Root Mean Square Error
mae: number; // Mean Absolute Error
r2_score: number;
};
training_data_period: {
start_date: string;
end_date: string;
total_records: number;
};
last_training_date: string;
performance_trend: 'improving' | 'stable' | 'declining';
}
import {
ForecastRequest,
ForecastResponse,
PredictionBatch,
ModelPerformance
} from '../../types/forecasting.types';
class ForecastingService {
private readonly baseUrl = '/forecasting';

View File

@@ -1,35 +1,20 @@
import { apiClient, ApiResponse } from './client';
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
UnitOfMeasure,
ProductType,
StockMovementType,
Ingredient,
Stock,
StockMovement,
StockAlert,
InventorySummary,
StockLevelSummary
} from '../../types/inventory.types';
import { PaginatedResponse } from '../../types/api.types';
// Enums
export enum UnitOfMeasure {
KILOGRAM = 'kg',
GRAM = 'g',
LITER = 'l',
MILLILITER = 'ml',
PIECE = 'piece',
PACKAGE = 'package',
BAG = 'bag',
BOX = 'box',
DOZEN = 'dozen',
}
export enum ProductType {
INGREDIENT = 'ingredient',
FINISHED_PRODUCT = 'finished_product',
}
export enum StockMovementType {
PURCHASE = 'purchase',
SALE = 'sale',
USAGE = 'usage',
WASTE = 'waste',
ADJUSTMENT = 'adjustment',
TRANSFER = 'transfer',
RETURN = 'return',
}
// Request/Response Types
export interface IngredientCreate {
// Service-specific types for Create/Update operations
interface IngredientCreate {
name: string;
product_type?: ProductType;
sku?: string;
@@ -57,74 +42,11 @@ export interface IngredientCreate {
allergen_info?: Record<string, any>;
}
export interface IngredientUpdate {
name?: string;
product_type?: ProductType;
sku?: string;
barcode?: string;
category?: string;
subcategory?: string;
description?: string;
brand?: string;
unit_of_measure?: UnitOfMeasure;
package_size?: number;
average_cost?: number;
standard_cost?: number;
low_stock_threshold?: number;
reorder_point?: number;
reorder_quantity?: number;
max_stock_level?: number;
requires_refrigeration?: boolean;
requires_freezing?: boolean;
storage_temperature_min?: number;
storage_temperature_max?: number;
storage_humidity_max?: number;
shelf_life_days?: number;
storage_instructions?: string;
interface IngredientUpdate extends Partial<IngredientCreate> {
is_active?: boolean;
is_perishable?: boolean;
allergen_info?: Record<string, any>;
}
export interface IngredientResponse {
id: string;
tenant_id: string;
name: string;
product_type: ProductType;
sku?: string;
barcode?: string;
category?: string;
subcategory?: string;
description?: string;
brand?: string;
unit_of_measure: UnitOfMeasure;
package_size?: number;
average_cost?: number;
last_purchase_price?: number;
standard_cost?: number;
low_stock_threshold: number;
reorder_point: number;
reorder_quantity: number;
max_stock_level?: number;
requires_refrigeration: boolean;
requires_freezing: boolean;
storage_temperature_min?: number;
storage_temperature_max?: number;
storage_humidity_max?: number;
shelf_life_days?: number;
storage_instructions?: string;
is_active: boolean;
is_perishable: boolean;
allergen_info?: Record<string, any>;
created_at: string;
updated_at: string;
created_by?: string;
current_stock?: number;
is_low_stock?: boolean;
needs_reorder?: boolean;
}
export interface StockCreate {
interface StockCreate {
ingredient_id: string;
batch_number?: string;
lot_number?: string;
@@ -140,50 +62,12 @@ export interface StockCreate {
quality_status?: string;
}
export interface StockUpdate {
batch_number?: string;
lot_number?: string;
supplier_batch_ref?: string;
current_quantity?: number;
interface StockUpdate extends Partial<StockCreate> {
reserved_quantity?: number;
received_date?: string;
expiration_date?: string;
best_before_date?: string;
unit_cost?: number;
storage_location?: string;
warehouse_zone?: string;
shelf_position?: string;
is_available?: boolean;
quality_status?: string;
}
export interface StockResponse {
id: string;
tenant_id: string;
ingredient_id: string;
batch_number?: string;
lot_number?: string;
supplier_batch_ref?: string;
current_quantity: number;
reserved_quantity: number;
available_quantity: number;
received_date?: string;
expiration_date?: string;
best_before_date?: string;
unit_cost?: number;
total_cost?: number;
storage_location?: string;
warehouse_zone?: string;
shelf_position?: string;
is_available: boolean;
is_expired: boolean;
quality_status: string;
created_at: string;
updated_at: string;
ingredient?: IngredientResponse;
}
export interface StockMovementCreate {
interface StockMovementCreate {
ingredient_id: string;
stock_id?: string;
movement_type: StockMovementType;
@@ -196,107 +80,11 @@ export interface StockMovementCreate {
movement_date?: string;
}
export interface StockMovementResponse {
id: string;
tenant_id: string;
ingredient_id: string;
stock_id?: string;
movement_type: StockMovementType;
quantity: number;
unit_cost?: number;
total_cost?: number;
quantity_before?: number;
quantity_after?: number;
reference_number?: string;
supplier_id?: string;
notes?: string;
reason_code?: string;
movement_date: string;
created_at: string;
created_by?: string;
ingredient?: IngredientResponse;
}
export interface StockAlertResponse {
id: string;
tenant_id: string;
ingredient_id: string;
stock_id?: string;
alert_type: string;
severity: string;
title: string;
message: string;
current_quantity?: number;
threshold_value?: number;
expiration_date?: string;
is_active: boolean;
is_acknowledged: boolean;
acknowledged_by?: string;
acknowledged_at?: string;
is_resolved: boolean;
resolved_by?: string;
resolved_at?: string;
resolution_notes?: string;
created_at: string;
updated_at: string;
ingredient?: IngredientResponse;
}
export interface InventorySummary {
total_ingredients: number;
total_stock_value: number;
low_stock_alerts: number;
expiring_soon_items: number;
expired_items: number;
out_of_stock_items: number;
stock_by_category: Record<string, Record<string, any>>;
recent_movements: number;
recent_purchases: number;
recent_waste: number;
}
export interface StockLevelSummary {
ingredient_id: string;
ingredient_name: string;
unit_of_measure: string;
total_quantity: number;
available_quantity: number;
reserved_quantity: number;
is_low_stock: boolean;
needs_reorder: boolean;
has_expired_stock: boolean;
total_batches: number;
oldest_batch_date?: string;
newest_batch_date?: string;
next_expiration_date?: string;
average_unit_cost?: number;
total_stock_value?: number;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
size: number;
pages: number;
}
export interface InventoryFilter {
category?: string;
is_active?: boolean;
is_low_stock?: boolean;
needs_reorder?: boolean;
search?: string;
}
export interface StockFilter {
ingredient_id?: string;
is_available?: boolean;
is_expired?: boolean;
expiring_within_days?: number;
storage_location?: string;
quality_status?: string;
}
// Type aliases for response consistency
type IngredientResponse = Ingredient;
type StockResponse = Stock;
type StockMovementResponse = StockMovement;
type StockAlertResponse = StockAlert;
class InventoryService {
private readonly baseUrl = '/inventory';

View File

@@ -88,6 +88,7 @@ class OnboardingApiService {
/**
* Step 1: Validate uploaded file and extract unique products
* Now uses Sales Service directly
*/
async validateOnboardingFile(
tenantId: string,
@@ -98,7 +99,7 @@ class OnboardingApiService {
formData.append('file', file);
const response = await apiClient.post<OnboardingFileValidationResponse>(
`${this.basePath}/${tenantId}/onboarding/validate-file`,
`${this.salesBasePath}/${tenantId}/sales/import/validate`,
formData,
{
headers: {
@@ -120,6 +121,7 @@ class OnboardingApiService {
/**
* Step 2: Generate AI-powered inventory suggestions
* Now uses Inventory Service directly
*/
async generateInventorySuggestions(
tenantId: string,
@@ -127,18 +129,24 @@ class OnboardingApiService {
productList: string[]
): Promise<ProductSuggestionsResponse> {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('product_list', JSON.stringify(productList));
if (!productList || !Array.isArray(productList) || productList.length === 0) {
throw new Error('Product list is empty or invalid');
}
// Transform product list into the expected format for BatchClassificationRequest
const products = productList.map(productName => ({
product_name: productName,
// sales_volume is optional, omit it if we don't have the data
sales_data: {} // Additional context can be added later
}));
const requestData = {
products: products
};
const response = await apiClient.post<ProductSuggestionsResponse>(
`${this.basePath}/${tenantId}/onboarding/generate-suggestions`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
`${this.basePath}/${tenantId}/inventory/classify-products-batch`,
requestData
);
if (!response.success) {
@@ -154,34 +162,56 @@ class OnboardingApiService {
/**
* Step 3: Create inventory items from approved suggestions
* Now uses Inventory Service directly
*/
async createInventoryFromSuggestions(
tenantId: string,
approvedSuggestions: any[]
): Promise<InventoryCreationResponse> {
try {
const response = await apiClient.post<InventoryCreationResponse>(
`${this.basePath}/${tenantId}/onboarding/create-inventory`,
{
suggestions: approvedSuggestions
}
);
const createdItems: any[] = [];
const failedItems: any[] = [];
const inventoryMapping: { [productName: string]: string } = {};
if (!response.success) {
throw new Error(`Inventory creation failed: ${response.error || 'Unknown error'}`);
}
// Create inventory items one by one using inventory service
for (const suggestion of approvedSuggestions) {
try {
const ingredientData = {
name: suggestion.suggested_name,
category: suggestion.category,
unit_of_measure: suggestion.unit_of_measure,
shelf_life_days: suggestion.estimated_shelf_life_days,
requires_refrigeration: suggestion.requires_refrigeration,
requires_freezing: suggestion.requires_freezing,
is_seasonal: suggestion.is_seasonal,
product_type: suggestion.product_type
};
// Create inventory mapping if not provided
if (!response.data.inventory_mapping) {
response.data.inventory_mapping = {};
response.data.created_items.forEach((item, index) => {
if (approvedSuggestions[index]) {
response.data.inventory_mapping![approvedSuggestions[index].original_name] = item.id;
const response = await apiClient.post<any>(
`${this.basePath}/${tenantId}/ingredients`,
ingredientData
);
if (response.success) {
createdItems.push(response.data);
inventoryMapping[suggestion.original_name] = response.data.id;
} else {
failedItems.push({ suggestion, error: response.error });
}
});
} catch (error) {
failedItems.push({ suggestion, error: error.message });
}
}
return response.data;
const result = {
created_items: createdItems,
failed_items: failedItems,
total_approved: approvedSuggestions.length,
success_rate: createdItems.length / approvedSuggestions.length,
inventory_mapping: inventoryMapping
};
return result;
} catch (error) {
console.error('Inventory creation failed:', error);
throw error;
@@ -190,6 +220,7 @@ class OnboardingApiService {
/**
* Step 4: Import sales data with inventory mapping
* Now uses Sales Service directly with validation first
*/
async importSalesWithInventory(
tenantId: string,
@@ -197,12 +228,16 @@ class OnboardingApiService {
inventoryMapping: { [productName: string]: string }
): Promise<SalesImportResponse> {
try {
// First validate the file with inventory mapping
await this.validateSalesData(tenantId, file);
// Then import the sales data
const formData = new FormData();
formData.append('file', file);
formData.append('inventory_mapping', JSON.stringify(inventoryMapping));
formData.append('update_existing', 'true');
const response = await apiClient.post<SalesImportResponse>(
`${this.basePath}/${tenantId}/onboarding/import-sales`,
`${this.salesBasePath}/${tenantId}/sales/import`,
formData,
{
headers: {
@@ -224,25 +259,80 @@ class OnboardingApiService {
/**
* Get business model specific recommendations
* Returns static recommendations since orchestration is removed
*/
async getBusinessModelGuide(
tenantId: string,
model: 'production' | 'retail' | 'hybrid'
): Promise<BusinessModelGuide> {
try {
const response = await apiClient.get<BusinessModelGuide>(
`${this.basePath}/${tenantId}/onboarding/business-model-guide?model=${model}`
);
if (!response.success) {
throw new Error(`Failed to get business model guide: ${response.error || 'Unknown error'}`);
// Return static business model guides since we removed orchestration
const guides = {
production: {
title: 'Production Bakery Setup',
description: 'Your bakery focuses on creating products from raw ingredients.',
next_steps: [
'Set up ingredient inventory management',
'Configure recipe management',
'Set up production planning',
'Implement quality control processes'
],
recommended_features: [
'Inventory tracking for raw ingredients',
'Recipe costing and management',
'Production scheduling',
'Supplier management'
],
sample_workflows: [
'Daily production planning based on demand forecasts',
'Inventory reordering based on production schedules',
'Quality control checkpoints during production'
]
},
retail: {
title: 'Retail Bakery Setup',
description: 'Your bakery focuses on selling finished products to customers.',
next_steps: [
'Set up finished product inventory',
'Configure point-of-sale integration',
'Set up customer management',
'Implement sales analytics'
],
recommended_features: [
'Finished product inventory tracking',
'Sales analytics and reporting',
'Customer loyalty programs',
'Promotional campaign management'
],
sample_workflows: [
'Daily sales reporting and analysis',
'Inventory reordering based on sales velocity',
'Customer engagement and retention campaigns'
]
},
hybrid: {
title: 'Hybrid Bakery Setup',
description: 'Your bakery combines production and retail operations.',
next_steps: [
'Set up both ingredient and finished product inventory',
'Configure production-to-retail workflows',
'Set up integrated analytics',
'Implement comprehensive supplier management'
],
recommended_features: [
'Dual inventory management system',
'Production-to-sales analytics',
'Integrated supplier and customer management',
'Cross-channel reporting'
],
sample_workflows: [
'Production planning based on both wholesale and retail demand',
'Integrated inventory management across production and retail',
'Comprehensive business intelligence and reporting'
]
}
};
return response.data;
} catch (error) {
console.error('Failed to get business model guide:', error);
throw error;
}
return guides[model] || guides.hybrid;
}
/**
@@ -366,14 +456,18 @@ class OnboardingApiService {
/**
* Utility: Check if a tenant has completed onboarding
* Uses Auth Service for user progress tracking
*/
async checkOnboardingStatus(tenantId: string): Promise<{ completed: boolean; steps_completed: string[] }> {
try {
const response = await apiClient.get<any>(
`${this.basePath}/${tenantId}/onboarding/status`
'/me/onboarding/progress'
);
return response.data || { completed: false, steps_completed: [] };
return {
completed: response.data?.onboarding_completed || false,
steps_completed: response.data?.completed_steps || []
};
} catch (error) {
console.warn('Could not check onboarding status:', error);
return { completed: false, steps_completed: [] };
@@ -382,11 +476,12 @@ class OnboardingApiService {
/**
* Utility: Mark onboarding as complete
* Uses Auth Service for user progress tracking
*/
async completeOnboarding(tenantId: string, metadata?: any): Promise<void> {
try {
await apiClient.post(
`${this.basePath}/${tenantId}/onboarding/complete`,
'/me/onboarding/complete',
{ metadata }
);
} catch (error) {
@@ -394,6 +489,7 @@ class OnboardingApiService {
// Don't throw error, this is not critical
}
}
}
export const onboardingApiService = new OnboardingApiService();

View File

@@ -1,134 +1,25 @@
import { apiClient, ApiResponse } from './client';
// Enums
export enum OrderStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
IN_PREPARATION = 'in_preparation',
READY = 'ready',
DELIVERED = 'delivered',
CANCELLED = 'cancelled',
}
export enum OrderType {
DINE_IN = 'dine_in',
TAKEAWAY = 'takeaway',
DELIVERY = 'delivery',
CATERING = 'catering',
}
// Request/Response Types
export interface OrderItem {
product_id?: string;
product_name: string;
quantity: number;
unit_price: number;
total_price: number;
notes?: string;
customizations?: Record<string, any>;
}
export interface OrderCreate {
customer_id?: string;
customer_name: string;
customer_email?: string;
customer_phone?: string;
order_type: OrderType;
items: OrderItem[];
special_instructions?: string;
delivery_address?: string;
delivery_date?: string;
delivery_time?: string;
payment_method?: string;
}
export interface OrderUpdate {
status?: OrderStatus;
customer_name?: string;
customer_email?: string;
customer_phone?: string;
special_instructions?: string;
delivery_address?: string;
delivery_date?: string;
delivery_time?: string;
estimated_completion_time?: string;
actual_completion_time?: string;
}
export interface OrderResponse {
id: string;
tenant_id: string;
order_number: string;
customer_id?: string;
customer_name: string;
customer_email?: string;
customer_phone?: string;
order_type: OrderType;
status: OrderStatus;
items: OrderItem[];
subtotal: number;
tax_amount: number;
discount_amount: number;
total_amount: number;
special_instructions?: string;
delivery_address?: string;
delivery_date?: string;
delivery_time?: string;
estimated_completion_time?: string;
actual_completion_time?: string;
payment_method?: string;
payment_status?: string;
created_at: string;
updated_at: string;
created_by?: string;
}
export interface Customer {
id: string;
tenant_id: string;
name: string;
email?: string;
phone?: string;
address?: string;
preferences?: Record<string, any>;
total_orders: number;
total_spent: number;
created_at: string;
updated_at: string;
}
export interface OrderAnalytics {
total_orders: number;
total_revenue: number;
average_order_value: number;
order_completion_rate: number;
delivery_success_rate: number;
customer_satisfaction_score?: number;
popular_products: Array<{
product_name: string;
quantity_sold: number;
revenue: number;
}>;
order_trends: Array<{
date: string;
orders: number;
revenue: number;
}>;
}
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
OrderStatus,
OrderType,
OrderItem,
OrderCreate,
OrderUpdate,
OrderResponse,
Customer,
OrderAnalytics,
OrderFilters,
CustomerFilters,
OrderTrendsParams,
OrderTrendData
} from '../../types/orders.types';
class OrdersService {
private readonly baseUrl = '/orders';
// Order management
async getOrders(params?: {
page?: number;
size?: number;
status?: OrderStatus;
order_type?: OrderType;
customer_id?: string;
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<{ items: OrderResponse[]; total: number; page: number; size: number; pages: number }>> {
async getOrders(params?: OrderFilters): Promise<ApiResponse<{ items: OrderResponse[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
@@ -179,11 +70,7 @@ class OrdersService {
}
// Customer management
async getCustomers(params?: {
page?: number;
size?: number;
search?: string;
}): Promise<ApiResponse<{ items: Customer[]; total: number; page: number; size: number; pages: number }>> {
async getCustomers(params?: CustomerFilters): Promise<ApiResponse<{ items: Customer[]; total: number; page: number; size: number; pages: number }>> {
const queryParams = new URLSearchParams();
if (params) {
@@ -232,16 +119,7 @@ class OrdersService {
return apiClient.get(url);
}
async getOrderTrends(params?: {
start_date?: string;
end_date?: string;
granularity?: 'hourly' | 'daily' | 'weekly' | 'monthly';
}): Promise<ApiResponse<Array<{
period: string;
orders: number;
revenue: number;
avg_order_value: number;
}>>> {
async getOrderTrends(params?: OrderTrendsParams): Promise<ApiResponse<OrderTrendData[]>> {
const queryParams = new URLSearchParams();
if (params) {

View File

@@ -1,126 +1,28 @@
import { apiClient, ApiResponse } from './client';
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
SupplierCreate,
SupplierUpdate,
SupplierResponse,
SupplierSummary,
SupplierSearchParams,
SupplierApproval,
SupplierStatistics,
PurchaseOrderCreate,
PurchaseOrderUpdate,
PurchaseOrderResponse,
PurchaseOrderStatus,
DeliveryCreate,
DeliveryResponse,
DeliveryStatus,
DeliveryReceiptConfirmation,
Supplier
} from '../../types/suppliers.types';
// Enums
export enum PurchaseOrderStatus {
DRAFT = 'draft',
PENDING = 'pending',
APPROVED = 'approved',
SENT = 'sent',
PARTIALLY_RECEIVED = 'partially_received',
RECEIVED = 'received',
CANCELLED = 'cancelled',
}
export enum DeliveryStatus {
SCHEDULED = 'scheduled',
IN_TRANSIT = 'in_transit',
DELIVERED = 'delivered',
FAILED = 'failed',
RETURNED = 'returned',
}
// Request/Response Types
export interface PurchaseOrderItem {
ingredient_id: string;
ingredient_name: string;
quantity: number;
unit_price: number;
total_price: number;
notes?: string;
}
export interface PurchaseOrderCreate {
supplier_id: string;
items: PurchaseOrderItem[];
delivery_date?: string;
notes?: string;
priority?: 'low' | 'normal' | 'high' | 'urgent';
}
export interface PurchaseOrderUpdate {
supplier_id?: string;
delivery_date?: string;
notes?: string;
priority?: 'low' | 'normal' | 'high' | 'urgent';
status?: PurchaseOrderStatus;
}
export interface PurchaseOrderResponse {
id: string;
tenant_id: string;
order_number: string;
supplier_id: string;
supplier_name: string;
status: PurchaseOrderStatus;
items: PurchaseOrderItem[];
subtotal: number;
tax_amount: number;
total_amount: number;
delivery_date?: string;
expected_delivery_date?: string;
actual_delivery_date?: string;
notes?: string;
priority: 'low' | 'normal' | 'high' | 'urgent';
created_at: string;
updated_at: string;
created_by: string;
approved_by?: string;
approved_at?: string;
}
export interface Supplier {
id: string;
tenant_id: string;
name: string;
contact_name?: string;
email?: string;
phone?: string;
address: string;
tax_id?: string;
payment_terms?: string;
delivery_terms?: string;
rating?: number;
is_active: boolean;
performance_metrics: {
on_time_delivery_rate: number;
quality_score: number;
total_orders: number;
average_delivery_time: number;
};
created_at: string;
updated_at: string;
}
export interface DeliveryResponse {
id: string;
tenant_id: string;
purchase_order_id: string;
delivery_number: string;
supplier_id: string;
status: DeliveryStatus;
scheduled_date: string;
actual_delivery_date?: string;
delivery_items: Array<{
ingredient_id: string;
ingredient_name: string;
ordered_quantity: number;
delivered_quantity: number;
unit_price: number;
batch_number?: string;
expiration_date?: string;
quality_notes?: string;
}>;
total_items: number;
delivery_notes?: string;
quality_check_notes?: string;
received_by?: string;
created_at: string;
updated_at: string;
}
class ProcurementService {
private readonly baseUrl = '/procurement';
// Purchase Order management
async getPurchaseOrders(params?: {
page?: number;
@@ -141,90 +43,88 @@ class ProcurementService {
}
const url = queryParams.toString()
? `${this.baseUrl}/purchase-orders?${queryParams.toString()}`
: `${this.baseUrl}/purchase-orders`;
? `/purchase-orders?${queryParams.toString()}`
: `/purchase-orders`;
return apiClient.get(url);
}
async getPurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.get(`${this.baseUrl}/purchase-orders/${orderId}`);
return apiClient.get(`/purchase-orders/${orderId}`);
}
async createPurchaseOrder(orderData: PurchaseOrderCreate): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`${this.baseUrl}/purchase-orders`, orderData);
return apiClient.post(`/purchase-orders`, orderData);
}
async updatePurchaseOrder(orderId: string, orderData: PurchaseOrderUpdate): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.put(`${this.baseUrl}/purchase-orders/${orderId}`, orderData);
return apiClient.put(`/purchase-orders/${orderId}`, orderData);
}
async approvePurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`${this.baseUrl}/purchase-orders/${orderId}/approve`);
return apiClient.post(`/purchase-orders/${orderId}/approve`);
}
async sendPurchaseOrder(orderId: string, sendEmail: boolean = true): Promise<ApiResponse<{ message: string; sent_at: string }>> {
return apiClient.post(`${this.baseUrl}/purchase-orders/${orderId}/send`, { send_email: sendEmail });
return apiClient.post(`/purchase-orders/${orderId}/send`, { send_email: sendEmail });
}
async cancelPurchaseOrder(orderId: string, reason?: string): Promise<ApiResponse<PurchaseOrderResponse>> {
return apiClient.post(`${this.baseUrl}/purchase-orders/${orderId}/cancel`, { reason });
return apiClient.post(`/purchase-orders/${orderId}/cancel`, { reason });
}
// Supplier management
async getSuppliers(params?: {
page?: number;
size?: number;
is_active?: boolean;
search?: string;
}): Promise<ApiResponse<{ items: Supplier[]; total: number; page: number; size: number; pages: number }>> {
async getSuppliers(params?: SupplierSearchParams): Promise<ApiResponse<SupplierSummary[]>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
if (params.search_term) queryParams.append('search_term', params.search_term);
if (params.supplier_type) queryParams.append('supplier_type', params.supplier_type);
if (params.status) queryParams.append('status', params.status);
if (params.limit) queryParams.append('limit', params.limit.toString());
if (params.offset) queryParams.append('offset', params.offset.toString());
}
const url = queryParams.toString()
? `${this.baseUrl}/suppliers?${queryParams.toString()}`
: `${this.baseUrl}/suppliers`;
? `/suppliers?${queryParams.toString()}`
: `/suppliers`;
return apiClient.get(url);
}
async getSupplier(supplierId: string): Promise<ApiResponse<Supplier>> {
return apiClient.get(`${this.baseUrl}/suppliers/${supplierId}`);
async getSupplier(supplierId: string): Promise<ApiResponse<SupplierResponse>> {
return apiClient.get(`/suppliers/${supplierId}`);
}
async createSupplier(supplierData: Omit<Supplier, 'id' | 'tenant_id' | 'performance_metrics' | 'created_at' | 'updated_at'>): Promise<ApiResponse<Supplier>> {
return apiClient.post(`${this.baseUrl}/suppliers`, supplierData);
async createSupplier(supplierData: SupplierCreate): Promise<ApiResponse<SupplierResponse>> {
return apiClient.post(`/suppliers`, supplierData);
}
async updateSupplier(supplierId: string, supplierData: Partial<Supplier>): Promise<ApiResponse<Supplier>> {
return apiClient.put(`${this.baseUrl}/suppliers/${supplierId}`, supplierData);
async updateSupplier(supplierId: string, supplierData: SupplierUpdate): Promise<ApiResponse<SupplierResponse>> {
return apiClient.put(`/suppliers/${supplierId}`, supplierData);
}
async deleteSupplier(supplierId: string): Promise<ApiResponse<{ message: string }>> {
return apiClient.delete(`${this.baseUrl}/suppliers/${supplierId}`);
return apiClient.delete(`/suppliers/${supplierId}`);
}
async getSupplierPerformance(supplierId: string): Promise<ApiResponse<{
supplier: Supplier;
performance_history: Array<{
month: string;
on_time_delivery_rate: number;
quality_score: number;
order_count: number;
total_value: number;
}>;
recent_deliveries: DeliveryResponse[];
}>> {
return apiClient.get(`${this.baseUrl}/suppliers/${supplierId}/performance`);
async approveSupplier(supplierId: string, approval: SupplierApproval): Promise<ApiResponse<SupplierResponse>> {
return apiClient.post(`/suppliers/${supplierId}/approve`, approval);
}
async getSupplierStatistics(): Promise<ApiResponse<SupplierStatistics>> {
return apiClient.get(`/suppliers/statistics`);
}
async getActiveSuppliers(): Promise<ApiResponse<SupplierSummary[]>> {
return apiClient.get(`/suppliers/active`);
}
async getTopSuppliers(limit: number = 10): Promise<ApiResponse<SupplierSummary[]>> {
return apiClient.get(`/suppliers/top?limit=${limit}`);
}
// Delivery management
async getDeliveries(params?: {
page?: number;
@@ -246,131 +146,32 @@ class ProcurementService {
}
const url = queryParams.toString()
? `${this.baseUrl}/deliveries?${queryParams.toString()}`
: `${this.baseUrl}/deliveries`;
? `/deliveries?${queryParams.toString()}`
: `/deliveries`;
return apiClient.get(url);
}
async getDelivery(deliveryId: string): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.get(`${this.baseUrl}/deliveries/${deliveryId}`);
return apiClient.get(`/deliveries/${deliveryId}`);
}
async receiveDelivery(deliveryId: string, deliveryData: {
delivered_items: Array<{
ingredient_id: string;
delivered_quantity: number;
batch_number?: string;
expiration_date?: string;
quality_notes?: string;
}>;
delivery_notes?: string;
quality_check_notes?: string;
}): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.post(`${this.baseUrl}/deliveries/${deliveryId}/receive`, deliveryData);
async createDelivery(deliveryData: DeliveryCreate): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.post(`/deliveries`, deliveryData);
}
async reportDeliveryIssue(deliveryId: string, issue: {
issue_type: 'late_delivery' | 'quality_issue' | 'quantity_mismatch' | 'damaged_goods' | 'other';
description: string;
affected_items?: string[];
severity: 'low' | 'medium' | 'high';
}): Promise<ApiResponse<{ message: string; issue_id: string }>> {
return apiClient.post(`${this.baseUrl}/deliveries/${deliveryId}/report-issue`, issue);
async updateDelivery(deliveryId: string, deliveryData: Partial<DeliveryCreate>): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.put(`/deliveries/${deliveryId}`, deliveryData);
}
// Analytics and reporting
async getProcurementAnalytics(params?: {
start_date?: string;
end_date?: string;
supplier_id?: string;
}): Promise<ApiResponse<{
total_purchase_value: number;
total_orders: number;
average_order_value: number;
on_time_delivery_rate: number;
quality_score: number;
cost_savings: number;
top_suppliers: Array<{
supplier_name: string;
total_value: number;
order_count: number;
performance_score: number;
}>;
spending_trends: Array<{
month: string;
total_spending: number;
order_count: number;
}>;
}>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/analytics?${queryParams.toString()}`
: `${this.baseUrl}/analytics`;
return apiClient.get(url);
async updateDeliveryStatus(deliveryId: string, status: DeliveryStatus, notes?: string): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.put(`/deliveries/${deliveryId}/status`, { status, notes });
}
async getSpendingByCategory(params?: {
start_date?: string;
end_date?: string;
}): Promise<ApiResponse<Array<{
category: string;
total_spending: number;
percentage: number;
trend: 'up' | 'down' | 'stable';
}>>> {
const queryParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString());
}
});
}
const url = queryParams.toString()
? `${this.baseUrl}/spending/by-category?${queryParams.toString()}`
: `${this.baseUrl}/spending/by-category`;
return apiClient.get(url);
async confirmDeliveryReceipt(deliveryId: string, confirmation: DeliveryReceiptConfirmation): Promise<ApiResponse<DeliveryResponse>> {
return apiClient.post(`/deliveries/${deliveryId}/confirm-receipt`, confirmation);
}
// Automated procurement
async getReorderSuggestions(): Promise<ApiResponse<Array<{
ingredient_id: string;
ingredient_name: string;
current_stock: number;
reorder_point: number;
suggested_quantity: number;
preferred_supplier: {
id: string;
name: string;
last_price: number;
lead_time_days: number;
};
urgency: 'low' | 'medium' | 'high' | 'critical';
}>>> {
return apiClient.get(`${this.baseUrl}/reorder-suggestions`);
}
async createReorderFromSuggestions(suggestions: Array<{
ingredient_id: string;
supplier_id: string;
quantity: number;
}>): Promise<ApiResponse<PurchaseOrderResponse[]>> {
return apiClient.post(`${this.baseUrl}/auto-reorder`, { suggestions });
}
// Utility methods
getPurchaseOrderStatusOptions(): { value: PurchaseOrderStatus; label: string; color: string }[] {
@@ -395,14 +196,6 @@ class ProcurementService {
];
}
getPriorityOptions(): { value: string; label: string; color: string }[] {
return [
{ value: 'low', label: 'Low', color: 'gray' },
{ value: 'normal', label: 'Normal', color: 'blue' },
{ value: 'high', label: 'High', color: 'orange' },
{ value: 'urgent', label: 'Urgent', color: 'red' },
];
}
}
export const procurementService = new ProcurementService();

View File

@@ -1,27 +1,21 @@
import { apiClient, ApiResponse } from './client';
import {
ProductionBatchStatus,
QualityCheckStatus,
ProductionPriority,
ProductionBatch,
ProductionSchedule,
QualityCheck,
Recipe
} from '../../types/production.types';
// Enums
export enum ProductionBatchStatus {
PLANNED = 'planned',
IN_PROGRESS = 'in_progress',
COMPLETED = 'completed',
CANCELLED = 'cancelled',
ON_HOLD = 'on_hold',
}
export enum QualityCheckStatus {
PASSED = 'passed',
FAILED = 'failed',
PENDING = 'pending',
REQUIRES_REVIEW = 'requires_review',
}
export enum ProductionPriority {
LOW = 'low',
NORMAL = 'normal',
HIGH = 'high',
URGENT = 'urgent',
}
// Type aliases for service compatibility
type ProductionBatchCreate = Omit<ProductionBatch, 'id' | 'tenant_id' | 'created_at' | 'updated_at'>;
type ProductionBatchUpdate = Partial<ProductionBatchCreate>;
type ProductionBatchResponse = ProductionBatch;
type ProductionScheduleEntry = ProductionSchedule;
type QualityCheckCreate = Omit<QualityCheck, 'id' | 'tenant_id' | 'created_at' | 'updated_at'>;
type QualityCheckResponse = QualityCheck;
// Request/Response Types
export interface ProductionBatchCreate {

View File

@@ -2,69 +2,76 @@
* Training service for ML model training operations
*/
import { ApiClient } from './client';
import { apiClient } from './client';
import { ApiResponse } from '../../types/api.types';
import {
TrainingJob,
TrainingJobCreate,
TrainingJobUpdate
} from '../../types/training.types';
export interface TrainingJob {
id: string;
model_id: string;
status: 'pending' | 'running' | 'completed' | 'failed';
progress: number;
started_at?: string;
completed_at?: string;
error_message?: string;
parameters: Record<string, any>;
metrics?: Record<string, number>;
}
export class TrainingService {
private getTenantId(): string {
const tenantStorage = localStorage.getItem('tenant-storage');
if (tenantStorage) {
try {
const { state } = JSON.parse(tenantStorage);
return state?.currentTenant?.id;
} catch {
return '';
}
}
return '';
}
export interface TrainingJobCreate {
model_id: string;
parameters?: Record<string, any>;
}
export interface TrainingJobUpdate {
parameters?: Record<string, any>;
}
export class TrainingService extends ApiClient {
constructor() {
super('/ml/training');
private getBaseUrl(): string {
const tenantId = this.getTenantId();
return `/tenants/${tenantId}/training`;
}
async getTrainingJobs(modelId?: string): Promise<ApiResponse<TrainingJob[]>> {
const params = modelId ? { model_id: modelId } : {};
return this.get('/', params);
const queryParams = new URLSearchParams();
if (params.model_id) {
queryParams.append('model_id', params.model_id);
}
const url = queryParams.toString()
? `${this.getBaseUrl()}/jobs?${queryParams.toString()}`
: `${this.getBaseUrl()}/jobs`;
return apiClient.get(url);
}
async getTrainingJob(id: string): Promise<ApiResponse<TrainingJob>> {
return this.get(`/${id}`);
return apiClient.get(`${this.getBaseUrl()}/jobs/${id}`);
}
async createTrainingJob(data: TrainingJobCreate): Promise<ApiResponse<TrainingJob>> {
return this.post('/', data);
return apiClient.post(`${this.getBaseUrl()}/jobs`, data);
}
async updateTrainingJob(id: string, data: TrainingJobUpdate): Promise<ApiResponse<TrainingJob>> {
return this.put(`/${id}`, data);
return apiClient.put(`${this.getBaseUrl()}/jobs/${id}`, data);
}
async deleteTrainingJob(id: string): Promise<ApiResponse<void>> {
return this.delete(`/${id}`);
return apiClient.delete(`${this.getBaseUrl()}/jobs/${id}`);
}
async startTraining(id: string): Promise<ApiResponse<TrainingJob>> {
return this.post(`/${id}/start`);
return apiClient.post(`${this.getBaseUrl()}/jobs/${id}/start`);
}
async stopTraining(id: string): Promise<ApiResponse<TrainingJob>> {
return this.post(`/${id}/stop`);
return apiClient.post(`${this.getBaseUrl()}/jobs/${id}/stop`);
}
async getTrainingLogs(id: string): Promise<ApiResponse<string[]>> {
return this.get(`/${id}/logs`);
return apiClient.get(`${this.getBaseUrl()}/jobs/${id}/logs`);
}
async getTrainingMetrics(id: string): Promise<ApiResponse<Record<string, number>>> {
return this.get(`/${id}/metrics`);
return apiClient.get(`${this.getBaseUrl()}/jobs/${id}/metrics`);
}
}
}
export const trainingService = new TrainingService();

View File

@@ -75,7 +75,8 @@ export const useTenantStore = create<TenantState>()(
set({ isLoading: true, error: null });
// Get current user to determine user ID
const user = useAuthUser.getState?.() || JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state?.user;
const authState = JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state;
const user = authState?.user;
if (!user?.id) {
throw new Error('User not authenticated');
@@ -130,7 +131,8 @@ export const useTenantStore = create<TenantState>()(
if (!currentTenant) return false;
// Get user to determine role within this tenant
const user = useAuthUser.getState?.() || JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state?.user;
const authState = JSON.parse(localStorage.getItem('auth-storage') || '{}')?.state;
const user = authState?.user;
// Admin role has all permissions
if (user?.role === 'admin') return true;

View File

@@ -1,40 +1,45 @@
// API and common response types
/**
* API Response Types - Matching actual backend implementation
*/
// Base API response structure
// Standard FastAPI response structure
export interface ApiResponse<T = any> {
data: T;
success: boolean;
data?: T;
success?: boolean;
message?: string;
detail?: string;
error?: string;
timestamp?: string;
}
// Error response structure
// FastAPI error response structure
export interface ApiError {
success: false;
error: ErrorDetail;
timestamp: string;
request_id?: string;
detail: string | ValidationError[];
type?: string;
}
export interface ErrorDetail {
message: string;
code?: string;
field?: string;
details?: Record<string, any>;
export interface ValidationError {
loc: (string | number)[];
msg: string;
type: string;
ctx?: Record<string, any>;
}
// Pagination types
// Pagination types (used by backend services)
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
size: number;
pages: number;
has_next: boolean;
has_prev: boolean;
items?: T[];
records?: T[]; // Some endpoints use 'records'
data?: T[]; // Some endpoints use 'data'
total?: number;
page?: number;
size?: number;
limit?: number;
offset?: number;
pages?: number;
has_next?: boolean;
has_prev?: boolean;
}
// Query parameters for API requests
export interface PaginationParams {
page?: number;
size?: number;
@@ -42,368 +47,33 @@ export interface PaginationParams {
offset?: number;
}
// Sorting and filtering
export interface SortParams {
sort_by?: string;
sort_order?: SortOrder;
order_by?: string;
order?: 'asc' | 'desc';
}
export interface FilterParams {
search?: string;
search_term?: string;
q?: string;
filters?: Record<string, any>;
[key: string]: any;
}
export interface QueryParams extends PaginationParams, SortParams, FilterParams {}
// File upload types
export interface FileUploadResponse {
file_id: string;
filename: string;
size: number;
mime_type: string;
url?: string;
download_url?: string;
expires_at?: string;
}
export interface FileUploadProgress {
loaded: number;
total: number;
percentage: number;
speed?: number;
estimated_time?: number;
}
export interface BulkOperationResponse {
total: number;
processed: number;
successful: number;
failed: number;
errors: BulkOperationError[];
warnings?: BulkOperationWarning[];
}
export interface BulkOperationError {
index: number;
item_id?: string;
error_code: string;
message: string;
field?: string;
details?: Record<string, any>;
}
export interface BulkOperationWarning {
index: number;
item_id?: string;
warning_code: string;
message: string;
suggestion?: string;
}
// Task/Job status types
// Task/Job status (used in ML training and other async operations)
export interface TaskStatus {
id: string;
task_id: string;
status: TaskStatusType;
progress: number;
message: string;
progress?: number;
message?: string;
result?: any;
error?: string;
created_at: string;
started_at?: string;
completed_at?: string;
estimated_completion?: string;
}
export interface LongRunningTask {
id: string;
name: string;
type: TaskType;
status: TaskStatusType;
progress: number;
total_steps?: number;
current_step?: number;
step_description?: string;
result_data?: any;
error_details?: TaskError;
created_by: string;
created_at: string;
updated_at: string;
}
export interface TaskError {
code: string;
message: string;
stack_trace?: string;
context?: Record<string, any>;
}
// Health check types
export interface HealthCheckResponse {
status: HealthStatus;
timestamp: string;
version: string;
uptime: number;
services: ServiceHealth[];
system_info?: SystemInfo;
}
export interface ServiceHealth {
name: string;
status: HealthStatus;
response_time_ms?: number;
error_message?: string;
dependencies?: ServiceHealth[];
}
export interface SystemInfo {
memory_usage: MemoryUsage;
cpu_usage?: number;
disk_usage?: DiskUsage;
network_stats?: NetworkStats;
}
export interface MemoryUsage {
used: number;
available: number;
total: number;
percentage: number;
}
export interface DiskUsage {
used: number;
available: number;
total: number;
percentage: number;
}
export interface NetworkStats {
bytes_sent: number;
bytes_received: number;
packets_sent: number;
packets_received: number;
}
// Audit and logging types
export interface AuditLog {
id: string;
user_id: string;
user_email: string;
tenant_id: string;
action: AuditAction;
resource_type: ResourceType;
resource_id: string;
changes?: AuditChange[];
metadata?: Record<string, any>;
ip_address: string;
user_agent: string;
timestamp: string;
}
export interface AuditChange {
field: string;
old_value?: any;
new_value?: any;
change_type: ChangeType;
}
// Cache types
export interface CacheInfo {
key: string;
size: number;
hits: number;
misses: number;
hit_rate: number;
created_at: string;
expires_at?: string;
last_accessed: string;
}
export interface CacheStats {
total_keys: number;
total_size: number;
hit_rate: number;
memory_usage: number;
cache_by_type: Record<string, CacheTypeStats>;
}
export interface CacheTypeStats {
count: number;
size: number;
hit_rate: number;
avg_ttl: number;
}
// Webhook types
export interface WebhookConfig {
id: string;
tenant_id: string;
name: string;
url: string;
events: WebhookEvent[];
secret?: string;
is_active: boolean;
retry_count: number;
timeout_seconds: number;
headers?: Record<string, string>;
created_at: string;
updated_at: string;
}
export interface WebhookDelivery {
id: string;
webhook_id: string;
event_type: WebhookEvent;
payload: Record<string, any>;
status: DeliveryStatus;
http_status?: number;
response_body?: string;
error_message?: string;
attempts: number;
delivered_at?: string;
created_at: string;
next_retry_at?: string;
}
// Rate limiting types
export interface RateLimit {
requests_per_minute: number;
requests_per_hour: number;
requests_per_day: number;
current_usage: RateLimitUsage;
reset_time: string;
}
export interface RateLimitUsage {
minute: number;
hour: number;
day: number;
percentage_used: number;
}
// Search types
export interface SearchRequest {
query: string;
filters?: SearchFilter[];
sort?: SearchSort[];
facets?: string[];
highlight?: SearchHighlight;
size?: number;
from?: number;
}
export interface SearchFilter {
field: string;
operator: FilterOperator;
value: any;
values?: any[];
}
export interface SearchSort {
field: string;
order: SortOrder;
}
export interface SearchHighlight {
fields: string[];
pre_tag?: string;
post_tag?: string;
}
export interface SearchResponse<T> {
results: T[];
total: number;
facets?: SearchFacet[];
highlights?: Record<string, string[]>;
suggestions?: SearchSuggestion[];
took_ms: number;
}
export interface SearchFacet {
field: string;
values: FacetValue[];
}
export interface FacetValue {
value: any;
count: number;
selected: boolean;
}
export interface SearchSuggestion {
text: string;
highlighted: string;
score: number;
}
// Export/Import types
export interface ExportRequest {
format: ExportFormat;
filters?: Record<string, any>;
fields?: string[];
include_metadata?: boolean;
compression?: CompressionType;
}
export interface ExportResponse {
export_id: string;
status: ExportStatus;
download_url?: string;
file_size?: number;
expires_at?: string;
created_at: string;
}
export interface ImportRequest {
file_url?: string;
file_data?: string;
format: ImportFormat;
options?: ImportOptions;
mapping?: Record<string, string>;
}
export interface ImportOptions {
skip_validation?: boolean;
update_existing?: boolean;
batch_size?: number;
dry_run?: boolean;
}
export interface ImportResponse {
import_id: string;
status: ImportStatus;
total_records?: number;
processed_records?: number;
successful_records?: number;
failed_records?: number;
errors?: ImportError[];
warnings?: ImportWarning[];
created_at: string;
}
export interface ImportError {
row: number;
field?: string;
value?: any;
error_code: string;
message: string;
}
export interface ImportWarning {
row: number;
field?: string;
value?: any;
warning_code: string;
message: string;
}
// Enums
export enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}
export enum TaskStatusType {
@@ -411,153 +81,69 @@ export enum TaskStatusType {
RUNNING = 'running',
COMPLETED = 'completed',
FAILED = 'failed',
CANCELLED = 'cancelled',
PAUSED = 'paused',
CANCELLED = 'cancelled'
}
export enum TaskType {
DATA_IMPORT = 'data_import',
DATA_EXPORT = 'data_export',
REPORT_GENERATION = 'report_generation',
MODEL_TRAINING = 'model_training',
DATA_PROCESSING = 'data_processing',
BULK_OPERATION = 'bulk_operation',
SYNC_OPERATION = 'sync_operation',
// Health check types (used by monitoring endpoints)
export interface HealthCheckResponse {
status: 'healthy' | 'unhealthy' | 'degraded';
service: string;
version: string;
timestamp: string;
dependencies?: ServiceHealth[];
}
export enum HealthStatus {
HEALTHY = 'healthy',
DEGRADED = 'degraded',
UNHEALTHY = 'unhealthy',
UNKNOWN = 'unknown',
export interface ServiceHealth {
name: string;
status: 'healthy' | 'unhealthy' | 'degraded';
response_time?: number;
error?: string;
}
export enum AuditAction {
CREATE = 'create',
READ = 'read',
UPDATE = 'update',
DELETE = 'delete',
LOGIN = 'login',
LOGOUT = 'logout',
EXPORT = 'export',
IMPORT = 'import',
// File upload types
export interface FileUploadResponse {
file_id: string;
filename: string;
size: number;
content_type: string;
url?: string;
}
export enum ResourceType {
USER = 'user',
TENANT = 'tenant',
INGREDIENT = 'ingredient',
STOCK = 'stock',
PRODUCTION_BATCH = 'production_batch',
SALES_RECORD = 'sales_record',
FORECAST = 'forecast',
ORDER = 'order',
SUPPLIER = 'supplier',
RECIPE = 'recipe',
NOTIFICATION = 'notification',
// Bulk operation response
export interface BulkOperationResponse {
total: number;
processed: number;
successful: number;
failed: number;
errors?: BulkOperationError[];
}
export enum ChangeType {
CREATED = 'created',
UPDATED = 'updated',
DELETED = 'deleted',
ARCHIVED = 'archived',
RESTORED = 'restored',
export interface BulkOperationError {
index: number;
error: string;
details?: any;
}
export enum WebhookEvent {
USER_CREATED = 'user.created',
USER_UPDATED = 'user.updated',
USER_DELETED = 'user.deleted',
INVENTORY_LOW_STOCK = 'inventory.low_stock',
PRODUCTION_COMPLETED = 'production.completed',
ORDER_CREATED = 'order.created',
ORDER_UPDATED = 'order.updated',
FORECAST_GENERATED = 'forecast.generated',
ALERT_TRIGGERED = 'alert.triggered',
// Common enums
export enum SortOrder {
ASC = 'asc',
DESC = 'desc'
}
export enum DeliveryStatus {
PENDING = 'pending',
DELIVERED = 'delivered',
FAILED = 'failed',
RETRYING = 'retrying',
CANCELLED = 'cancelled',
}
export enum FilterOperator {
EQUALS = 'eq',
NOT_EQUALS = 'ne',
GREATER_THAN = 'gt',
GREATER_THAN_OR_EQUAL = 'gte',
LESS_THAN = 'lt',
LESS_THAN_OR_EQUAL = 'lte',
CONTAINS = 'contains',
STARTS_WITH = 'starts_with',
ENDS_WITH = 'ends_with',
IN = 'in',
NOT_IN = 'not_in',
IS_NULL = 'is_null',
IS_NOT_NULL = 'is_not_null',
BETWEEN = 'between',
}
export enum ExportFormat {
CSV = 'csv',
EXCEL = 'excel',
JSON = 'json',
PDF = 'pdf',
XML = 'xml',
}
export enum ImportFormat {
CSV = 'csv',
EXCEL = 'excel',
JSON = 'json',
XML = 'xml',
}
export enum CompressionType {
NONE = 'none',
ZIP = 'zip',
GZIP = 'gzip',
}
export enum ExportStatus {
PENDING = 'pending',
PROCESSING = 'processing',
COMPLETED = 'completed',
FAILED = 'failed',
EXPIRED = 'expired',
}
export enum ImportStatus {
PENDING = 'pending',
PROCESSING = 'processing',
COMPLETED = 'completed',
FAILED = 'failed',
CANCELLED = 'cancelled',
PARTIALLY_COMPLETED = 'partially_completed',
}
// Utility types for HTTP methods
// HTTP methods
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
// Generic type for API endpoints
export type ApiEndpoint<TRequest = any, TResponse = any> = {
method: HttpMethod;
path: string;
request?: TRequest;
response: TResponse;
};
// Type guards
export const isApiError = (obj: any): obj is ApiError => {
return obj && obj.success === false && obj.error && typeof obj.error.message === 'string';
return obj && typeof obj.detail === 'string';
};
export const isPaginatedResponse = <T>(obj: any): obj is PaginatedResponse<T> => {
return obj && Array.isArray(obj.items) && typeof obj.total === 'number';
return obj && (
Array.isArray(obj.items) ||
Array.isArray(obj.records) ||
Array.isArray(obj.data)
);
};
export const isTaskStatus = (obj: any): obj is TaskStatus => {

View File

@@ -1,39 +1,23 @@
// Authentication related types - Updated to match backend exactly
export interface User {
id: string;
email: string;
full_name: string; // Backend uses full_name, not name
is_active: boolean;
is_verified: boolean;
created_at: string; // ISO format datetime string
last_login?: string;
phone?: string;
language?: string;
timezone?: string;
tenant_id?: string;
role?: string; // Backend uses string, not enum
}
/**
* Authentication Types - Matching backend schemas exactly
* Based on services/auth/app/schemas/auth.py
*/
// ============================================================================
// REQUEST TYPES (Frontend -> Backend)
// ============================================================================
export interface UserRegistration {
email: string;
password: string;
full_name: string;
tenant_name?: string; // Optional in backend
role?: string; // Backend uses string, defaults to "user"
tenant_name?: string;
role?: 'user' | 'admin' | 'manager';
}
export interface UserLogin {
email: string;
password: string;
remember_me?: boolean;
}
export interface TokenResponse {
access_token: string;
refresh_token?: string;
token_type: string; // defaults to "bearer"
expires_in: number; // seconds, defaults to 3600
user?: User;
}
export interface RefreshTokenRequest {
@@ -54,6 +38,64 @@ export interface PasswordResetConfirm {
new_password: string;
}
// ============================================================================
// RESPONSE TYPES (Backend -> Frontend)
// ============================================================================
export interface UserData {
id: string;
email: string;
full_name: string;
is_active: boolean;
is_verified: boolean;
created_at: string;
tenant_id?: string;
role?: string;
}
export interface TokenResponse {
access_token: string;
refresh_token?: string;
token_type: string; // defaults to "bearer"
expires_in: number; // seconds, defaults to 3600
user?: UserData;
}
// ============================================================================
// JWT TOKEN CLAIMS (Internal)
// ============================================================================
export interface TokenClaims {
sub: string; // user ID
email: string;
full_name: string;
user_id: string;
is_verified: boolean;
tenant_id?: string;
role?: string;
type: 'access' | 'refresh';
iat: number; // issued at
exp: number; // expires at
iss: string; // issuer
}
// ============================================================================
// FRONTEND STATE TYPES
// ============================================================================
export interface AuthState {
isAuthenticated: boolean;
user: UserData | null;
token: string | null;
refreshToken: string | null;
loading: boolean;
error: string | null;
}
// ============================================================================
// AUTH SERVICE SPECIFIC TYPES
// ============================================================================
export interface TokenVerification {
valid: boolean;
user_id?: string;
@@ -62,27 +104,27 @@ export interface TokenVerification {
message?: string;
}
export interface UserResponse extends UserData {
last_login?: string;
phone?: string;
language?: string;
timezone?: string;
}
export interface UserUpdate {
full_name?: string;
phone?: string;
language?: string;
timezone?: string;
avatar_url?: string;
}
export interface AuthState {
isAuthenticated: boolean;
user: User | null;
token: string | null;
refreshToken: string | null;
loading: boolean;
error: string | null;
}
// ============================================================================
// FORM DATA TYPES (Frontend UI)
// ============================================================================
export interface LoginFormData {
email: string;
password: string;
remember_me: boolean;
}
export interface RegisterFormData {
@@ -90,17 +132,7 @@ export interface RegisterFormData {
password: string;
confirmPassword: string;
full_name: string;
tenant_name?: string; // Optional to match backend
phone?: string;
acceptTerms: boolean;
}
export interface ProfileFormData {
full_name: string;
email: string;
phone?: string;
language: string;
timezone: string;
tenant_name?: string;
}
export interface PasswordChangeFormData {
@@ -109,142 +141,40 @@ export interface PasswordChangeFormData {
confirm_password: string;
}
export interface EmailVerificationRequest {
email: string;
}
export interface EmailVerificationConfirm {
token: string;
}
export interface UserContext {
user_id: string;
email: string;
tenant_id?: string;
roles: string[];
is_verified: boolean;
permissions: string[];
}
export interface TokenClaims {
sub: string;
email: string;
full_name: string;
user_id: string;
is_verified: boolean;
tenant_id?: string;
iat: number;
exp: number;
iss: string;
roles?: string[];
permissions?: string[];
}
// ============================================================================
// UTILITY TYPES
// ============================================================================
export interface AuthError {
code: string;
message: string;
field?: string;
detail: string;
type?: string;
}
export interface MFASetup {
enabled: boolean;
secret?: string;
backup_codes?: string[];
qr_code?: string;
}
// Keep User as alias for UserData for backward compatibility
export interface User extends UserData {}
export interface MFAVerification {
token: string;
backup_code?: string;
}
// ============================================================================
// ENUMS
// ============================================================================
export interface SessionInfo {
id: string;
user_id: string;
ip_address: string;
user_agent: string;
created_at: string;
last_activity: string;
is_current: boolean;
location?: string;
device_type?: string;
}
export interface OAuthProvider {
name: string;
display_name: string;
icon: string;
color: string;
enabled: boolean;
}
// Enums - Simplified to match backend
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
MANAGER = 'manager',
MANAGER = 'manager'
}
export enum AuthProvider {
EMAIL = 'email',
GOOGLE = 'google',
MICROSOFT = 'microsoft',
APPLE = 'apple',
}
// ============================================================================
// TYPE GUARDS
// ============================================================================
export enum Permission {
// User management
USER_READ = 'user:read',
USER_WRITE = 'user:write',
USER_DELETE = 'user:delete',
// Inventory
INVENTORY_READ = 'inventory:read',
INVENTORY_WRITE = 'inventory:write',
INVENTORY_DELETE = 'inventory:delete',
// Production
PRODUCTION_READ = 'production:read',
PRODUCTION_WRITE = 'production:write',
PRODUCTION_DELETE = 'production:delete',
// Sales
SALES_READ = 'sales:read',
SALES_WRITE = 'sales:write',
SALES_DELETE = 'sales:delete',
// Forecasting
FORECASTING_READ = 'forecasting:read',
FORECASTING_WRITE = 'forecasting:write',
FORECASTING_DELETE = 'forecasting:delete',
// Orders
ORDERS_READ = 'orders:read',
ORDERS_WRITE = 'orders:write',
ORDERS_DELETE = 'orders:delete',
// Procurement
PROCUREMENT_READ = 'procurement:read',
PROCUREMENT_WRITE = 'procurement:write',
PROCUREMENT_DELETE = 'procurement:delete',
// Settings
SETTINGS_READ = 'settings:read',
SETTINGS_WRITE = 'settings:write',
// Analytics
ANALYTICS_READ = 'analytics:read',
ANALYTICS_EXPORT = 'analytics:export',
// Admin
ADMIN_ALL = 'admin:all',
}
// Type guards
export const isUser = (obj: any): obj is User => {
export const isUser = (obj: any): obj is UserData => {
return obj && typeof obj.id === 'string' && typeof obj.email === 'string';
};
export const isTokenResponse = (obj: any): obj is TokenResponse => {
return obj && typeof obj.access_token === 'string' && typeof obj.token_type === 'string';
};
export const isAuthError = (obj: any): obj is AuthError => {
return obj && typeof obj.detail === 'string';
};

View File

@@ -0,0 +1,258 @@
/**
* External Data Service Types
* Weather, Traffic, and Events data
*/
// ============================================================================
// WEATHER TYPES
// ============================================================================
export interface WeatherData {
id: string;
tenant_id: string;
location_id: string;
date: string;
temperature_avg: number;
temperature_min: number;
temperature_max: number;
humidity: number;
precipitation: number;
wind_speed: number;
condition: string;
description: string;
created_at: string;
}
export interface WeatherCondition {
value: string;
label: string;
impact: 'positive' | 'negative' | 'neutral';
}
// ============================================================================
// TRAFFIC TYPES
// ============================================================================
export interface TrafficData {
id: string;
tenant_id: string;
location_id: string;
date: string;
hour: number;
traffic_level: number;
congestion_index: number;
average_speed: number;
incident_count: number;
created_at: string;
}
export interface TrafficPattern {
period: string;
average_traffic_level: number;
peak_hours: number[];
congestion_patterns: Record<string, number>;
}
// ============================================================================
// EVENTS TYPES
// ============================================================================
export interface EventData {
id: string;
tenant_id: string;
location_id: string;
event_name: string;
event_type: string;
start_date: string;
end_date: string;
expected_attendance?: number;
impact_radius_km?: number;
impact_score: number;
created_at: string;
}
export interface EventType {
value: string;
label: string;
typical_impact: 'positive' | 'negative' | 'neutral';
}
export interface CustomEventCreate {
location_id: string;
event_name: string;
event_type: string;
start_date: string;
end_date: string;
expected_attendance?: number;
impact_radius_km?: number;
impact_score?: number;
}
// ============================================================================
// LOCATION TYPES
// ============================================================================
export interface LocationConfig {
id: string;
tenant_id: string;
name: string;
latitude: number;
longitude: number;
address: string;
city: string;
country: string;
is_primary: boolean;
data_sources: {
weather_enabled: boolean;
traffic_enabled: boolean;
events_enabled: boolean;
};
created_at: string;
updated_at: string;
}
export interface LocationCreate {
name: string;
latitude: number;
longitude: number;
address: string;
city: string;
country?: string;
is_primary?: boolean;
data_sources?: LocationConfig['data_sources'];
}
// ============================================================================
// ANALYTICS TYPES
// ============================================================================
export interface ExternalFactorsImpact {
weather_impact: {
temperature_correlation: number;
precipitation_impact: number;
most_favorable_conditions: string;
};
traffic_impact: {
congestion_correlation: number;
peak_traffic_effect: number;
optimal_traffic_levels: number[];
};
events_impact: {
positive_events: EventData[];
negative_events: EventData[];
average_event_boost: number;
};
}
export interface DataQualityReport {
overall_score: number;
data_sources: Array<{
source: 'weather' | 'traffic' | 'events';
completeness: number;
freshness_hours: number;
reliability_score: number;
last_update: string;
}>;
recommendations: Array<{
priority: 'high' | 'medium' | 'low';
message: string;
action: string;
}>;
}
// ============================================================================
// CONFIGURATION TYPES
// ============================================================================
export interface DataSettings {
auto_refresh_enabled: boolean;
refresh_intervals: {
weather_minutes: number;
traffic_minutes: number;
events_hours: number;
};
data_retention_days: {
weather: number;
traffic: number;
events: number;
};
external_apis: {
weather_provider: string;
traffic_provider: string;
events_provider: string;
};
}
export interface DataSettingsUpdate {
auto_refresh_enabled?: boolean;
refresh_intervals?: {
weather_minutes?: number;
traffic_minutes?: number;
events_hours?: number;
};
data_retention_days?: {
weather?: number;
traffic?: number;
events?: number;
};
}
export interface RefreshInterval {
value: number;
label: string;
suitable_for: string[];
}
// ============================================================================
// QUERY PARAMETER TYPES
// ============================================================================
export interface WeatherDataParams {
location_id?: string;
start_date?: string;
end_date?: string;
page?: number;
size?: number;
}
export interface TrafficDataParams {
location_id?: string;
start_date?: string;
end_date?: string;
hour?: number;
page?: number;
size?: number;
}
export interface TrafficPatternsParams {
days_back?: number;
granularity?: 'hourly' | 'daily';
}
export interface EventsParams {
location_id?: string;
start_date?: string;
end_date?: string;
event_type?: string;
page?: number;
size?: number;
}
export interface ExternalFactorsParams {
location_id?: string;
start_date?: string;
end_date?: string;
}
// ============================================================================
// RESPONSE TYPES
// ============================================================================
export interface RefreshDataResponse {
message: string;
updated_records: number;
}
export interface DeleteResponse {
message: string;
}

View File

@@ -81,21 +81,85 @@ export type {
DataPreprocessing
} from './forecasting.types';
// Suppliers types
export type {
SupplierCreate,
SupplierUpdate,
SupplierResponse,
SupplierSummary,
SupplierSearchParams,
SupplierApproval,
SupplierStatistics,
PurchaseOrderItemCreate,
PurchaseOrderItemResponse,
PurchaseOrderCreate,
PurchaseOrderUpdate,
PurchaseOrderResponse,
DeliveryItemCreate,
DeliveryItemResponse,
DeliveryCreate,
DeliveryResponse,
DeliveryReceiptConfirmation,
PurchaseOrderStatus,
DeliveryStatus,
Supplier
} from './suppliers.types';
// Data types
export type {
WeatherData,
WeatherDataParams,
WeatherCondition,
TrafficData,
TrafficDataParams,
TrafficPattern,
TrafficPatternsParams,
EventData,
EventsParams,
EventType,
CustomEventCreate,
LocationConfig,
LocationCreate,
ExternalFactorsImpact,
ExternalFactorsParams,
DataQualityReport,
DataSettings,
DataSettingsUpdate,
RefreshDataResponse,
DeleteResponse,
RefreshInterval
} from './data.types';
// API and common types
export type {
ApiResponse,
ApiError,
ValidationError,
PaginatedResponse,
PaginationParams,
SortParams,
FilterParams,
QueryParams,
TaskStatus,
HealthCheckResponse
TaskStatusType,
HealthCheckResponse,
ServiceHealth,
FileUploadResponse,
BulkOperationResponse,
SortOrder,
HttpMethod
} from './api.types';
// Re-export commonly used types for convenience
export type {
User,
UserData,
UserLogin,
UserRegistration,
TokenResponse,
AuthState,
AuthError,
UserRole
} from './auth.types';
export type {
@@ -135,6 +199,16 @@ export type {
BatchStatus,
} from './forecasting.types';
export type {
SupplierResponse,
SupplierSummary,
SupplierCreate,
PurchaseOrderResponse,
DeliveryResponse,
PurchaseOrderStatus,
DeliveryStatus,
} from './suppliers.types';
export type {
ApiResponse,
ApiError,

View File

@@ -0,0 +1,166 @@
// Orders service types
export enum OrderStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
IN_PREPARATION = 'in_preparation',
READY = 'ready',
DELIVERED = 'delivered',
CANCELLED = 'cancelled',
}
export enum OrderType {
DINE_IN = 'dine_in',
TAKEAWAY = 'takeaway',
DELIVERY = 'delivery',
CATERING = 'catering',
}
export interface OrderItem {
product_id?: string;
product_name: string;
quantity: number;
unit_price: number;
total_price: number;
notes?: string;
customizations?: Record<string, any>;
}
export interface OrderCreate {
customer_id?: string;
customer_name: string;
customer_email?: string;
customer_phone?: string;
order_type: OrderType;
items: OrderItem[];
special_instructions?: string;
delivery_address?: string;
delivery_date?: string;
delivery_time?: string;
payment_method?: string;
}
export interface OrderUpdate {
status?: OrderStatus;
customer_name?: string;
customer_email?: string;
customer_phone?: string;
special_instructions?: string;
delivery_address?: string;
delivery_date?: string;
delivery_time?: string;
estimated_completion_time?: string;
actual_completion_time?: string;
}
export interface OrderResponse {
id: string;
tenant_id: string;
order_number: string;
customer_id?: string;
customer_name: string;
customer_email?: string;
customer_phone?: string;
order_type: OrderType;
status: OrderStatus;
items: OrderItem[];
subtotal: number;
tax_amount: number;
discount_amount: number;
total_amount: number;
special_instructions?: string;
delivery_address?: string;
delivery_date?: string;
delivery_time?: string;
estimated_completion_time?: string;
actual_completion_time?: string;
payment_method?: string;
payment_status?: string;
created_at: string;
updated_at: string;
created_by?: string;
}
export interface Customer {
id: string;
tenant_id: string;
name: string;
email?: string;
phone?: string;
address?: string;
preferences?: Record<string, any>;
total_orders: number;
total_spent: number;
created_at: string;
updated_at: string;
}
export interface OrderAnalytics {
total_orders: number;
total_revenue: number;
average_order_value: number;
order_completion_rate: number;
delivery_success_rate: number;
customer_satisfaction_score?: number;
popular_products: Array<{
product_name: string;
quantity_sold: number;
revenue: number;
}>;
order_trends: Array<{
date: string;
orders: number;
revenue: number;
}>;
}
// Form data interfaces
export interface OrderFormData extends OrderCreate {}
export interface CustomerFormData {
name: string;
email?: string;
phone?: string;
address?: string;
preferences?: Record<string, any>;
}
// Filter interfaces
export interface OrderFilters {
page?: number;
size?: number;
status?: OrderStatus;
order_type?: OrderType;
customer_id?: string;
start_date?: string;
end_date?: string;
}
export interface CustomerFilters {
page?: number;
size?: number;
search?: string;
}
// Analytics interfaces
export interface OrderTrendsParams {
start_date?: string;
end_date?: string;
granularity?: 'hourly' | 'daily' | 'weekly' | 'monthly';
}
export interface OrderTrendData {
period: string;
orders: number;
revenue: number;
avg_order_value: number;
}
// Type guards
export const isOrderResponse = (obj: any): obj is OrderResponse => {
return obj && typeof obj.id === 'string' && typeof obj.order_number === 'string';
};
export const isCustomer = (obj: any): obj is Customer => {
return obj && typeof obj.id === 'string' && typeof obj.name === 'string';
};

View File

@@ -0,0 +1,379 @@
/**
* Type definitions for Suppliers Service API
* Based on backend schemas from services/suppliers/app/schemas/suppliers.py
*/
// ============================================================================
// ENUMS
// ============================================================================
export enum PurchaseOrderStatus {
DRAFT = 'draft',
PENDING = 'pending',
APPROVED = 'approved',
SENT = 'sent',
PARTIALLY_RECEIVED = 'partially_received',
RECEIVED = 'received',
CANCELLED = 'cancelled',
}
export enum DeliveryStatus {
SCHEDULED = 'scheduled',
IN_TRANSIT = 'in_transit',
DELIVERED = 'delivered',
FAILED = 'failed',
RETURNED = 'returned',
}
// ============================================================================
// SUPPLIER TYPES
// ============================================================================
export interface SupplierCreate {
name: string;
supplier_code?: string;
tax_id?: string;
registration_number?: string;
supplier_type: string;
contact_person?: string;
email?: string;
phone?: string;
mobile?: string;
website?: string;
address_line1?: string;
address_line2?: string;
city?: string;
state_province?: string;
postal_code?: string;
country?: string;
payment_terms?: string;
credit_limit?: number;
currency?: string;
standard_lead_time?: number;
minimum_order_amount?: number;
delivery_area?: string;
notes?: string;
certifications?: any;
business_hours?: any;
specializations?: any;
}
export interface SupplierUpdate {
name?: string;
supplier_code?: string;
tax_id?: string;
registration_number?: string;
supplier_type?: string;
status?: string;
contact_person?: string;
email?: string;
phone?: string;
mobile?: string;
website?: string;
address_line1?: string;
address_line2?: string;
city?: string;
state_province?: string;
postal_code?: string;
country?: string;
payment_terms?: string;
credit_limit?: number;
currency?: string;
standard_lead_time?: number;
minimum_order_amount?: number;
delivery_area?: string;
notes?: string;
certifications?: any;
business_hours?: any;
specializations?: any;
}
export interface SupplierResponse {
id: string;
tenant_id: string;
name: string;
supplier_code?: string;
tax_id?: string;
registration_number?: string;
supplier_type: string;
status: string;
contact_person?: string;
email?: string;
phone?: string;
mobile?: string;
website?: string;
address_line1?: string;
address_line2?: string;
city?: string;
state_province?: string;
postal_code?: string;
country?: string;
payment_terms: string;
credit_limit?: number;
currency: string;
standard_lead_time: number;
minimum_order_amount?: number;
delivery_area?: string;
quality_rating?: number;
delivery_rating?: number;
total_orders: number;
total_amount: number;
approved_by?: string;
approved_at?: string;
rejection_reason?: string;
notes?: string;
certifications?: any;
business_hours?: any;
specializations?: any;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
}
export interface SupplierSummary {
id: string;
name: string;
supplier_code?: string;
supplier_type: string;
status: string;
contact_person?: string;
email?: string;
phone?: string;
city?: string;
country?: string;
quality_rating?: number;
delivery_rating?: number;
total_orders: number;
total_amount: number;
created_at: string;
}
export interface SupplierSearchParams {
search_term?: string;
supplier_type?: string;
status?: string;
limit?: number;
offset?: number;
}
export interface SupplierApproval {
action: 'approve' | 'reject';
notes?: string;
}
export interface SupplierStatistics {
total_suppliers: number;
active_suppliers: number;
pending_suppliers: number;
avg_quality_rating: number;
avg_delivery_rating: number;
total_spend: number;
}
// ============================================================================
// PURCHASE ORDER TYPES
// ============================================================================
export interface PurchaseOrderItemCreate {
inventory_product_id: string;
product_code?: string;
ordered_quantity: number;
unit_of_measure: string;
unit_price: number;
quality_requirements?: string;
item_notes?: string;
}
export interface PurchaseOrderItemResponse {
id: string;
tenant_id: string;
purchase_order_id: string;
price_list_item_id?: string;
inventory_product_id: string;
product_code?: string;
ordered_quantity: number;
unit_of_measure: string;
unit_price: number;
line_total: number;
received_quantity: number;
remaining_quantity: number;
quality_requirements?: string;
item_notes?: string;
created_at: string;
updated_at: string;
}
export interface PurchaseOrderCreate {
supplier_id: string;
reference_number?: string;
priority?: string;
required_delivery_date?: string;
delivery_address?: string;
delivery_instructions?: string;
delivery_contact?: string;
delivery_phone?: string;
tax_amount?: number;
shipping_cost?: number;
discount_amount?: number;
notes?: string;
internal_notes?: string;
terms_and_conditions?: string;
items: PurchaseOrderItemCreate[];
}
export interface PurchaseOrderUpdate {
reference_number?: string;
priority?: string;
required_delivery_date?: string;
estimated_delivery_date?: string;
delivery_address?: string;
delivery_instructions?: string;
delivery_contact?: string;
delivery_phone?: string;
tax_amount?: number;
shipping_cost?: number;
discount_amount?: number;
notes?: string;
internal_notes?: string;
terms_and_conditions?: string;
supplier_reference?: string;
}
export interface PurchaseOrderResponse {
id: string;
tenant_id: string;
supplier_id: string;
po_number: string;
reference_number?: string;
status: PurchaseOrderStatus;
priority: string;
order_date: string;
required_delivery_date?: string;
estimated_delivery_date?: string;
subtotal: number;
tax_amount: number;
shipping_cost: number;
discount_amount: number;
total_amount: number;
currency: string;
delivery_address?: string;
delivery_instructions?: string;
delivery_contact?: string;
delivery_phone?: string;
requires_approval: boolean;
approved_by?: string;
approved_at?: string;
rejection_reason?: string;
sent_to_supplier_at?: string;
supplier_confirmation_date?: string;
supplier_reference?: string;
notes?: string;
internal_notes?: string;
terms_and_conditions?: string;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
supplier?: SupplierSummary;
items?: PurchaseOrderItemResponse[];
}
// ============================================================================
// DELIVERY TYPES
// ============================================================================
export interface DeliveryItemCreate {
purchase_order_item_id: string;
inventory_product_id: string;
ordered_quantity: number;
delivered_quantity: number;
accepted_quantity: number;
rejected_quantity?: number;
batch_lot_number?: string;
expiry_date?: string;
quality_grade?: string;
quality_issues?: string;
rejection_reason?: string;
item_notes?: string;
}
export interface DeliveryItemResponse {
id: string;
tenant_id: string;
delivery_id: string;
purchase_order_item_id: string;
inventory_product_id: string;
ordered_quantity: number;
delivered_quantity: number;
accepted_quantity: number;
rejected_quantity: number;
batch_lot_number?: string;
expiry_date?: string;
quality_grade?: string;
quality_issues?: string;
rejection_reason?: string;
item_notes?: string;
created_at: string;
updated_at: string;
}
export interface DeliveryCreate {
purchase_order_id: string;
supplier_id: string;
supplier_delivery_note?: string;
scheduled_date?: string;
estimated_arrival?: string;
delivery_address?: string;
delivery_contact?: string;
delivery_phone?: string;
carrier_name?: string;
tracking_number?: string;
notes?: string;
items: DeliveryItemCreate[];
}
export interface DeliveryResponse {
id: string;
tenant_id: string;
purchase_order_id: string;
supplier_id: string;
delivery_number: string;
supplier_delivery_note?: string;
status: DeliveryStatus;
scheduled_date?: string;
estimated_arrival?: string;
actual_arrival?: string;
completed_at?: string;
delivery_address?: string;
delivery_contact?: string;
delivery_phone?: string;
carrier_name?: string;
tracking_number?: string;
inspection_passed?: boolean;
inspection_notes?: string;
quality_issues?: any;
received_by?: string;
received_at?: string;
notes?: string;
photos?: any;
created_at: string;
updated_at: string;
created_by: string;
supplier?: SupplierSummary;
items?: DeliveryItemResponse[];
}
export interface DeliveryReceiptConfirmation {
inspection_passed?: boolean;
inspection_notes?: string;
quality_issues?: any;
notes?: string;
}
// ============================================================================
// UTILITY TYPES
// ============================================================================
// Keep legacy Supplier interface for backward compatibility
export interface Supplier extends SupplierResponse {}

View File

@@ -0,0 +1,35 @@
// ML Training service types
export interface TrainingJob {
id: string;
model_id: string;
status: 'pending' | 'running' | 'completed' | 'failed';
progress: number;
started_at?: string;
completed_at?: string;
error_message?: string;
parameters: Record<string, any>;
metrics?: Record<string, number>;
}
export interface TrainingJobCreate {
model_id: string;
parameters?: Record<string, any>;
}
export interface TrainingJobUpdate {
parameters?: Record<string, any>;
}
export enum TrainingJobStatus {
PENDING = 'pending',
RUNNING = 'running',
COMPLETED = 'completed',
FAILED = 'failed'
}
// Form data interfaces
export interface TrainingJobFormData {
model_id: string;
parameters: Record<string, any>;
}