fix UI 1
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
ProductTransformationResponse,
|
||||
ProductionStage,
|
||||
DeletionSummary,
|
||||
BulkStockResponse,
|
||||
} from '../types/inventory';
|
||||
import { ApiError } from '../client';
|
||||
|
||||
@@ -342,7 +343,7 @@ export const useAddStock = (
|
||||
options?: UseMutationOptions<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
return useMutation<StockResponse, ApiError, { tenantId: string; stockData: StockCreate }>({
|
||||
mutationFn: ({ tenantId, stockData }) => inventoryService.addStock(tenantId, stockData),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
@@ -355,6 +356,30 @@ export const useAddStock = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useBulkAddStock = (
|
||||
options?: UseMutationOptions<BulkStockResponse, ApiError, { tenantId: string; stocks: StockCreate[] }>
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<BulkStockResponse, ApiError, { tenantId: string; stocks: StockCreate[] }>({
|
||||
mutationFn: ({ tenantId, stocks }) => inventoryService.bulkAddStock(tenantId, stocks),
|
||||
onSuccess: (data, { tenantId }) => {
|
||||
// Invalidate all stock queries since multiple ingredients may have been affected
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.stock.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: inventoryKeys.ingredients.lists() });
|
||||
// Invalidate per-ingredient stock queries
|
||||
data.results.forEach((result) => {
|
||||
if (result.success && result.stock) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: inventoryKeys.stock.byIngredient(tenantId, result.stock.ingredient_id)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateStock = (
|
||||
options?: UseMutationOptions<
|
||||
StockResponse,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
StockFilter,
|
||||
StockMovementCreate,
|
||||
StockMovementResponse,
|
||||
BulkStockResponse,
|
||||
// Operations
|
||||
StockConsumptionRequest,
|
||||
StockConsumptionResponse,
|
||||
@@ -162,6 +163,16 @@ export class InventoryService {
|
||||
);
|
||||
}
|
||||
|
||||
async bulkAddStock(
|
||||
tenantId: string,
|
||||
stocks: StockCreate[]
|
||||
): Promise<BulkStockResponse> {
|
||||
return apiClient.post<BulkStockResponse>(
|
||||
`${this.baseUrl}/${tenantId}/inventory/stock/bulk`,
|
||||
{ stocks }
|
||||
);
|
||||
}
|
||||
|
||||
async getStock(tenantId: string, stockId: string): Promise<StockResponse> {
|
||||
return apiClient.get<StockResponse>(
|
||||
`${this.baseUrl}/${tenantId}/inventory/stock/${stockId}`
|
||||
|
||||
@@ -330,6 +330,28 @@ export interface StockResponse {
|
||||
ingredient?: IngredientResponse | null;
|
||||
}
|
||||
|
||||
// ===== BULK STOCK SCHEMAS =====
|
||||
// Mirror: BulkStockCreate, BulkStockResult, BulkStockResponse from inventory.py
|
||||
|
||||
export interface BulkStockCreate {
|
||||
stocks: StockCreate[];
|
||||
}
|
||||
|
||||
export interface BulkStockResult {
|
||||
index: number;
|
||||
success: boolean;
|
||||
stock: StockResponse | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface BulkStockResponse {
|
||||
total_requested: number;
|
||||
total_created: number;
|
||||
total_failed: number;
|
||||
results: BulkStockResult[];
|
||||
transaction_id: string;
|
||||
}
|
||||
|
||||
// ===== STOCK MOVEMENT SCHEMAS =====
|
||||
// Mirror: StockMovementCreate from inventory.py:277
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SetupStepProps } from '../types';
|
||||
import { useIngredients, useCreateIngredient, useUpdateIngredient, useSoftDeleteIngredient, useAddStock, useStockByIngredient } from '../../../../api/hooks/inventory';
|
||||
import { useIngredients, useCreateIngredient, useUpdateIngredient, useSoftDeleteIngredient, useBulkAddStock } from '../../../../api/hooks/inventory';
|
||||
import { useSuppliers } from '../../../../api/hooks/suppliers';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useAuthUser } from '../../../../stores/auth.store';
|
||||
import { UnitOfMeasure, IngredientCategory, ProductionStage } from '../../../../api/types/inventory';
|
||||
import type { IngredientCreate, IngredientUpdate, StockCreate, StockResponse } from '../../../../api/types/inventory';
|
||||
import type { IngredientCreate, IngredientUpdate, StockCreate } from '../../../../api/types/inventory';
|
||||
import { ESSENTIAL_INGREDIENTS, COMMON_INGREDIENTS, PACKAGING_ITEMS, type IngredientTemplate, templateToIngredientCreate } from '../data/ingredientTemplates';
|
||||
|
||||
export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplete, onPrevious, canContinue }) => {
|
||||
@@ -29,7 +29,7 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
|
||||
const createIngredientMutation = useCreateIngredient();
|
||||
const updateIngredientMutation = useUpdateIngredient();
|
||||
const deleteIngredientMutation = useSoftDeleteIngredient();
|
||||
const addStockMutation = useAddStock();
|
||||
const bulkAddStockMutation = useBulkAddStock();
|
||||
|
||||
// Form state
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
@@ -59,8 +59,10 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
|
||||
});
|
||||
const [stockErrors, setStockErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Track stocks added per ingredient (for displaying the list)
|
||||
const [ingredientStocks, setIngredientStocks] = useState<Record<string, StockResponse[]>>({});
|
||||
// Track pending stocks to be submitted in batch (for display and batch submission)
|
||||
const [pendingStocks, setPendingStocks] = useState<StockCreate[]>([]);
|
||||
// Track stocks display per ingredient (using StockCreate for pending, before API submission)
|
||||
const [ingredientStocks, setIngredientStocks] = useState<Record<string, Array<StockCreate & { _tempId: string }>>>({});
|
||||
|
||||
// Notify parent when count changes
|
||||
useEffect(() => {
|
||||
@@ -270,55 +272,79 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
|
||||
const handleSaveStock = async (addAnother: boolean = false) => {
|
||||
if (!addingStockForId || !validateStockForm()) return;
|
||||
|
||||
try {
|
||||
const stockData: StockCreate = {
|
||||
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,
|
||||
lot_number: stockFormData.lot_number || undefined,
|
||||
production_stage: ProductionStage.RAW_INGREDIENT,
|
||||
quality_status: 'good',
|
||||
};
|
||||
const stockData: StockCreate = {
|
||||
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,
|
||||
lot_number: stockFormData.lot_number || undefined,
|
||||
production_stage: ProductionStage.RAW_INGREDIENT,
|
||||
quality_status: 'good',
|
||||
};
|
||||
|
||||
const result = await addStockMutation.mutateAsync({
|
||||
tenantId,
|
||||
stockData,
|
||||
// Generate a temporary ID for display purposes
|
||||
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
const stockWithTempId = { ...stockData, _tempId: tempId };
|
||||
|
||||
// Add to pending stocks for batch submission
|
||||
setPendingStocks(prev => [...prev, stockData]);
|
||||
|
||||
// Add to local state for display
|
||||
setIngredientStocks(prev => ({
|
||||
...prev,
|
||||
[addingStockForId]: [...(prev[addingStockForId] || []), stockWithTempId],
|
||||
}));
|
||||
|
||||
if (addAnother) {
|
||||
// Reset form for adding another lot
|
||||
setStockFormData({
|
||||
current_quantity: '',
|
||||
expiration_date: '',
|
||||
supplier_id: stockFormData.supplier_id, // Keep supplier selected
|
||||
batch_number: '',
|
||||
lot_number: '',
|
||||
});
|
||||
|
||||
// Add to local state for display
|
||||
setIngredientStocks(prev => ({
|
||||
...prev,
|
||||
[addingStockForId]: [...(prev[addingStockForId] || []), result],
|
||||
}));
|
||||
|
||||
if (addAnother) {
|
||||
// Reset form for adding another lot
|
||||
setStockFormData({
|
||||
current_quantity: '',
|
||||
expiration_date: '',
|
||||
supplier_id: stockFormData.supplier_id, // Keep supplier selected
|
||||
batch_number: '',
|
||||
lot_number: '',
|
||||
});
|
||||
setStockErrors({});
|
||||
} else {
|
||||
handleCancelStock();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding stock:', error);
|
||||
setStockErrors({ submit: t('common:error_saving', 'Error saving. Please try again.') });
|
||||
setStockErrors({});
|
||||
} else {
|
||||
handleCancelStock();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteStock = async (ingredientId: string, stockId: string) => {
|
||||
// Remove from local state
|
||||
const handleDeleteStock = (ingredientId: string, tempId: string) => {
|
||||
// Remove from local display state
|
||||
setIngredientStocks(prev => ({
|
||||
...prev,
|
||||
[ingredientId]: (prev[ingredientId] || []).filter(s => s.id !== stockId),
|
||||
[ingredientId]: (prev[ingredientId] || []).filter(s => s._tempId !== tempId),
|
||||
}));
|
||||
// Note: We don't delete from backend during setup - stocks are created and can be managed later
|
||||
// Remove from pending stocks
|
||||
setPendingStocks(prev => prev.filter((s) => {
|
||||
// Find and remove the matching stock entry
|
||||
const stocksForIngredient = ingredientStocks[ingredientId] || [];
|
||||
const stockToRemove = stocksForIngredient.find(st => st._tempId === tempId);
|
||||
if (!stockToRemove) return true;
|
||||
return s.ingredient_id !== stockToRemove.ingredient_id ||
|
||||
s.current_quantity !== stockToRemove.current_quantity ||
|
||||
s.batch_number !== stockToRemove.batch_number;
|
||||
}));
|
||||
};
|
||||
|
||||
// Submit all pending stocks when proceeding to next step
|
||||
const handleSubmitPendingStocks = async (): Promise<boolean> => {
|
||||
if (pendingStocks.length === 0) return true;
|
||||
|
||||
try {
|
||||
await bulkAddStockMutation.mutateAsync({
|
||||
tenantId,
|
||||
stocks: pendingStocks,
|
||||
});
|
||||
// Clear pending stocks after successful submission
|
||||
setPendingStocks([]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error submitting stocks:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const categoryOptions = [
|
||||
@@ -640,7 +666,7 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
|
||||
<div className="ml-6 space-y-1">
|
||||
{stocks.map((stock) => (
|
||||
<div
|
||||
key={stock.id}
|
||||
key={stock._tempId}
|
||||
className="flex items-center justify-between p-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded text-sm"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
@@ -659,7 +685,7 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteStock(ingredient.id, stock.id)}
|
||||
onClick={() => handleDeleteStock(ingredient.id, stock._tempId)}
|
||||
className="p-1 text-[var(--text-secondary)] hover:text-[var(--color-error)] rounded transition-colors"
|
||||
aria-label="Delete lot"
|
||||
>
|
||||
@@ -782,33 +808,19 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSaveStock(true)}
|
||||
disabled={addStockMutation.isPending}
|
||||
className="px-4 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-secondary)] text-[var(--text-primary)] rounded hover:bg-[var(--bg-secondary)] disabled:opacity-50 transition-colors"
|
||||
className="px-4 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-secondary)] text-[var(--text-primary)] rounded hover:bg-[var(--bg-secondary)] transition-colors"
|
||||
>
|
||||
{t('setup_wizard:inventory.add_another_lot', '+ Add Another Lot')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSaveStock(false)}
|
||||
disabled={addStockMutation.isPending}
|
||||
className="px-4 py-2 text-sm bg-[var(--color-primary)] text-white rounded hover:bg-[var(--color-primary-dark)] disabled:opacity-50 transition-colors flex items-center gap-1"
|
||||
className="px-4 py-2 text-sm bg-[var(--color-primary)] text-white rounded hover:bg-[var(--color-primary-dark)] transition-colors flex items-center gap-1"
|
||||
>
|
||||
{addStockMutation.isPending ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
{t('common:saving', 'Saving...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{t('common:save', 'Save')}
|
||||
</>
|
||||
)}
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{t('common:save', 'Save')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1021,13 +1033,33 @@ export const InventorySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onCompl
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{pendingStocks.length > 0 && (
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{pendingStocks.length} {t('setup_wizard:inventory.pending_stocks', 'stock entries pending')}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onComplete()}
|
||||
disabled={canContinue === false}
|
||||
onClick={async () => {
|
||||
const success = await handleSubmitPendingStocks();
|
||||
if (success) {
|
||||
onComplete();
|
||||
}
|
||||
}}
|
||||
disabled={canContinue === false || bulkAddStockMutation.isPending}
|
||||
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium flex items-center gap-2"
|
||||
>
|
||||
{t('common:next', 'Continuar →')}
|
||||
{bulkAddStockMutation.isPending ? (
|
||||
<>
|
||||
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
{t('common:saving', 'Saving...')}
|
||||
</>
|
||||
) : (
|
||||
t('common:next', 'Continuar →')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,10 +79,10 @@ const DemoPage = () => {
|
||||
// Helper function to calculate estimated progress based on elapsed time
|
||||
const calculateEstimatedProgress = (tier: string, startTime: number): number => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const duration = tier === 'enterprise' ? 90000 : 40000; // ms (90s for enterprise, 40s for professional)
|
||||
const duration = 5000; // ms (5s for both professional and enterprise)
|
||||
const linearProgress = Math.min(95, (elapsed / duration) * 100);
|
||||
// Logarithmic curve for natural feel - starts fast, slows down
|
||||
return Math.min(95, Math.round(linearProgress * (1 - Math.exp(-elapsed / 10000))));
|
||||
return Math.min(95, Math.round(linearProgress * (1 - Math.exp(-elapsed / 1000))));
|
||||
};
|
||||
|
||||
const demoOptions = [
|
||||
@@ -645,6 +645,7 @@ const DemoPage = () => {
|
||||
<div
|
||||
key={option.id}
|
||||
className={`
|
||||
flex flex-col
|
||||
bg-[var(--bg-primary)]
|
||||
border border-[var(--border-primary)]
|
||||
rounded-xl
|
||||
@@ -691,7 +692,7 @@ const DemoPage = () => {
|
||||
</div>
|
||||
|
||||
{/* Card Body */}
|
||||
<div className="p-6">
|
||||
<div className="p-6 flex-1">
|
||||
{/* Features List with Icons */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<h4 className="font-semibold text-[var(--text-primary)] text-sm uppercase tracking-wide mb-4">
|
||||
@@ -765,7 +766,7 @@ const DemoPage = () => {
|
||||
</div>
|
||||
|
||||
{/* Card Footer */}
|
||||
<div className="p-6 bg-[var(--bg-secondary)] border-t border-[var(--border-primary)]">
|
||||
<div className="px-6 py-4 bg-[var(--bg-secondary)] border-t border-[var(--border-primary)]">
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -774,7 +775,7 @@ const DemoPage = () => {
|
||||
disabled={creatingTier !== null}
|
||||
size="lg"
|
||||
isFullWidth={true}
|
||||
variant={option.tier === 'enterprise' ? 'gradient' : 'primary'}
|
||||
variant="gradient"
|
||||
className="font-semibold group"
|
||||
>
|
||||
{creatingTier === option.tier ? (
|
||||
|
||||
@@ -293,7 +293,7 @@ export const routesConfig: RouteConfig[] = [
|
||||
name: 'Distribution',
|
||||
component: 'DistributionPage',
|
||||
title: 'Distribución',
|
||||
icon: 'truck',
|
||||
icon: 'distribution',
|
||||
requiresAuth: true,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
|
||||
Reference in New Issue
Block a user