Remove manual path and add inventory lots UI to AI-assisted onboarding
## Architectural Changes **1. Remove Manual Entry Path** - Deleted data-source-choice step (DataSourceChoiceStep) - Removed manual inventory-setup step (InventorySetupStep) - Removed all manual path conditions from wizard flow - Set dataSource to 'ai-assisted' by default in WizardContext Files modified: - frontend/src/components/domain/onboarding/UnifiedOnboardingWizard.tsx:11-28,61-162 - frontend/src/components/domain/onboarding/context/WizardContext.tsx:64 **2. Add Inventory Lots UI to AI Inventory Step** Added full stock lot management with expiration tracking to UploadSalesDataStep: **Features Added:** - Inline stock lot entry form after each AI-suggested ingredient - Multi-lot support - add multiple lots per ingredient with different expiration dates - Fields: quantity*, expiration date, supplier, batch/lot number - Visual list of added lots with expiration dates - Delete individual lots before completing - Smart validation with expiration date warnings - FIFO help text - Auto-select supplier if only one exists **Technical Implementation:** - Added useAddStock and useSuppliers hooks (lines 5,7,102-103) - Added stock state management (lines 106-114) - Stock handler functions (lines 336-428): - handleAddStockClick - Opens stock form - handleCancelStock - Closes and resets form - validateStockForm - Validates quantity and expiration - handleSaveStockLot - Saves to local state, supports "Add Another Lot" - handleDeleteStockLot - Removes from list - Modified handleNext to create stock lots after ingredients (lines 490-533) - Added stock lots UI section in ingredient rendering (lines 679-830) **UI Flow:** 1. User uploads sales data 2. AI suggests ingredients 3. User reviews/edits ingredients 4. **NEW**: User can optionally add stock lots with expiration dates 5. Click "Next" creates both ingredients AND stock lots 6. FIFO tracking enabled from day one **Benefits:** - Addresses JTBD: waste prevention, expiration tracking from onboarding - Progressive disclosure - optional but encouraged - Maintains simplicity of AI-assisted path - Enables inventory best practices from the start Files modified: - frontend/src/components/domain/onboarding/steps/UploadSalesDataStep.tsx:1-12,90-114,335-533,679-830 **Build Status:** ✓ Successful in 20.78s
This commit is contained in:
@@ -10,7 +10,6 @@ import { useTenantInitializer } from '../../../stores/useTenantInitializer';
|
||||
import { WizardProvider, useWizardContext, BakeryType, DataSource } from './context';
|
||||
import {
|
||||
BakeryTypeSelectionStep,
|
||||
DataSourceChoiceStep,
|
||||
RegisterTenantStep,
|
||||
UploadSalesDataStep,
|
||||
ProductCategorizationStep,
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
// Import setup wizard steps
|
||||
import {
|
||||
SuppliersSetupStep,
|
||||
InventorySetupStep,
|
||||
RecipesSetupStep,
|
||||
QualitySetupStep,
|
||||
TeamSetupStep,
|
||||
@@ -66,14 +64,6 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.bakery_type.description', 'Selecciona tu tipo de negocio'),
|
||||
component: BakeryTypeSelectionStep,
|
||||
},
|
||||
{
|
||||
id: 'data-source-choice',
|
||||
title: t('onboarding:steps.data_source.title', 'Método de Configuración'),
|
||||
description: t('onboarding:steps.data_source.description', 'Elige cómo configurar'),
|
||||
component: DataSourceChoiceStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.bakeryType !== null,
|
||||
},
|
||||
// Phase 2: Core Setup
|
||||
{
|
||||
id: 'setup',
|
||||
@@ -81,16 +71,16 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.setup.description', 'Información básica'),
|
||||
component: RegisterTenantStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.dataSource !== null,
|
||||
condition: (ctx) => ctx.state.bakeryType !== null,
|
||||
},
|
||||
// Phase 2a: AI-Assisted Path
|
||||
// Phase 2a: AI-Assisted Path (ONLY PATH NOW)
|
||||
{
|
||||
id: 'smart-inventory-setup',
|
||||
title: t('onboarding:steps.smart_inventory.title', 'Subir Datos de Ventas'),
|
||||
description: t('onboarding:steps.smart_inventory.description', 'Configuración con IA'),
|
||||
component: UploadSalesDataStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.dataSource === 'ai-assisted',
|
||||
condition: (ctx) => ctx.tenantId !== null,
|
||||
},
|
||||
{
|
||||
id: 'product-categorization',
|
||||
@@ -98,7 +88,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.categorization.description', 'Clasifica ingredientes vs productos'),
|
||||
component: ProductCategorizationStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.dataSource === 'ai-assisted' && ctx.state.aiAnalysisComplete,
|
||||
condition: (ctx) => ctx.state.aiAnalysisComplete,
|
||||
},
|
||||
{
|
||||
id: 'initial-stock-entry',
|
||||
@@ -106,19 +96,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.stock.description', 'Cantidades iniciales'),
|
||||
component: InitialStockEntryStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.dataSource === 'ai-assisted' && ctx.state.categorizationCompleted,
|
||||
},
|
||||
// Phase 2b: Core Data Entry (Manual Path)
|
||||
// IMPORTANT: Inventory must come BEFORE suppliers so suppliers can associate products
|
||||
{
|
||||
id: 'inventory-setup',
|
||||
title: t('onboarding:steps.inventory.title', 'Inventario'),
|
||||
description: t('onboarding:steps.inventory.description', 'Productos e ingredientes'),
|
||||
component: InventorySetupStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) =>
|
||||
// Only show for manual path (AI path creates inventory earlier)
|
||||
ctx.state.dataSource === 'manual',
|
||||
condition: (ctx) => ctx.state.categorizationCompleted,
|
||||
},
|
||||
{
|
||||
id: 'suppliers-setup',
|
||||
@@ -126,10 +104,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.suppliers.description', 'Configura tus proveedores'),
|
||||
component: SuppliersSetupStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) =>
|
||||
// Show after inventory exists (either from AI or manual path)
|
||||
(ctx.state.dataSource === 'ai-assisted' && ctx.state.stockEntryCompleted) ||
|
||||
(ctx.state.dataSource === 'manual' && ctx.state.inventoryCompleted),
|
||||
condition: (ctx) => ctx.state.stockEntryCompleted,
|
||||
},
|
||||
{
|
||||
id: 'recipes-setup',
|
||||
@@ -156,7 +131,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.quality.description', 'Estándares de calidad'),
|
||||
component: QualitySetupStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.dataSource !== null,
|
||||
condition: (ctx) => ctx.tenantId !== null,
|
||||
},
|
||||
{
|
||||
id: 'team-setup',
|
||||
@@ -164,7 +139,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.team.description', 'Miembros del equipo'),
|
||||
component: TeamSetupStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.dataSource !== null,
|
||||
condition: (ctx) => ctx.tenantId !== null,
|
||||
},
|
||||
// Phase 4: ML & Finalization
|
||||
{
|
||||
@@ -173,7 +148,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.ml_training.description', 'Modelo personalizado'),
|
||||
component: MLTrainingStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.inventoryCompleted || ctx.state.aiAnalysisComplete,
|
||||
condition: (ctx) => ctx.state.aiAnalysisComplete,
|
||||
},
|
||||
{
|
||||
id: 'setup-review',
|
||||
@@ -181,7 +156,7 @@ const OnboardingWizardContent: React.FC = () => {
|
||||
description: t('onboarding:steps.review.description', 'Confirma tu configuración'),
|
||||
component: ReviewSetupStep,
|
||||
isConditional: true,
|
||||
condition: (ctx) => ctx.state.dataSource !== null,
|
||||
condition: (ctx) => ctx.tenantId !== null,
|
||||
},
|
||||
{
|
||||
id: 'completion',
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface WizardContextValue {
|
||||
|
||||
const initialState: WizardState = {
|
||||
bakeryType: null,
|
||||
dataSource: null,
|
||||
dataSource: 'ai-assisted', // Only AI-assisted path supported now
|
||||
aiSuggestions: [],
|
||||
aiAnalysisComplete: false,
|
||||
categorizedProducts: undefined,
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '../../../ui/Button';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useCreateIngredient, useClassifyBatch } from '../../../../api/hooks/inventory';
|
||||
import { useCreateIngredient, useClassifyBatch, useAddStock } from '../../../../api/hooks/inventory';
|
||||
import { useValidateImportFile, useImportSalesData } from '../../../../api/hooks/sales';
|
||||
import { useSuppliers } from '../../../../api/hooks/suppliers';
|
||||
import type { ImportValidationResponse } from '../../../../api/types/dataImport';
|
||||
import type { ProductSuggestionResponse } from '../../../../api/types/inventory';
|
||||
import type { ProductSuggestionResponse, StockCreate, StockResponse } from '../../../../api/types/inventory';
|
||||
import { ProductionStage } from '../../../../api/types/inventory';
|
||||
import { useAuth } from '../../../../contexts/AuthContext';
|
||||
import { BatchAddIngredientsModal } from '../../inventory/BatchAddIngredientsModal';
|
||||
|
||||
@@ -87,10 +89,29 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const { user } = useAuth();
|
||||
const tenantId = currentTenant?.id || '';
|
||||
|
||||
// API hooks
|
||||
const validateFileMutation = useValidateImportFile();
|
||||
const createIngredient = useCreateIngredient();
|
||||
const importMutation = useImportSalesData();
|
||||
const classifyBatchMutation = useClassifyBatch();
|
||||
const addStockMutation = useAddStock();
|
||||
|
||||
// Fetch suppliers for stock entry
|
||||
const { data: suppliersData } = useSuppliers(tenantId, { limit: 100 }, { enabled: !!tenantId });
|
||||
const suppliers = (suppliersData || []).filter(s => s.status === 'active');
|
||||
|
||||
// Stock lots state
|
||||
const [addingStockForId, setAddingStockForId] = useState<string | null>(null);
|
||||
const [stockFormData, setStockFormData] = useState({
|
||||
current_quantity: '',
|
||||
expiration_date: '',
|
||||
supplier_id: '',
|
||||
batch_number: '',
|
||||
});
|
||||
const [stockErrors, setStockErrors] = useState<Record<string, string>>({});
|
||||
const [ingredientStocks, setIngredientStocks] = useState<Record<string, StockResponse[]>>({});
|
||||
|
||||
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
@@ -311,6 +332,101 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
setInventoryItems(items => items.filter(item => item.id !== itemId));
|
||||
};
|
||||
|
||||
// Stock lot handlers
|
||||
const handleAddStockClick = (ingredientId: string) => {
|
||||
setAddingStockForId(ingredientId);
|
||||
setStockFormData({
|
||||
current_quantity: '',
|
||||
expiration_date: '',
|
||||
supplier_id: suppliers.length === 1 ? suppliers[0].id : '',
|
||||
batch_number: '',
|
||||
});
|
||||
setStockErrors({});
|
||||
};
|
||||
|
||||
const handleCancelStock = () => {
|
||||
setAddingStockForId(null);
|
||||
setStockFormData({
|
||||
current_quantity: '',
|
||||
expiration_date: '',
|
||||
supplier_id: '',
|
||||
batch_number: '',
|
||||
});
|
||||
setStockErrors({});
|
||||
};
|
||||
|
||||
const validateStockForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!stockFormData.current_quantity || Number(stockFormData.current_quantity) <= 0) {
|
||||
newErrors.current_quantity = 'La cantidad debe ser mayor que cero';
|
||||
}
|
||||
|
||||
if (stockFormData.expiration_date) {
|
||||
const expDate = new Date(stockFormData.expiration_date);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (expDate < today) {
|
||||
newErrors.expiration_date = 'La fecha de caducidad está en el pasado';
|
||||
}
|
||||
|
||||
const threeDaysFromNow = new Date(today);
|
||||
threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);
|
||||
if (expDate < threeDaysFromNow) {
|
||||
newErrors.expiration_warning = '⚠️ Este ingrediente caduca muy pronto!';
|
||||
}
|
||||
}
|
||||
|
||||
setStockErrors(newErrors);
|
||||
return Object.keys(newErrors).filter(k => k !== 'expiration_warning').length === 0;
|
||||
};
|
||||
|
||||
const handleSaveStockLot = (addAnother: boolean = false) => {
|
||||
if (!addingStockForId || !validateStockForm()) return;
|
||||
|
||||
// Create a temporary stock lot entry (will be saved when ingredients are created)
|
||||
const newLot: StockResponse = {
|
||||
id: `temp-${Date.now()}`,
|
||||
tenant_id: tenantId,
|
||||
ingredient_id: addingStockForId,
|
||||
current_quantity: Number(stockFormData.current_quantity),
|
||||
expiration_date: stockFormData.expiration_date || undefined,
|
||||
supplier_id: stockFormData.supplier_id || undefined,
|
||||
batch_number: stockFormData.batch_number || undefined,
|
||||
production_stage: ProductionStage.RAW_INGREDIENT,
|
||||
quality_status: 'good',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
} as StockResponse;
|
||||
|
||||
// Add to local state
|
||||
setIngredientStocks(prev => ({
|
||||
...prev,
|
||||
[addingStockForId]: [...(prev[addingStockForId] || []), newLot],
|
||||
}));
|
||||
|
||||
if (addAnother) {
|
||||
// Reset form for adding another lot
|
||||
setStockFormData({
|
||||
current_quantity: '',
|
||||
expiration_date: '',
|
||||
supplier_id: stockFormData.supplier_id, // Keep supplier selected
|
||||
batch_number: '',
|
||||
});
|
||||
setStockErrors({});
|
||||
} else {
|
||||
handleCancelStock();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteStockLot = (ingredientId: string, stockId: string) => {
|
||||
setIngredientStocks(prev => ({
|
||||
...prev,
|
||||
[ingredientId]: (prev[ingredientId] || []).filter(s => s.id !== stockId),
|
||||
}));
|
||||
};
|
||||
|
||||
// Create all inventory items when Next is clicked
|
||||
const handleNext = async () => {
|
||||
if (inventoryItems.length === 0) {
|
||||
@@ -371,10 +487,55 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
|
||||
console.log(`Creados exitosamente ${createdIngredients.length} ingredientes`);
|
||||
|
||||
// Create stock lots for ingredients
|
||||
setProgressState({
|
||||
stage: 'creating_stock',
|
||||
progress: 40,
|
||||
message: 'Creando lotes de stock...'
|
||||
});
|
||||
|
||||
const stockCreationPromises: Promise<any>[] = [];
|
||||
|
||||
createdIngredients.forEach((ingredient) => {
|
||||
// Find the original UI item to get its temporary ID
|
||||
const originalItem = inventoryItems.find(item => item.name === ingredient.name);
|
||||
if (originalItem) {
|
||||
const lots = ingredientStocks[originalItem.id] || [];
|
||||
|
||||
// Create stock lots for this ingredient
|
||||
lots.forEach((lot) => {
|
||||
const stockData: StockCreate = {
|
||||
ingredient_id: ingredient.id,
|
||||
current_quantity: lot.current_quantity,
|
||||
production_stage: ProductionStage.RAW_INGREDIENT,
|
||||
quality_status: 'good',
|
||||
expiration_date: lot.expiration_date,
|
||||
supplier_id: lot.supplier_id,
|
||||
batch_number: lot.batch_number,
|
||||
};
|
||||
|
||||
stockCreationPromises.push(
|
||||
addStockMutation.mutateAsync({
|
||||
tenantId: currentTenant.id,
|
||||
stockData
|
||||
}).catch(err => {
|
||||
console.error(`Error creando lote de stock para ${ingredient.name}:`, err);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (stockCreationPromises.length > 0) {
|
||||
await Promise.allSettled(stockCreationPromises);
|
||||
console.log(`Creados exitosamente ${stockCreationPromises.length} lotes de stock`);
|
||||
}
|
||||
|
||||
// Import sales data if available
|
||||
setProgressState({
|
||||
stage: 'importing_sales',
|
||||
progress: 50,
|
||||
progress: 60,
|
||||
message: 'Importando datos de ventas...'
|
||||
});
|
||||
|
||||
@@ -514,6 +675,159 @@ export const UploadSalesDataStep: React.FC<UploadSalesDataStepProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stock Lots Section */}
|
||||
{(() => {
|
||||
const lots = ingredientStocks[item.id] || [];
|
||||
const isAddingStock = addingStockForId === item.id;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t border-[var(--border-secondary)]">
|
||||
{lots.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)]">
|
||||
Lotes agregados ({lots.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{lots.map((lot) => (
|
||||
<div
|
||||
key={lot.id}
|
||||
className="flex items-center justify-between p-2 bg-[var(--bg-primary)] rounded text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium">{lot.current_quantity} {item.unit_of_measure}</span>
|
||||
{lot.expiration_date && (
|
||||
<span className="text-[var(--text-tertiary)]">
|
||||
📅 Caduca: {new Date(lot.expiration_date).toLocaleDateString('es-ES')}
|
||||
</span>
|
||||
)}
|
||||
{lot.batch_number && (
|
||||
<span className="text-[var(--text-tertiary)]">
|
||||
Lote: {lot.batch_number}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteStockLot(item.id, lot.id)}
|
||||
className="p-1 text-[var(--text-secondary)] hover:text-[var(--color-error)] rounded"
|
||||
title="Eliminar lote"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAddingStock ? (
|
||||
<div className="p-3 bg-[var(--color-primary)]/5 border-2 border-[var(--color-primary)] rounded-lg">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
|
||||
Cantidad * <span className="text-[var(--text-tertiary)]">({item.unit_of_measure})</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={stockFormData.current_quantity}
|
||||
onChange={(e) => setStockFormData(prev => ({ ...prev, current_quantity: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-sm border rounded"
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
{stockErrors.current_quantity && (
|
||||
<p className="text-xs text-[var(--color-error)] mt-1">{stockErrors.current_quantity}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
|
||||
Fecha de caducidad
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={stockFormData.expiration_date}
|
||||
onChange={(e) => setStockFormData(prev => ({ ...prev, expiration_date: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-sm border rounded"
|
||||
/>
|
||||
{stockErrors.expiration_date && (
|
||||
<p className="text-xs text-[var(--color-error)] mt-1">{stockErrors.expiration_date}</p>
|
||||
)}
|
||||
{stockErrors.expiration_warning && (
|
||||
<p className="text-xs text-[var(--color-warning)] mt-1">{stockErrors.expiration_warning}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
|
||||
Proveedor
|
||||
</label>
|
||||
<select
|
||||
value={stockFormData.supplier_id}
|
||||
onChange={(e) => setStockFormData(prev => ({ ...prev, supplier_id: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-sm border rounded"
|
||||
>
|
||||
<option value="">Seleccionar...</option>
|
||||
{suppliers.map(s => (
|
||||
<option key={s.id} value={s.id}>{s.company_name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-[var(--text-primary)] mb-1">
|
||||
Número de lote
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stockFormData.batch_number}
|
||||
onChange={(e) => setStockFormData(prev => ({ ...prev, batch_number: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-sm border rounded"
|
||||
placeholder="Opcional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-tertiary)] italic">
|
||||
💡 Los lotes con fecha de caducidad se gestionarán automáticamente con FIFO
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleSaveStockLot(true)}
|
||||
className="px-3 py-1 text-xs bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border rounded transition-colors"
|
||||
>
|
||||
+ Agregar Otro Lote
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSaveStockLot(false)}
|
||||
className="px-3 py-1 text-xs bg-[var(--color-primary)] text-white rounded hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Guardar
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelStock}
|
||||
className="px-3 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleAddStockClick(item.id)}
|
||||
className="w-full px-3 py-2 text-xs border-2 border-dashed border-[var(--border-secondary)] hover:border-[var(--color-primary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors text-[var(--text-secondary)] hover:text-[var(--color-primary)] font-medium"
|
||||
>
|
||||
{lots.length === 0 ? '+ Agregar Stock Inicial (Opcional)' : '+ Agregar Otro Lote'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user