Start integrating the onboarding flow with backend 17
This commit is contained in:
@@ -85,6 +85,7 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
// Use onboarding hooks
|
// Use onboarding hooks
|
||||||
const {
|
const {
|
||||||
processSalesFile,
|
processSalesFile,
|
||||||
|
generateProductSuggestions, // New separated function
|
||||||
createInventoryFromSuggestions,
|
createInventoryFromSuggestions,
|
||||||
importSalesData,
|
importSalesData,
|
||||||
salesProcessing: {
|
salesProcessing: {
|
||||||
@@ -120,8 +121,11 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
const tenantCreatedSuccessfully = tenantCreation.isSuccess;
|
const tenantCreatedSuccessfully = tenantCreation.isSuccess;
|
||||||
const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true;
|
const tenantCreatedInOnboarding = data.bakery?.tenantCreated === true;
|
||||||
|
|
||||||
const result = Boolean(hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding));
|
// If user has already uploaded a file or has processing data, assume tenant is available
|
||||||
|
// This prevents blocking the UI due to temporary state inconsistencies after successful progress
|
||||||
|
const hasProgressData = !!(data.files?.salesData || data.processingResults || data.processingStage);
|
||||||
|
|
||||||
|
const result = Boolean(hasAuth && (hasTenantId || tenantCreatedSuccessfully || tenantCreatedInOnboarding || isAlreadyInStep));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -152,20 +156,55 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
productsLength: products.length,
|
productsLength: products.length,
|
||||||
suggestionsIsArray: Array.isArray(suggestions),
|
suggestionsIsArray: Array.isArray(suggestions),
|
||||||
suggestionsType: typeof suggestions,
|
suggestionsType: typeof suggestions,
|
||||||
|
shouldConvert: suggestions && suggestions.length > 0 && products.length === 0
|
||||||
});
|
});
|
||||||
|
|
||||||
if (suggestions && suggestions.length > 0 && products.length === 0) {
|
if (suggestions && Array.isArray(suggestions) && suggestions.length > 0 && products.length === 0) {
|
||||||
console.log('✅ Converting suggestions to products and setting stage to review');
|
console.log('✅ Converting suggestions to products and setting stage to review');
|
||||||
const newProducts = convertSuggestionsToCards(suggestions);
|
try {
|
||||||
setProducts(newProducts);
|
const newProducts = convertSuggestionsToCards(suggestions);
|
||||||
setLocalStage('review');
|
console.log('📦 Converted products:', newProducts.length);
|
||||||
|
setProducts(newProducts);
|
||||||
|
setLocalStage('review');
|
||||||
|
|
||||||
|
// Force update parent data immediately
|
||||||
|
const updatedData = {
|
||||||
|
...data,
|
||||||
|
detectedProducts: newProducts,
|
||||||
|
processingStage: 'review'
|
||||||
|
};
|
||||||
|
onDataChange(updatedData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error converting suggestions to products:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [suggestions, products.length]);
|
}, [suggestions, products.length, data, onDataChange]);
|
||||||
|
|
||||||
// Derive current stage
|
// Derive current stage
|
||||||
const stage = (localStage === 'completed' || localStage === 'error')
|
const stage = (() => {
|
||||||
? localStage
|
// If local stage is explicitly set to completed or error, use it
|
||||||
: (onboardingStage === 'completed' ? 'review' : onboardingStage || localStage);
|
if (localStage === 'completed' || localStage === 'error') {
|
||||||
|
return localStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have products to review, always show review stage
|
||||||
|
if (products.length > 0) {
|
||||||
|
return 'review';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If onboarding processing completed but no products yet, wait for conversion
|
||||||
|
if (onboardingStage === 'completed' && suggestions && suggestions.length > 0) {
|
||||||
|
return 'review';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file is validated but no suggestions generated yet, show confirmation stage
|
||||||
|
if (onboardingStage === 'validated' || localStage === 'validated') {
|
||||||
|
return 'validated';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use the onboarding stage or local stage
|
||||||
|
return onboardingStage || localStage;
|
||||||
|
})();
|
||||||
const progress = onboardingProgress || 0;
|
const progress = onboardingProgress || 0;
|
||||||
const currentMessage = onboardingMessage || '';
|
const currentMessage = onboardingMessage || '';
|
||||||
|
|
||||||
@@ -176,7 +215,10 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
onboardingStage,
|
onboardingStage,
|
||||||
finalStage: stage,
|
finalStage: stage,
|
||||||
productsLength: products.length,
|
productsLength: products.length,
|
||||||
suggestionsLength: suggestions?.length || 0
|
suggestionsLength: suggestions?.length || 0,
|
||||||
|
hasSuggestions: !!suggestions,
|
||||||
|
suggestionsArray: Array.isArray(suggestions),
|
||||||
|
willShowReview: stage === 'review' && products.length > 0
|
||||||
});
|
});
|
||||||
}, [localStage, onboardingStage, stage, products.length, suggestions?.length]);
|
}, [localStage, onboardingStage, stage, products.length, suggestions?.length]);
|
||||||
|
|
||||||
@@ -275,22 +317,14 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
console.log('🔄 SmartInventorySetup - Processing result:', { success });
|
console.log('🔄 SmartInventorySetup - Processing result:', { success });
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log('✅ File processed successfully, setting stage to review');
|
console.log('✅ File validation completed successfully');
|
||||||
setLocalStage('review');
|
// Don't set to review stage anymore - let the 'validated' stage show first
|
||||||
|
setLocalStage('validated');
|
||||||
|
|
||||||
// Check if there was a suggestion error (AI service timeout)
|
toast.addToast(`Archivo validado correctamente. Se encontraron ${validationResults?.product_list?.length || 0} productos.`, {
|
||||||
const stepData = data.allStepData?.['smart-inventory-setup'];
|
title: 'Validación completada',
|
||||||
if (stepData?.suggestionError) {
|
type: 'success'
|
||||||
toast.addToast(`Archivo procesado. ${stepData.suggestionError} Se crearon sugerencias básicas que puedes editar.`, {
|
});
|
||||||
title: 'Procesamiento completado con advertencias',
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.addToast('El archivo se procesó correctamente. Revisa los productos detectados.', {
|
|
||||||
title: 'Procesamiento completado',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ File processing failed - processSalesFile returned false');
|
console.error('❌ File processing failed - processSalesFile returned false');
|
||||||
throw new Error('Error procesando el archivo');
|
throw new Error('Error procesando el archivo');
|
||||||
@@ -364,6 +398,40 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
setEditingProduct(null);
|
setEditingProduct(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle generating suggestions after file validation
|
||||||
|
const handleGenerateSuggestions = async () => {
|
||||||
|
if (!validationResults?.product_list?.length) {
|
||||||
|
toast.addToast('No se encontraron productos para analizar', {
|
||||||
|
title: 'Error',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLocalStage('analyzing');
|
||||||
|
const success = await generateProductSuggestions(validationResults.product_list);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.addToast('Sugerencias generadas correctamente', {
|
||||||
|
title: 'Análisis completado',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.addToast('Error generando sugerencias de productos', {
|
||||||
|
title: 'Error en análisis',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating suggestions:', error);
|
||||||
|
toast.addToast('Error generando sugerencias de productos', {
|
||||||
|
title: 'Error en análisis',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Update parent data
|
// Update parent data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updatedData = {
|
const updatedData = {
|
||||||
@@ -529,6 +597,57 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* File Validated - User Confirmation Required */}
|
||||||
|
{stage === 'validated' && validationResults && (
|
||||||
|
<Card className="p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-20 h-20 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<CheckCircle className="w-10 h-10 text-[var(--color-success)]" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-semibold text-[var(--color-success)] mb-4">
|
||||||
|
¡Archivo Validado Correctamente!
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)] text-lg mb-6 max-w-2xl mx-auto">
|
||||||
|
Hemos encontrado <strong>{validationResults.product_list?.length || 0} productos únicos</strong> en tu archivo de ventas.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-[var(--bg-secondary)] rounded-lg p-6 mb-8 max-w-2xl mx-auto">
|
||||||
|
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Lo que haremos a continuación:</h4>
|
||||||
|
<ul className="text-[var(--text-secondary)] text-left space-y-2">
|
||||||
|
<li>🤖 <strong>Análisis con IA:</strong> Clasificaremos automáticamente tus productos</li>
|
||||||
|
<li>📦 <strong>Configuración inteligente:</strong> Calcularemos niveles de stock óptimos</li>
|
||||||
|
<li>✅ <strong>Tu aprobación:</strong> Podrás revisar y aprobar cada sugerencia</li>
|
||||||
|
<li>🎯 <strong>Inventario personalizado:</strong> Crearemos tu inventario final</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleGenerateSuggestions}
|
||||||
|
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white shadow-lg px-8 py-3 text-lg"
|
||||||
|
>
|
||||||
|
<Brain className="w-5 h-5 mr-2" />
|
||||||
|
Generar Sugerencias con IA
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setLocalStage('upload');
|
||||||
|
setUploadedFile(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-[var(--text-secondary)] border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)] px-6 py-3"
|
||||||
|
>
|
||||||
|
Subir otro archivo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Processing Stages */}
|
{/* Processing Stages */}
|
||||||
{(stage === 'validating' || stage === 'analyzing') && (
|
{(stage === 'validating' || stage === 'analyzing') && (
|
||||||
<Card className="p-8">
|
<Card className="p-8">
|
||||||
@@ -589,6 +708,23 @@ export const SmartInventorySetupStep: React.FC<OnboardingStepProps> = ({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Waiting for Suggestions to Load */}
|
||||||
|
{(stage === 'review' || (onboardingStage === 'completed' && suggestions)) && products.length === 0 && suggestions && suggestions.length > 0 && (
|
||||||
|
<Card className="p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center mx-auto mb-6 animate-pulse">
|
||||||
|
<Brain className="w-8 h-8 text-[var(--color-info)]" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-semibold text-[var(--text-primary)] mb-2">
|
||||||
|
Preparando sugerencias...
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--text-secondary)]">
|
||||||
|
Convirtiendo {suggestions.length} productos en sugerencias personalizadas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Review & Configure Stage */}
|
{/* Review & Configure Stage */}
|
||||||
{(stage === 'review') && products.length > 0 && (
|
{(stage === 'review') && products.length > 0 && (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
|||||||
@@ -184,6 +184,25 @@ export const useOnboardingActions = () => {
|
|||||||
return result.success;
|
return result.success;
|
||||||
}, [store, salesProcessing]);
|
}, [store, salesProcessing]);
|
||||||
|
|
||||||
|
const generateProductSuggestions = useCallback(async (productList: string[]): Promise<boolean> => {
|
||||||
|
console.log('🎬 Actions - generateProductSuggestions started for', productList.length, 'products');
|
||||||
|
store.setLoading(true);
|
||||||
|
|
||||||
|
const result = await salesProcessing.generateProductSuggestions(productList);
|
||||||
|
console.log('🎬 Actions - generateProductSuggestions result:', result);
|
||||||
|
|
||||||
|
store.setLoading(false);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('❌ Actions - Suggestions generation failed:', result.error);
|
||||||
|
store.setError(result.error || 'Error generating product suggestions');
|
||||||
|
} else {
|
||||||
|
console.log('✅ Actions - Product suggestions generated successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.success;
|
||||||
|
}, [store, salesProcessing]);
|
||||||
|
|
||||||
const createInventoryFromSuggestions = useCallback(async (
|
const createInventoryFromSuggestions = useCallback(async (
|
||||||
suggestions: ProductSuggestionResponse[]
|
suggestions: ProductSuggestionResponse[]
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
@@ -306,6 +325,7 @@ export const useOnboardingActions = () => {
|
|||||||
// Step-specific actions
|
// Step-specific actions
|
||||||
createTenant,
|
createTenant,
|
||||||
processSalesFile,
|
processSalesFile,
|
||||||
|
generateProductSuggestions, // New function for separated suggestion generation
|
||||||
createInventoryFromSuggestions,
|
createInventoryFromSuggestions,
|
||||||
importSalesData,
|
importSalesData,
|
||||||
startTraining,
|
startTraining,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export interface OnboardingData {
|
|||||||
files?: {
|
files?: {
|
||||||
salesData?: File;
|
salesData?: File;
|
||||||
};
|
};
|
||||||
processingStage?: 'upload' | 'validating' | 'analyzing' | 'review' | 'completed' | 'error';
|
processingStage?: 'upload' | 'validating' | 'validated' | 'analyzing' | 'review' | 'completed' | 'error';
|
||||||
processingResults?: {
|
processingResults?: {
|
||||||
is_valid: boolean;
|
is_valid: boolean;
|
||||||
total_records: number;
|
total_records: number;
|
||||||
@@ -161,7 +161,7 @@ export interface TenantCreationState extends ServiceState<BakeryRegistration> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SalesProcessingState extends ServiceState<any> {
|
export interface SalesProcessingState extends ServiceState<any> {
|
||||||
stage: 'idle' | 'validating' | 'analyzing' | 'completed' | 'error';
|
stage: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error';
|
||||||
progress: number;
|
progress: number;
|
||||||
currentMessage: string;
|
currentMessage: string;
|
||||||
validationResults: any | null;
|
validationResults: any | null;
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ export const useSalesProcessing = () => {
|
|||||||
// Simple, direct state management
|
// Simple, direct state management
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [suggestions, setSuggestions] = useState<ProductSuggestionResponse[] | null>(null);
|
const [suggestions, setSuggestions] = useState<ProductSuggestionResponse[]>([]);
|
||||||
const [stage, setStage] = useState<'idle' | 'validating' | 'analyzing' | 'completed' | 'error'>('idle');
|
const [stage, setStage] = useState<'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error'>('idle');
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [currentMessage, setCurrentMessage] = useState('');
|
const [currentMessage, setCurrentMessage] = useState('');
|
||||||
const [validationResults, setValidationResults] = useState<any | null>(null);
|
const [validationResults, setValidationResults] = useState<any | null>(null);
|
||||||
|
|
||||||
const updateProgress = useCallback((
|
const updateProgress = useCallback((
|
||||||
progressValue: number,
|
progressValue: number,
|
||||||
stageValue: 'idle' | 'validating' | 'analyzing' | 'completed' | 'error',
|
stageValue: 'idle' | 'validating' | 'validated' | 'analyzing' | 'completed' | 'error',
|
||||||
message: string,
|
message: string,
|
||||||
onProgress?: ProgressCallback
|
onProgress?: ProgressCallback
|
||||||
) => {
|
) => {
|
||||||
@@ -158,18 +158,68 @@ export const useSalesProcessing = () => {
|
|||||||
throw new Error('No se encontraron productos válidos en el archivo');
|
throw new Error('No se encontraron productos válidos en el archivo');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 2: Generate AI suggestions
|
// Stage 2: File validation completed - WAIT FOR USER CONFIRMATION
|
||||||
updateProgress(60, 'analyzing', 'Identificando productos únicos...', onProgress);
|
updateProgress(100, 'validated', 'Archivo validado correctamente. Esperando confirmación del usuario...', onProgress);
|
||||||
updateProgress(70, 'analyzing', 'Generando sugerencias de IA...', onProgress);
|
|
||||||
|
// Store validation results and wait for user action
|
||||||
|
setValidationResults(validationResult);
|
||||||
|
|
||||||
|
console.log('✅ File validation completed:', validationResult.product_list?.length, 'products found');
|
||||||
|
|
||||||
|
// Update onboarding store - ONLY with validation results
|
||||||
|
setStepData('smart-inventory-setup', {
|
||||||
|
files: { salesData: file },
|
||||||
|
processingStage: 'validated', // Changed from 'completed'
|
||||||
|
processingResults: validationResult,
|
||||||
|
// DON'T set suggestions here - they will be generated later
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📊 Updated onboarding store with suggestions');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
validationResults: validationResult,
|
||||||
|
// No suggestions returned from processFile - they will be generated separately
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo';
|
||||||
|
|
||||||
|
setError(errorMessage);
|
||||||
|
updateProgress(0, 'error', errorMessage, onProgress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [updateProgress, currentTenant, validateFile, extractProductList, setStepData]);
|
||||||
|
|
||||||
|
const generateProductSuggestions = useCallback(async (
|
||||||
|
productList: string[],
|
||||||
|
onProgress?: ProgressCallback
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
suggestions?: ProductSuggestionResponse[];
|
||||||
|
error?: string;
|
||||||
|
}> => {
|
||||||
|
console.log('🚀 Generating product suggestions for', productList.length, 'products');
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateProgress(10, 'analyzing', 'Iniciando análisis de productos...', onProgress);
|
||||||
|
updateProgress(30, 'analyzing', 'Identificando productos únicos...', onProgress);
|
||||||
|
updateProgress(50, 'analyzing', 'Generando sugerencias de IA...', onProgress);
|
||||||
|
|
||||||
let suggestions: ProductSuggestionResponse[] = [];
|
let suggestions: ProductSuggestionResponse[] = [];
|
||||||
let suggestionError: string | null = null;
|
let suggestionError: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateProgress(70, 'analyzing', 'Generando sugerencias de IA...', onProgress);
|
updateProgress(70, 'analyzing', 'Consultando servicios de IA...', onProgress);
|
||||||
suggestions = await generateSuggestions(validationResult.product_list);
|
suggestions = await generateSuggestions(productList);
|
||||||
|
|
||||||
console.log('🔍 After generateSuggestions call:', {
|
console.log('🔍 Generated suggestions:', {
|
||||||
suggestionsReceived: suggestions?.length || 0,
|
suggestionsReceived: suggestions?.length || 0,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -177,11 +227,10 @@ export const useSalesProcessing = () => {
|
|||||||
suggestionError = errorMessage;
|
suggestionError = errorMessage;
|
||||||
console.error('❌ Suggestions generation failed:', errorMessage);
|
console.error('❌ Suggestions generation failed:', errorMessage);
|
||||||
|
|
||||||
// Still continue with empty suggestions - user can manually add products later
|
// Create basic suggestions from product names as fallback
|
||||||
updateProgress(80, 'analyzing', 'Preparando productos básicos...', onProgress);
|
updateProgress(80, 'analyzing', 'Preparando productos básicos...', onProgress);
|
||||||
|
|
||||||
// Create basic suggestions from product names as fallback
|
suggestions = productList.map((productName, index) => ({
|
||||||
suggestions = validationResult.product_list.map((productName, index) => ({
|
|
||||||
suggestion_id: `manual-${index}`,
|
suggestion_id: `manual-${index}`,
|
||||||
original_name: productName,
|
original_name: productName,
|
||||||
suggested_name: productName,
|
suggested_name: productName,
|
||||||
@@ -200,42 +249,40 @@ export const useSalesProcessing = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress);
|
updateProgress(90, 'analyzing', 'Analizando patrones de venta...', onProgress);
|
||||||
updateProgress(100, 'completed', 'Procesamiento completado exitosamente', onProgress);
|
updateProgress(100, 'completed', 'Sugerencias generadas correctamente', onProgress);
|
||||||
|
|
||||||
// Update state with suggestions (even if empty or fallback)
|
// Update state with suggestions
|
||||||
setSuggestions(suggestions || []);
|
setSuggestions(suggestions || []);
|
||||||
setValidationResults(validationResult);
|
|
||||||
|
|
||||||
console.log('✅ Processing completed:', suggestions?.length || 0, 'suggestions generated');
|
console.log('✅ Suggestions generation completed:', suggestions?.length || 0, 'suggestions');
|
||||||
|
|
||||||
// Update onboarding store
|
// Update onboarding store with suggestions
|
||||||
setStepData('smart-inventory-setup', {
|
setStepData('smart-inventory-setup', (prevData) => ({
|
||||||
files: { salesData: file },
|
...prevData,
|
||||||
processingStage: 'completed',
|
processingStage: 'completed',
|
||||||
processingResults: validationResult,
|
|
||||||
suggestions: suggestions || [],
|
suggestions: suggestions || [],
|
||||||
suggestionError: suggestionError,
|
suggestionError: suggestionError,
|
||||||
});
|
}));
|
||||||
|
|
||||||
console.log('📊 Updated onboarding store with suggestions');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
validationResults: validationResult,
|
|
||||||
suggestions: suggestions || [],
|
suggestions: suggestions || [],
|
||||||
|
error: suggestionError || undefined,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Error procesando el archivo';
|
const errorMessage = error instanceof Error ? error.message : 'Error generando sugerencias';
|
||||||
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
updateProgress(0, 'error', errorMessage, onProgress);
|
updateProgress(0, 'error', errorMessage, onProgress);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [updateProgress, currentTenant, validateFile, extractProductList, generateSuggestions, setStepData]);
|
}, [updateProgress, generateSuggestions, setStepData]);
|
||||||
|
|
||||||
|
|
||||||
const clearError = useCallback(() => {
|
const clearError = useCallback(() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -244,7 +291,7 @@ export const useSalesProcessing = () => {
|
|||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuggestions(null);
|
setSuggestions([]);
|
||||||
setStage('idle');
|
setStage('idle');
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setCurrentMessage('');
|
setCurrentMessage('');
|
||||||
@@ -263,7 +310,7 @@ export const useSalesProcessing = () => {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
processFile,
|
processFile,
|
||||||
generateSuggestions,
|
generateProductSuggestions, // New separated function
|
||||||
clearError,
|
clearError,
|
||||||
reset,
|
reset,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const useOnboarding = () => {
|
|||||||
progress: salesProcessing.progress,
|
progress: salesProcessing.progress,
|
||||||
currentMessage: salesProcessing.currentMessage,
|
currentMessage: salesProcessing.currentMessage,
|
||||||
validationResults: salesProcessing.validationResults,
|
validationResults: salesProcessing.validationResults,
|
||||||
suggestions: salesProcessing.suggestions,
|
suggestions: Array.isArray(salesProcessing.suggestions) ? salesProcessing.suggestions : [],
|
||||||
},
|
},
|
||||||
|
|
||||||
inventorySetup: {
|
inventorySetup: {
|
||||||
@@ -123,6 +123,7 @@ export const useOnboarding = () => {
|
|||||||
// Step-specific actions
|
// Step-specific actions
|
||||||
createTenant: actions.createTenant,
|
createTenant: actions.createTenant,
|
||||||
processSalesFile: actions.processSalesFile,
|
processSalesFile: actions.processSalesFile,
|
||||||
|
generateProductSuggestions: actions.generateProductSuggestions, // New separated function
|
||||||
createInventoryFromSuggestions: actions.createInventoryFromSuggestions,
|
createInventoryFromSuggestions: actions.createInventoryFromSuggestions,
|
||||||
importSalesData: actions.importSalesData,
|
importSalesData: actions.importSalesData,
|
||||||
startTraining: actions.startTraining,
|
startTraining: actions.startTraining,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from uuid import UUID
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import structlog
|
import structlog
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
from shared.alerts.base_service import BaseAlertService, AlertServiceMixin
|
from shared.alerts.base_service import BaseAlertService, AlertServiceMixin
|
||||||
from shared.alerts.templates import format_item_message
|
from shared.alerts.templates import format_item_message
|
||||||
@@ -23,46 +24,48 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
def setup_scheduled_checks(self):
|
def setup_scheduled_checks(self):
|
||||||
"""Inventory-specific scheduled checks for alerts and recommendations"""
|
"""Inventory-specific scheduled checks for alerts and recommendations"""
|
||||||
|
|
||||||
# Critical stock checks - every 5 minutes (alerts)
|
# SPACED SCHEDULING TO PREVENT CONCURRENT EXECUTION AND DEADLOCKS
|
||||||
|
|
||||||
|
# Critical stock checks - every 5 minutes (alerts) - Start at minute 0, 5, 10, etc.
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
self.check_stock_levels,
|
self.check_stock_levels,
|
||||||
CronTrigger(minute='*/5'),
|
CronTrigger(minute='0,5,10,15,20,25,30,35,40,45,50,55'), # Explicit minutes
|
||||||
id='stock_levels',
|
id='stock_levels',
|
||||||
misfire_grace_time=30,
|
misfire_grace_time=30,
|
||||||
max_instances=1
|
max_instances=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Expiry checks - every 2 minutes (food safety critical, alerts)
|
# Expiry checks - every 2 minutes (food safety critical, alerts) - Start at minute 1, 3, 7, etc.
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
self.check_expiring_products,
|
self.check_expiring_products,
|
||||||
CronTrigger(minute='*/2'),
|
CronTrigger(minute='1,3,7,9,11,13,17,19,21,23,27,29,31,33,37,39,41,43,47,49,51,53,57,59'), # Avoid conflicts
|
||||||
id='expiry_check',
|
id='expiry_check',
|
||||||
misfire_grace_time=30,
|
misfire_grace_time=30,
|
||||||
max_instances=1
|
max_instances=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Temperature checks - every 2 minutes (alerts)
|
# Temperature checks - every 5 minutes (alerts) - Start at minute 2, 12, 22, etc. (reduced frequency)
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
self.check_temperature_breaches,
|
self.check_temperature_breaches,
|
||||||
CronTrigger(minute='*/2'),
|
CronTrigger(minute='2,12,22,32,42,52'), # Every 10 minutes, offset by 2
|
||||||
id='temperature_check',
|
id='temperature_check',
|
||||||
misfire_grace_time=30,
|
misfire_grace_time=30,
|
||||||
max_instances=1
|
max_instances=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Inventory optimization - every 30 minutes (recommendations)
|
# Inventory optimization - every 30 minutes (recommendations) - Start at minute 15, 45
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
self.generate_inventory_recommendations,
|
self.generate_inventory_recommendations,
|
||||||
CronTrigger(minute='*/30'),
|
CronTrigger(minute='15,45'), # Offset to avoid conflicts
|
||||||
id='inventory_recs',
|
id='inventory_recs',
|
||||||
misfire_grace_time=120,
|
misfire_grace_time=120,
|
||||||
max_instances=1
|
max_instances=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Waste reduction analysis - every hour (recommendations)
|
# Waste reduction analysis - every hour (recommendations) - Start at minute 30
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
self.generate_waste_reduction_recommendations,
|
self.generate_waste_reduction_recommendations,
|
||||||
CronTrigger(minute='0'),
|
CronTrigger(minute='30'), # Offset to avoid conflicts
|
||||||
id='waste_reduction_recs',
|
id='waste_reduction_recs',
|
||||||
misfire_grace_time=300,
|
misfire_grace_time=300,
|
||||||
max_instances=1
|
max_instances=1
|
||||||
@@ -96,7 +99,7 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
GREATEST(0, i.low_stock_threshold - COALESCE(SUM(s.current_quantity), 0)) as shortage_amount
|
GREATEST(0, i.low_stock_threshold - COALESCE(SUM(s.current_quantity), 0)) as shortage_amount
|
||||||
FROM ingredients i
|
FROM ingredients i
|
||||||
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
||||||
WHERE i.tenant_id = $1 AND i.is_active = true
|
WHERE i.tenant_id = :tenant_id AND i.is_active = true
|
||||||
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level, i.reorder_point
|
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level, i.reorder_point
|
||||||
)
|
)
|
||||||
SELECT * FROM stock_analysis WHERE status != 'normal'
|
SELECT * FROM stock_analysis WHERE status != 'normal'
|
||||||
@@ -113,13 +116,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
|
|
||||||
for tenant_id in tenants:
|
for tenant_id in tenants:
|
||||||
try:
|
try:
|
||||||
from sqlalchemy import text
|
# Add timeout to prevent hanging connections
|
||||||
async with self.db_manager.get_session() as session:
|
async with asyncio.timeout(30): # 30 second timeout
|
||||||
result = await session.execute(text(query), {"tenant_id": tenant_id})
|
async with self.db_manager.get_background_session() as session:
|
||||||
issues = result.fetchall()
|
result = await session.execute(text(query), {"tenant_id": tenant_id})
|
||||||
|
issues = result.fetchall()
|
||||||
|
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
await self._process_stock_issue(tenant_id, issue)
|
# Convert SQLAlchemy Row to dictionary for easier access
|
||||||
|
issue_dict = dict(issue._mapping) if hasattr(issue, '_mapping') else dict(issue)
|
||||||
|
await self._process_stock_issue(tenant_id, issue_dict)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error checking stock for tenant",
|
logger.error("Error checking stock for tenant",
|
||||||
@@ -227,18 +233,21 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
ORDER BY s.expiration_date ASC
|
ORDER BY s.expiration_date ASC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import text
|
# Add timeout to prevent hanging connections
|
||||||
async with self.db_manager.get_session() as session:
|
async with asyncio.timeout(30): # 30 second timeout
|
||||||
result = await session.execute(text(query))
|
async with self.db_manager.get_background_session() as session:
|
||||||
expiring_items = result.fetchall()
|
result = await session.execute(text(query))
|
||||||
|
expiring_items = result.fetchall()
|
||||||
|
|
||||||
# Group by tenant
|
# Group by tenant
|
||||||
by_tenant = {}
|
by_tenant = {}
|
||||||
for item in expiring_items:
|
for item in expiring_items:
|
||||||
tenant_id = item['tenant_id']
|
# Convert SQLAlchemy Row to dictionary for easier access
|
||||||
|
item_dict = dict(item._mapping) if hasattr(item, '_mapping') else dict(item)
|
||||||
|
tenant_id = item_dict['tenant_id']
|
||||||
if tenant_id not in by_tenant:
|
if tenant_id not in by_tenant:
|
||||||
by_tenant[tenant_id] = []
|
by_tenant[tenant_id] = []
|
||||||
by_tenant[tenant_id].append(item)
|
by_tenant[tenant_id].append(item_dict)
|
||||||
|
|
||||||
for tenant_id, items in by_tenant.items():
|
for tenant_id, items in by_tenant.items():
|
||||||
await self._process_expiring_items(tenant_id, items)
|
await self._process_expiring_items(tenant_id, items)
|
||||||
@@ -328,13 +337,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
ORDER BY t.temperature_celsius DESC, t.deviation_minutes DESC
|
ORDER BY t.temperature_celsius DESC, t.deviation_minutes DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import text
|
# Add timeout to prevent hanging connections
|
||||||
async with self.db_manager.get_session() as session:
|
async with asyncio.timeout(30): # 30 second timeout
|
||||||
result = await session.execute(text(query))
|
async with self.db_manager.get_background_session() as session:
|
||||||
breaches = result.fetchall()
|
result = await session.execute(text(query))
|
||||||
|
breaches = result.fetchall()
|
||||||
|
|
||||||
for breach in breaches:
|
for breach in breaches:
|
||||||
await self._process_temperature_breach(breach)
|
# Convert SQLAlchemy Row to dictionary for easier access
|
||||||
|
breach_dict = dict(breach._mapping) if hasattr(breach, '_mapping') else dict(breach)
|
||||||
|
await self._process_temperature_breach(breach_dict)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Temperature check failed", error=str(e))
|
logger.error("Temperature check failed", error=str(e))
|
||||||
@@ -378,13 +390,13 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
}, item_type='alert')
|
}, item_type='alert')
|
||||||
|
|
||||||
# Update alert triggered flag to avoid spam
|
# Update alert triggered flag to avoid spam
|
||||||
from sqlalchemy import text
|
# Add timeout to prevent hanging connections
|
||||||
async with self.db_manager.get_session() as session:
|
async with asyncio.timeout(10): # 10 second timeout for simple update
|
||||||
await session.execute(
|
async with self.db_manager.get_background_session() as session:
|
||||||
text("UPDATE temperature_logs SET alert_triggered = true WHERE id = :id"),
|
await session.execute(
|
||||||
{"id": breach['id']}
|
text("UPDATE temperature_logs SET alert_triggered = true WHERE id = :id"),
|
||||||
)
|
{"id": breach['id']}
|
||||||
await session.commit()
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error processing temperature breach",
|
logger.error("Error processing temperature breach",
|
||||||
@@ -412,7 +424,7 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
FROM ingredients i
|
FROM ingredients i
|
||||||
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
||||||
LEFT JOIN stock_movements sm ON sm.ingredient_id = i.id
|
LEFT JOIN stock_movements sm ON sm.ingredient_id = i.id
|
||||||
WHERE i.is_active = true AND i.tenant_id = $1
|
WHERE i.is_active = true AND i.tenant_id = :tenant_id
|
||||||
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level
|
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level
|
||||||
HAVING COUNT(sm.id) FILTER (WHERE sm.movement_type = 'production_use'
|
HAVING COUNT(sm.id) FILTER (WHERE sm.movement_type = 'production_use'
|
||||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') >= 3
|
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') >= 3
|
||||||
@@ -438,12 +450,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
for tenant_id in tenants:
|
for tenant_id in tenants:
|
||||||
try:
|
try:
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
async with self.db_manager.get_session() as session:
|
# Add timeout to prevent hanging connections
|
||||||
result = await session.execute(text(query), {"tenant_id": tenant_id})
|
async with asyncio.timeout(30): # 30 second timeout
|
||||||
|
async with self.db_manager.get_background_session() as session:
|
||||||
|
result = await session.execute(text(query), {"tenant_id": tenant_id})
|
||||||
recommendations = result.fetchall()
|
recommendations = result.fetchall()
|
||||||
|
|
||||||
for rec in recommendations:
|
for rec in recommendations:
|
||||||
await self._generate_stock_recommendation(tenant_id, rec)
|
# Convert SQLAlchemy Row to dictionary for easier access
|
||||||
|
rec_dict = dict(rec._mapping) if hasattr(rec, '_mapping') else dict(rec)
|
||||||
|
await self._generate_stock_recommendation(tenant_id, rec_dict)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error generating recommendations for tenant",
|
logger.error("Error generating recommendations for tenant",
|
||||||
@@ -524,7 +540,7 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
JOIN stock_movements sm ON sm.ingredient_id = i.id
|
JOIN stock_movements sm ON sm.ingredient_id = i.id
|
||||||
WHERE sm.movement_type = 'waste'
|
WHERE sm.movement_type = 'waste'
|
||||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days'
|
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days'
|
||||||
AND i.tenant_id = $1
|
AND i.tenant_id = :tenant_id
|
||||||
GROUP BY i.id, i.name, i.tenant_id, sm.reason_code
|
GROUP BY i.id, i.name, i.tenant_id, sm.reason_code
|
||||||
HAVING SUM(sm.quantity) > 5 -- More than 5kg wasted
|
HAVING SUM(sm.quantity) > 5 -- More than 5kg wasted
|
||||||
ORDER BY total_waste_30d DESC
|
ORDER BY total_waste_30d DESC
|
||||||
@@ -535,12 +551,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
for tenant_id in tenants:
|
for tenant_id in tenants:
|
||||||
try:
|
try:
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
async with self.db_manager.get_session() as session:
|
# Add timeout to prevent hanging connections
|
||||||
result = await session.execute(text(query), {"tenant_id": tenant_id})
|
async with asyncio.timeout(30): # 30 second timeout
|
||||||
|
async with self.db_manager.get_background_session() as session:
|
||||||
|
result = await session.execute(text(query), {"tenant_id": tenant_id})
|
||||||
waste_data = result.fetchall()
|
waste_data = result.fetchall()
|
||||||
|
|
||||||
for waste in waste_data:
|
for waste in waste_data:
|
||||||
await self._generate_waste_recommendation(tenant_id, waste)
|
# Convert SQLAlchemy Row to dictionary for easier access
|
||||||
|
waste_dict = dict(waste._mapping) if hasattr(waste, '_mapping') else dict(waste)
|
||||||
|
await self._generate_waste_recommendation(tenant_id, waste_dict)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error generating waste recommendations",
|
logger.error("Error generating waste recommendations",
|
||||||
@@ -703,6 +723,28 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error handling order placed event", error=str(e))
|
logger.error("Error handling order placed event", error=str(e))
|
||||||
|
|
||||||
|
async def get_active_tenants(self) -> List[UUID]:
|
||||||
|
"""Get list of active tenant IDs from ingredients table (inventory service specific)"""
|
||||||
|
try:
|
||||||
|
query = text("SELECT DISTINCT tenant_id FROM ingredients WHERE is_active = true")
|
||||||
|
# Add timeout to prevent hanging connections
|
||||||
|
async with asyncio.timeout(10): # 10 second timeout
|
||||||
|
async with self.db_manager.get_background_session() as session:
|
||||||
|
result = await session.execute(query)
|
||||||
|
# Handle PostgreSQL UUID objects properly
|
||||||
|
tenant_ids = []
|
||||||
|
for row in result.fetchall():
|
||||||
|
tenant_id = row.tenant_id
|
||||||
|
# Convert to UUID if it's not already
|
||||||
|
if isinstance(tenant_id, UUID):
|
||||||
|
tenant_ids.append(tenant_id)
|
||||||
|
else:
|
||||||
|
tenant_ids.append(UUID(str(tenant_id)))
|
||||||
|
return tenant_ids
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error fetching active tenants from ingredients", error=str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
async def get_stock_after_order(self, ingredient_id: str, order_quantity: float) -> Optional[Dict[str, Any]]:
|
async def get_stock_after_order(self, ingredient_id: str, order_quantity: float) -> Optional[Dict[str, Any]]:
|
||||||
"""Get stock information after hypothetical order"""
|
"""Get stock information after hypothetical order"""
|
||||||
try:
|
try:
|
||||||
@@ -710,15 +752,19 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
|||||||
SELECT i.id, i.name,
|
SELECT i.id, i.name,
|
||||||
COALESCE(SUM(s.current_quantity), 0) as current_stock,
|
COALESCE(SUM(s.current_quantity), 0) as current_stock,
|
||||||
i.low_stock_threshold as minimum_stock,
|
i.low_stock_threshold as minimum_stock,
|
||||||
(COALESCE(SUM(s.current_quantity), 0) - $2) as remaining
|
(COALESCE(SUM(s.current_quantity), 0) - :order_quantity) as remaining
|
||||||
FROM ingredients i
|
FROM ingredients i
|
||||||
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
||||||
WHERE i.id = $1
|
WHERE i.id = :ingredient_id
|
||||||
GROUP BY i.id, i.name, i.low_stock_threshold
|
GROUP BY i.id, i.name, i.low_stock_threshold
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = await self.db_manager.fetchrow(query, ingredient_id, order_quantity)
|
# Add timeout to prevent hanging connections
|
||||||
return dict(result) if result else None
|
async with asyncio.timeout(10): # 10 second timeout
|
||||||
|
async with self.db_manager.get_background_session() as session:
|
||||||
|
result = await session.execute(text(query), {"ingredient_id": ingredient_id, "order_quantity": order_quantity})
|
||||||
|
row = result.fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error getting stock after order",
|
logger.error("Error getting stock after order",
|
||||||
|
|||||||
Reference in New Issue
Block a user