Fix new services implementation 2

This commit is contained in:
Urtzi Alfaro
2025-08-14 13:26:59 +02:00
parent 262b3dc9c4
commit 0951547e92
39 changed files with 1203 additions and 917 deletions

View File

@@ -392,6 +392,57 @@ private buildURL(endpoint: string): string {
return this.request<T>(endpoint, { ...config, method: 'DELETE' });
}
/**
* Raw request that returns the Response object for binary data
*/
async getRaw(endpoint: string, config?: RequestConfig): Promise<Response> {
const url = this.buildURL(endpoint);
const modifiedConfig = await this.applyRequestInterceptors(config || {});
const headers: Record<string, string> = {
...modifiedConfig.headers,
};
const fetchConfig: RequestInit = {
method: 'GET',
headers,
signal: AbortSignal.timeout(modifiedConfig.timeout || apiConfig.timeout),
};
// Add query parameters
const urlWithParams = new URL(url);
if (modifiedConfig.params) {
Object.entries(modifiedConfig.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
urlWithParams.searchParams.append(key, String(value));
}
});
}
const response = await fetch(urlWithParams.toString(), fetchConfig);
if (!response.ok) {
const errorText = await response.text();
let errorData: ApiError;
try {
errorData = JSON.parse(errorText);
} catch {
errorData = {
message: `HTTP ${response.status}: ${response.statusText}`,
detail: errorText,
code: `HTTP_${response.status}`,
};
}
const error = new Error(errorData.message || 'Request failed');
(error as any).response = { status: response.status, data: errorData };
throw error;
}
return response;
}
/**
* File upload with progress tracking
*/

View File

@@ -88,7 +88,7 @@ interface UseInventoryItemReturn {
// ========== MAIN INVENTORY HOOK ==========
export const useInventory = (autoLoad = true): UseInventoryReturn => {
const tenantId = useTenantId();
const { tenantId } = useTenantId();
// State
const [items, setItems] = useState<InventoryItem[]>([]);
@@ -373,7 +373,7 @@ export const useInventory = (autoLoad = true): UseInventoryReturn => {
// ========== DASHBOARD HOOK ==========
export const useInventoryDashboard = (): UseInventoryDashboardReturn => {
const tenantId = useTenantId();
const { tenantId } = useTenantId();
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
const [alerts, setAlerts] = useState<StockAlert[]>([]);
const [isLoading, setIsLoading] = useState(false);
@@ -419,7 +419,7 @@ export const useInventoryDashboard = (): UseInventoryDashboardReturn => {
// ========== SINGLE ITEM HOOK ==========
export const useInventoryItem = (itemId: string): UseInventoryItemReturn => {
const tenantId = useTenantId();
const { tenantId } = useTenantId();
const [item, setItem] = useState<InventoryItem | null>(null);
const [stockLevel, setStockLevel] = useState<StockLevel | null>(null);
const [recentMovements, setRecentMovements] = useState<StockMovement[]>([]);

View File

@@ -22,6 +22,23 @@ import {
} from '../services/suppliers.service';
import { useAuth } from './useAuth';
// Re-export types for component use
export type {
Supplier,
SupplierSummary,
CreateSupplierRequest,
UpdateSupplierRequest,
SupplierSearchParams,
SupplierStatistics,
PurchaseOrder,
CreatePurchaseOrderRequest,
PurchaseOrderSearchParams,
PurchaseOrderStatistics,
Delivery,
DeliverySearchParams,
DeliveryPerformanceStats
} from '../services/suppliers.service';
const suppliersService = new SuppliersService();
// ============================================================================
@@ -196,13 +213,13 @@ export function useSuppliers(): UseSuppliers {
// Create supplier
const createSupplier = useCallback(async (data: CreateSupplierRequest): Promise<Supplier | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setIsCreating(true);
setError(null);
const supplier = await suppliersService.createSupplier(user.tenant_id, user.user_id, data);
const supplier = await suppliersService.createSupplier(user.tenant_id, user.id, data);
// Refresh suppliers list
await loadSuppliers(currentParams);
@@ -217,17 +234,17 @@ export function useSuppliers(): UseSuppliers {
} finally {
setIsCreating(false);
}
}, [user?.tenant_id, user?.user_id, loadSuppliers, loadStatistics, currentParams]);
}, [user?.tenant_id, user?.id, loadSuppliers, loadStatistics, currentParams]);
// Update supplier
const updateSupplier = useCallback(async (supplierId: string, data: UpdateSupplierRequest): Promise<Supplier | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setIsUpdating(true);
setError(null);
const updatedSupplier = await suppliersService.updateSupplier(user.tenant_id, user.user_id, supplierId, data);
const updatedSupplier = await suppliersService.updateSupplier(user.tenant_id, user.id, supplierId, data);
// Update current supplier if it's the one being edited
if (supplier?.id === supplierId) {
@@ -246,7 +263,7 @@ export function useSuppliers(): UseSuppliers {
} finally {
setIsUpdating(false);
}
}, [user?.tenant_id, user?.user_id, supplier?.id, loadSuppliers, currentParams]);
}, [user?.tenant_id, user?.id, supplier?.id, loadSuppliers, currentParams]);
// Delete supplier
const deleteSupplier = useCallback(async (supplierId: string): Promise<boolean> => {
@@ -277,12 +294,12 @@ export function useSuppliers(): UseSuppliers {
// Approve/reject supplier
const approveSupplier = useCallback(async (supplierId: string, action: 'approve' | 'reject', notes?: string): Promise<Supplier | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setError(null);
const updatedSupplier = await suppliersService.approveSupplier(user.tenant_id, user.user_id, supplierId, action, notes);
const updatedSupplier = await suppliersService.approveSupplier(user.tenant_id, user.id, supplierId, action, notes);
// Update current supplier if it's the one being approved/rejected
if (supplier?.id === supplierId) {
@@ -300,7 +317,7 @@ export function useSuppliers(): UseSuppliers {
setError(errorMessage);
return null;
}
}, [user?.tenant_id, user?.user_id, supplier?.id, loadSuppliers, loadStatistics, currentParams]);
}, [user?.tenant_id, user?.id, supplier?.id, loadSuppliers, loadStatistics, currentParams]);
// Clear error
const clearError = useCallback(() => {
@@ -504,13 +521,13 @@ export function usePurchaseOrders(): UsePurchaseOrders {
}, [user?.tenant_id]);
const createPurchaseOrder = useCallback(async (data: CreatePurchaseOrderRequest): Promise<PurchaseOrder | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setIsCreating(true);
setError(null);
const order = await suppliersService.createPurchaseOrder(user.tenant_id, user.user_id, data);
const order = await suppliersService.createPurchaseOrder(user.tenant_id, user.id, data);
// Refresh orders list
await loadPurchaseOrders(currentParams);
@@ -525,15 +542,15 @@ export function usePurchaseOrders(): UsePurchaseOrders {
} finally {
setIsCreating(false);
}
}, [user?.tenant_id, user?.user_id, loadPurchaseOrders, loadStatistics, currentParams]);
}, [user?.tenant_id, user?.id, loadPurchaseOrders, loadStatistics, currentParams]);
const updateOrderStatus = useCallback(async (poId: string, status: string, notes?: string): Promise<PurchaseOrder | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setError(null);
const updatedOrder = await suppliersService.updatePurchaseOrderStatus(user.tenant_id, user.user_id, poId, status, notes);
const updatedOrder = await suppliersService.updatePurchaseOrderStatus(user.tenant_id, user.id, poId, status, notes);
if (purchaseOrder?.id === poId) {
setPurchaseOrder(updatedOrder);
@@ -548,15 +565,15 @@ export function usePurchaseOrders(): UsePurchaseOrders {
setError(errorMessage);
return null;
}
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
}, [user?.tenant_id, user?.id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
const approveOrder = useCallback(async (poId: string, action: 'approve' | 'reject', notes?: string): Promise<PurchaseOrder | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setError(null);
const updatedOrder = await suppliersService.approvePurchaseOrder(user.tenant_id, user.user_id, poId, action, notes);
const updatedOrder = await suppliersService.approvePurchaseOrder(user.tenant_id, user.id, poId, action, notes);
if (purchaseOrder?.id === poId) {
setPurchaseOrder(updatedOrder);
@@ -572,15 +589,15 @@ export function usePurchaseOrders(): UsePurchaseOrders {
setError(errorMessage);
return null;
}
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, loadOrdersRequiringApproval, currentParams]);
}, [user?.tenant_id, user?.id, purchaseOrder?.id, loadPurchaseOrders, loadOrdersRequiringApproval, currentParams]);
const sendToSupplier = useCallback(async (poId: string, sendEmail: boolean = true): Promise<PurchaseOrder | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setError(null);
const updatedOrder = await suppliersService.sendToSupplier(user.tenant_id, user.user_id, poId, sendEmail);
const updatedOrder = await suppliersService.sendToSupplier(user.tenant_id, user.id, poId, sendEmail);
if (purchaseOrder?.id === poId) {
setPurchaseOrder(updatedOrder);
@@ -595,15 +612,15 @@ export function usePurchaseOrders(): UsePurchaseOrders {
setError(errorMessage);
return null;
}
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
}, [user?.tenant_id, user?.id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
const cancelOrder = useCallback(async (poId: string, reason: string): Promise<PurchaseOrder | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setError(null);
const updatedOrder = await suppliersService.cancelPurchaseOrder(user.tenant_id, user.user_id, poId, reason);
const updatedOrder = await suppliersService.cancelPurchaseOrder(user.tenant_id, user.id, poId, reason);
if (purchaseOrder?.id === poId) {
setPurchaseOrder(updatedOrder);
@@ -618,7 +635,7 @@ export function usePurchaseOrders(): UsePurchaseOrders {
setError(errorMessage);
return null;
}
}, [user?.tenant_id, user?.user_id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
}, [user?.tenant_id, user?.id, purchaseOrder?.id, loadPurchaseOrders, currentParams]);
const clearError = useCallback(() => {
setError(null);
@@ -804,12 +821,12 @@ export function useDeliveries(): UseDeliveries {
}, [user?.tenant_id]);
const updateDeliveryStatus = useCallback(async (deliveryId: string, status: string, notes?: string): Promise<Delivery | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setError(null);
const updatedDelivery = await suppliersService.updateDeliveryStatus(user.tenant_id, user.user_id, deliveryId, status, notes);
const updatedDelivery = await suppliersService.updateDeliveryStatus(user.tenant_id, user.id, deliveryId, status, notes);
if (delivery?.id === deliveryId) {
setDelivery(updatedDelivery);
@@ -824,15 +841,15 @@ export function useDeliveries(): UseDeliveries {
setError(errorMessage);
return null;
}
}, [user?.tenant_id, user?.user_id, delivery?.id, loadDeliveries, currentParams]);
}, [user?.tenant_id, user?.id, delivery?.id, loadDeliveries, currentParams]);
const receiveDelivery = useCallback(async (deliveryId: string, receiptData: any): Promise<Delivery | null> => {
if (!user?.tenant_id || !user?.user_id) return null;
if (!user?.tenant_id || !user?.id) return null;
try {
setError(null);
const updatedDelivery = await suppliersService.receiveDelivery(user.tenant_id, user.user_id, deliveryId, receiptData);
const updatedDelivery = await suppliersService.receiveDelivery(user.tenant_id, user.id, deliveryId, receiptData);
if (delivery?.id === deliveryId) {
setDelivery(updatedDelivery);
@@ -847,7 +864,7 @@ export function useDeliveries(): UseDeliveries {
setError(errorMessage);
return null;
}
}, [user?.tenant_id, user?.user_id, delivery?.id, loadDeliveries, currentParams]);
}, [user?.tenant_id, user?.id, delivery?.id, loadDeliveries, currentParams]);
const clearError = useCallback(() => {
setError(null);

View File

@@ -43,6 +43,7 @@ export interface InventoryItem {
supplier?: string;
notes?: string;
barcode?: string;
sku?: string;
cost_per_unit?: number;
is_active: boolean;
created_at: string;

View File

@@ -211,7 +211,20 @@ export class OnboardingService {
suggestions: suggestions.map(s => ({
suggestion_id: s.suggestion_id,
approved: s.user_approved ?? true,
modifications: s.user_modifications || {}
modifications: s.user_modifications || {},
// Include full suggestion data for backend processing
original_name: s.original_name,
suggested_name: s.suggested_name,
product_type: s.product_type,
category: s.category,
unit_of_measure: s.unit_of_measure,
confidence_score: s.confidence_score,
estimated_shelf_life_days: s.estimated_shelf_life_days,
requires_refrigeration: s.requires_refrigeration,
requires_freezing: s.requires_freezing,
is_seasonal: s.is_seasonal,
suggested_supplier: s.suggested_supplier,
notes: s.notes
}))
});
}

View File

@@ -367,14 +367,14 @@ export class RecipesService {
headers: { 'X-Tenant-ID': tenantId },
params
});
return response.data;
return response;
}
async getRecipe(tenantId: string, recipeId: string): Promise<Recipe> {
const response = await apiClient.get<Recipe>(`${this.baseUrl}/recipes/${recipeId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
return response;
}
async createRecipe(tenantId: string, userId: string, data: CreateRecipeRequest): Promise<Recipe> {
@@ -384,7 +384,7 @@ export class RecipesService {
'X-User-ID': userId
}
});
return response.data;
return response;
}
async updateRecipe(tenantId: string, userId: string, recipeId: string, data: UpdateRecipeRequest): Promise<Recipe> {
@@ -394,7 +394,7 @@ export class RecipesService {
'X-User-ID': userId
}
});
return response.data;
return response;
}
async deleteRecipe(tenantId: string, recipeId: string): Promise<void> {
@@ -413,7 +413,7 @@ export class RecipesService {
}
}
);
return response.data;
return response;
}
async activateRecipe(tenantId: string, userId: string, recipeId: string): Promise<Recipe> {
@@ -423,7 +423,7 @@ export class RecipesService {
'X-User-ID': userId
}
});
return response.data;
return response;
}
async checkRecipeFeasibility(tenantId: string, recipeId: string, batchMultiplier: number = 1.0): Promise<RecipeFeasibility> {
@@ -431,21 +431,21 @@ export class RecipesService {
headers: { 'X-Tenant-ID': tenantId },
params: { batch_multiplier: batchMultiplier }
});
return response.data;
return response;
}
async getRecipeStatistics(tenantId: string): Promise<RecipeStatistics> {
const response = await apiClient.get<RecipeStatistics>(`${this.baseUrl}/recipes/statistics/dashboard`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
return response;
}
async getRecipeCategories(tenantId: string): Promise<string[]> {
const response = await apiClient.get<{ categories: string[] }>(`${this.baseUrl}/recipes/categories/list`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data.categories;
return response.categories;
}
// Production Management
@@ -454,14 +454,14 @@ export class RecipesService {
headers: { 'X-Tenant-ID': tenantId },
params
});
return response.data;
return response;
}
async getProductionBatch(tenantId: string, batchId: string): Promise<ProductionBatch> {
const response = await apiClient.get<ProductionBatch>(`${this.baseUrl}/production/batches/${batchId}`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
return response;
}
async createProductionBatch(tenantId: string, userId: string, data: CreateProductionBatchRequest): Promise<ProductionBatch> {
@@ -471,7 +471,7 @@ export class RecipesService {
'X-User-ID': userId
}
});
return response.data;
return response;
}
async updateProductionBatch(tenantId: string, userId: string, batchId: string, data: UpdateProductionBatchRequest): Promise<ProductionBatch> {
@@ -481,7 +481,7 @@ export class RecipesService {
'X-User-ID': userId
}
});
return response.data;
return response;
}
async deleteProductionBatch(tenantId: string, batchId: string): Promise<void> {
@@ -494,7 +494,7 @@ export class RecipesService {
const response = await apiClient.get<ProductionBatch[]>(`${this.baseUrl}/production/batches/active/list`, {
headers: { 'X-Tenant-ID': tenantId }
});
return response.data;
return response;
}
async startProductionBatch(tenantId: string, userId: string, batchId: string, data: {
@@ -519,7 +519,7 @@ export class RecipesService {
'X-User-ID': userId
}
});
return response.data;
return response;
}
async completeProductionBatch(tenantId: string, userId: string, batchId: string, data: {
@@ -538,7 +538,7 @@ export class RecipesService {
'X-User-ID': userId
}
});
return response.data;
return response;
}
async getProductionStatistics(tenantId: string, startDate?: string, endDate?: string): Promise<ProductionStatistics> {
@@ -546,6 +546,6 @@ export class RecipesService {
headers: { 'X-Tenant-ID': tenantId },
params: { start_date: startDate, end_date: endDate }
});
return response.data;
return response;
}
}

View File

@@ -361,7 +361,7 @@ export class SuppliersService {
`${this.baseUrl}?${searchParams.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getSupplier(tenantId: string, supplierId: string): Promise<Supplier> {
@@ -369,7 +369,7 @@ export class SuppliersService {
`${this.baseUrl}/${supplierId}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async createSupplier(tenantId: string, userId: string, data: CreateSupplierRequest): Promise<Supplier> {
@@ -378,7 +378,7 @@ export class SuppliersService {
data,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async updateSupplier(tenantId: string, userId: string, supplierId: string, data: UpdateSupplierRequest): Promise<Supplier> {
@@ -387,7 +387,7 @@ export class SuppliersService {
data,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async deleteSupplier(tenantId: string, supplierId: string): Promise<void> {
@@ -403,7 +403,7 @@ export class SuppliersService {
{ action, notes },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
// Supplier Analytics & Lists
@@ -412,7 +412,7 @@ export class SuppliersService {
`${this.baseUrl}/statistics`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getActiveSuppliers(tenantId: string): Promise<SupplierSummary[]> {
@@ -420,7 +420,7 @@ export class SuppliersService {
`${this.baseUrl}/active`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getTopSuppliers(tenantId: string, limit: number = 10): Promise<SupplierSummary[]> {
@@ -428,7 +428,7 @@ export class SuppliersService {
`${this.baseUrl}/top?limit=${limit}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getSuppliersByType(tenantId: string, supplierType: string): Promise<SupplierSummary[]> {
@@ -436,7 +436,7 @@ export class SuppliersService {
`${this.baseUrl}/types/${supplierType}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getSuppliersNeedingReview(tenantId: string, daysSinceLastOrder: number = 30): Promise<SupplierSummary[]> {
@@ -444,7 +444,7 @@ export class SuppliersService {
`${this.baseUrl}/pending-review?days_since_last_order=${daysSinceLastOrder}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
// Purchase Orders
@@ -463,7 +463,7 @@ export class SuppliersService {
`/api/v1/purchase-orders?${searchParams.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getPurchaseOrder(tenantId: string, poId: string): Promise<PurchaseOrder> {
@@ -471,7 +471,7 @@ export class SuppliersService {
`/api/v1/purchase-orders/${poId}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async createPurchaseOrder(tenantId: string, userId: string, data: CreatePurchaseOrderRequest): Promise<PurchaseOrder> {
@@ -480,7 +480,7 @@ export class SuppliersService {
data,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async updatePurchaseOrderStatus(tenantId: string, userId: string, poId: string, status: string, notes?: string): Promise<PurchaseOrder> {
@@ -489,7 +489,7 @@ export class SuppliersService {
{ status, notes },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async approvePurchaseOrder(tenantId: string, userId: string, poId: string, action: 'approve' | 'reject', notes?: string): Promise<PurchaseOrder> {
@@ -498,7 +498,7 @@ export class SuppliersService {
{ action, notes },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async sendToSupplier(tenantId: string, userId: string, poId: string, sendEmail: boolean = true): Promise<PurchaseOrder> {
@@ -507,7 +507,7 @@ export class SuppliersService {
{},
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async cancelPurchaseOrder(tenantId: string, userId: string, poId: string, reason: string): Promise<PurchaseOrder> {
@@ -516,7 +516,7 @@ export class SuppliersService {
{},
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async getPurchaseOrderStatistics(tenantId: string): Promise<PurchaseOrderStatistics> {
@@ -524,7 +524,7 @@ export class SuppliersService {
'/api/v1/purchase-orders/statistics',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getOrdersRequiringApproval(tenantId: string): Promise<PurchaseOrder[]> {
@@ -532,7 +532,7 @@ export class SuppliersService {
'/api/v1/purchase-orders/pending-approval',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getOverdueOrders(tenantId: string): Promise<PurchaseOrder[]> {
@@ -540,7 +540,7 @@ export class SuppliersService {
'/api/v1/purchase-orders/overdue',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
// Deliveries
@@ -558,7 +558,7 @@ export class SuppliersService {
`/api/v1/deliveries?${searchParams.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getDelivery(tenantId: string, deliveryId: string): Promise<Delivery> {
@@ -566,7 +566,7 @@ export class SuppliersService {
`/api/v1/deliveries/${deliveryId}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getTodaysDeliveries(tenantId: string): Promise<Delivery[]> {
@@ -574,7 +574,7 @@ export class SuppliersService {
'/api/v1/deliveries/today',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async getOverdueDeliveries(tenantId: string): Promise<Delivery[]> {
@@ -582,7 +582,7 @@ export class SuppliersService {
'/api/v1/deliveries/overdue',
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
async updateDeliveryStatus(tenantId: string, userId: string, deliveryId: string, status: string, notes?: string): Promise<Delivery> {
@@ -591,7 +591,7 @@ export class SuppliersService {
{ status, notes, update_timestamps: true },
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async receiveDelivery(tenantId: string, userId: string, deliveryId: string, receiptData: {
@@ -605,7 +605,7 @@ export class SuppliersService {
receiptData,
{ headers: { 'X-Tenant-ID': tenantId, 'X-User-ID': userId } }
);
return response.data;
return response;
}
async getDeliveryPerformanceStats(tenantId: string, daysBack: number = 30, supplierId?: string): Promise<DeliveryPerformanceStats> {
@@ -617,6 +617,6 @@ export class SuppliersService {
`/api/v1/deliveries/performance-stats?${params.toString()}`,
{ headers: { 'X-Tenant-ID': tenantId } }
);
return response.data;
return response;
}
}

View File

@@ -39,4 +39,16 @@ export interface BaseQueryParams {
search?: string;
sort?: string;
order?: 'asc' | 'desc';
}
export interface CreateResponse<T = any> {
data: T;
message?: string;
status: string;
}
export interface UpdateResponse<T = any> {
data: T;
message?: string;
status: string;
}

View File

@@ -18,6 +18,15 @@ export interface SalesData {
source: string;
created_at: string;
external_factors?: ExternalFactors;
// Additional properties used by components
sales_channel?: string;
is_validated?: boolean;
cost_of_goods?: number;
revenue?: number;
quantity_sold?: number;
inventory_product_id?: string;
discount_applied?: number;
weather_condition?: string;
}
export interface SalesValidationResult {
@@ -53,6 +62,10 @@ export interface SalesDataQuery extends BaseQueryParams {
max_quantity?: number;
min_revenue?: number;
max_revenue?: number;
search_term?: string;
sales_channel?: string;
inventory_product_id?: string;
is_validated?: boolean;
}
export interface SalesDataImport {

View File

@@ -14,6 +14,8 @@ export interface TenantInfo {
settings?: TenantSettings;
subscription?: TenantSubscription;
location?: TenantLocation;
business_type?: 'bakery' | 'coffee_shop' | 'pastry_shop' | 'restaurant';
business_model?: 'individual_bakery' | 'central_baker_satellite' | 'retail_bakery' | 'hybrid_bakery';
}
export interface TenantSettings {
@@ -62,7 +64,8 @@ export interface TenantSubscription {
export interface TenantCreate {
name: string;
address?: string;
business_type?: 'individual' | 'central_workshop';
business_type?: 'bakery' | 'coffee_shop' | 'pastry_shop' | 'restaurant';
business_model?: 'individual_bakery' | 'central_baker_satellite' | 'retail_bakery' | 'hybrid_bakery';
postal_code: string;
phone: string;
description?: string;

View File

@@ -120,8 +120,8 @@ export const useTrainingWebSocket = (jobId: string, tenantId?: string) => {
const config = {
url: actualTenantId
? `ws://localhost:8002/api/v1/ws/tenants/${actualTenantId}/training/jobs/${jobId}/live`
: `ws://localhost:8002/api/v1/ws/tenants/unknown/training/jobs/${jobId}/live`,
? `ws://localhost:8000/api/v1/ws/tenants/${actualTenantId}/training/jobs/${jobId}/live`
: `ws://localhost:8000/api/v1/ws/tenants/unknown/training/jobs/${jobId}/live`,
reconnect: true,
reconnectInterval: 3000,
maxReconnectAttempts: 10

View File

@@ -16,7 +16,9 @@ import {
CheckCircle2,
XCircle,
ArrowRight,
Lightbulb
Lightbulb,
Building2,
Truck
} from 'lucide-react';
import toast from 'react-hot-toast';
@@ -236,6 +238,43 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
const renderBusinessModelInsight = (analysis: BusinessModelAnalysis) => {
const modelConfig = {
individual_bakery: {
icon: Factory,
title: 'Panadería Individual',
description: 'Producción completa desde ingredientes básicos (harina, levadura, etc.)',
color: 'blue',
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
textColor: 'text-blue-900'
},
central_baker_satellite: {
icon: Truck,
title: 'Punto de Venta - Obrador Central',
description: 'Recibe productos semi-elaborados y los finaliza (horneado, decoración)',
color: 'amber',
bgColor: 'bg-amber-50',
borderColor: 'border-amber-200',
textColor: 'text-amber-900'
},
retail_bakery: {
icon: Store,
title: 'Panadería de Distribución',
description: 'Vende productos terminados de proveedores externos',
color: 'green',
bgColor: 'bg-green-50',
borderColor: 'border-green-200',
textColor: 'text-green-900'
},
hybrid_bakery: {
icon: Settings2,
title: 'Modelo Mixto',
description: 'Combina producción propia con productos de proveedores',
color: 'purple',
bgColor: 'bg-purple-50',
borderColor: 'border-purple-200',
textColor: 'text-purple-900'
},
// Legacy fallbacks
production: {
icon: Factory,
title: 'Panadería de Producción',
@@ -265,7 +304,8 @@ const SmartHistoricalDataImport: React.FC<SmartHistoricalDataImportProps> = ({
}
};
const config = modelConfig[analysis.model];
// Provide fallback if analysis.model is not in modelConfig
const config = modelConfig[analysis.model as keyof typeof modelConfig] || modelConfig.hybrid_bakery;
const IconComponent = config.icon;
return (

View File

@@ -43,8 +43,8 @@ const SalesAnalyticsDashboard: React.FC = () => {
} = useSales();
const {
ingredients: products,
loadIngredients: loadProducts,
items: products,
loadItems: loadProducts,
isLoading: inventoryLoading
} = useInventory();
@@ -159,11 +159,14 @@ const SalesAnalyticsDashboard: React.FC = () => {
// Top products
const topProducts = Object.entries(productPerformance)
.map(([productId, data]) => ({
productId,
...data as any,
avgPrice: data.revenue / data.units
}))
.map(([productId, data]) => {
const perf = data as { revenue: number; units: number; orders: number };
return {
productId,
...perf,
avgPrice: perf.revenue / perf.units
};
})
.sort((a, b) => b.revenue - a.revenue)
.slice(0, 5);

View File

@@ -160,7 +160,7 @@ const SalesDashboardWidget: React.FC<SalesDashboardWidgetProps> = ({
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-gray-900">Ventas de Hoy</h3>
{onViewAll && (
<Button variant="ghost" size="sm" onClick={onViewAll}>
<Button variant="outline" size="sm" onClick={onViewAll}>
<Eye className="w-4 h-4" />
</Button>
)}

View File

@@ -10,7 +10,7 @@ import {
Package,
ShoppingCart,
MapPin,
Grid3X3,
Grid,
List,
AlertCircle,
TrendingUp,
@@ -51,8 +51,8 @@ const SalesManagementPage: React.FC = () => {
} = useSales();
const {
ingredients: products,
loadIngredients: loadProducts,
items: products,
loadItems: loadProducts,
isLoading: inventoryLoading
} = useInventory();
@@ -89,7 +89,9 @@ const SalesManagementPage: React.FC = () => {
const loadSalesData = async () => {
if (!user?.tenant_id) return;
const query: SalesDataQuery = {};
const query: SalesDataQuery = {
tenant_id: user.tenant_id
};
if (filters.search) {
query.search_term = filters.search;
@@ -155,7 +157,9 @@ const SalesManagementPage: React.FC = () => {
const handleExport = async () => {
if (!user?.tenant_id) return;
const query: SalesDataQuery = {};
const query: SalesDataQuery = {
tenant_id: user.tenant_id
};
if (filters.date_from) query.start_date = filters.date_from;
if (filters.date_to) query.end_date = filters.date_to;
if (filters.channel) query.sales_channel = filters.channel;
@@ -386,7 +390,7 @@ const SalesManagementPage: React.FC = () => {
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<Grid3X3 className="w-4 h-4" />
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}

View File

@@ -11,7 +11,7 @@ import {
Clock,
Star,
ArrowRight,
LightBulb,
Lightbulb,
Calendar,
Package
} from 'lucide-react';
@@ -53,10 +53,10 @@ const SalesPerformanceInsights: React.FC<SalesPerformanceInsightsProps> = ({
} = useSales();
const {
predictions,
loadPredictions,
performance,
loadPerformance,
forecasts,
getForecasts,
quickForecasts,
getQuickForecasts,
isLoading: forecastLoading
} = useForecast();
@@ -87,8 +87,8 @@ const SalesPerformanceInsights: React.FC<SalesPerformanceInsightsProps> = ({
end_date: endDate,
limit: 1000
}),
loadPredictions(),
loadPerformance()
getForecasts(user.tenant_id),
getQuickForecasts(user.tenant_id)
]);
setSalesAnalytics(analytics);
@@ -200,9 +200,9 @@ const SalesPerformanceInsights: React.FC<SalesPerformanceInsightsProps> = ({
}
// Forecasting insights
if (predictions.length > 0) {
const todayPrediction = predictions.find(p => {
const predDate = new Date(p.date).toDateString();
if (forecasts.length > 0) {
const todayPrediction = forecasts.find(p => {
const predDate = new Date(p.forecast_date).toDateString();
const today = new Date().toDateString();
return predDate === today;
});
@@ -213,8 +213,8 @@ const SalesPerformanceInsights: React.FC<SalesPerformanceInsightsProps> = ({
type: 'forecast',
title: 'Predicción para hoy',
description: `La IA predice ${todayPrediction.predicted_demand} unidades de demanda con ${
todayPrediction.confidence === 'high' ? 'alta' :
todayPrediction.confidence === 'medium' ? 'media' : 'baja'
(todayPrediction.confidence_level || 0) > 0.8 ? 'alta' :
(todayPrediction.confidence_level || 0) > 0.6 ? 'media' : 'baja'
} confianza.`,
value: `${todayPrediction.predicted_demand} unidades`,
priority: 'high',
@@ -227,8 +227,9 @@ const SalesPerformanceInsights: React.FC<SalesPerformanceInsightsProps> = ({
}
// Performance vs forecast insight
if (performance) {
const accuracy = performance.accuracy || 0;
if (quickForecasts.length > 0) {
const latestForecast = quickForecasts[0];
const accuracy = latestForecast.confidence_score || 0;
if (accuracy > 85) {
insights.push({
id: 'forecast_accuracy',
@@ -288,7 +289,7 @@ const SalesPerformanceInsights: React.FC<SalesPerformanceInsightsProps> = ({
// Sort by priority
const priorityOrder = { high: 3, medium: 2, low: 1 };
return insights.sort((a, b) => priorityOrder[b.priority] - priorityOrder[a.priority]);
}, [salesAnalytics, salesData, predictions, performance, onActionClick]);
}, [salesAnalytics, salesData, forecasts, quickForecasts, onActionClick]);
// Get insight icon
const getInsightIcon = (type: PerformanceInsight['type']) => {
@@ -301,7 +302,7 @@ const SalesPerformanceInsights: React.FC<SalesPerformanceInsightsProps> = ({
return Brain;
case 'info':
default:
return LightBulb;
return Lightbulb;
}
};

View File

@@ -11,7 +11,7 @@ import {
CheckCircle,
Calendar,
TrendingUp,
Grid3X3,
Grid,
List,
Download,
MapPin,
@@ -486,7 +486,7 @@ const DeliveryTrackingPage: React.FC = () => {
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<Grid3X3 className="w-4 h-4" />
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}

View File

@@ -100,7 +100,7 @@ const PurchaseOrderForm: React.FC<PurchaseOrderFormProps> = ({
const [activeTab, setActiveTab] = useState<'basic' | 'delivery' | 'items' | 'financial'>('basic');
const { activeSuppliers, loadActiveSuppliers } = useSuppliers();
const { ingredients, loadInventoryItems } = useInventory();
const { items: ingredients, loadItems: loadInventoryItems } = useInventory();
// Initialize form data when order changes
useEffect(() => {

View File

@@ -12,7 +12,7 @@ import {
AlertCircle,
Package,
DollarSign,
Grid3X3,
Grid,
List
} from 'lucide-react';
@@ -446,7 +446,7 @@ const PurchaseOrderManagementPage: React.FC = () => {
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<Grid3X3 className="w-4 h-4" />
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
@@ -599,7 +599,7 @@ const PurchaseOrderManagementPage: React.FC = () => {
isOpen={showPurchaseOrderForm}
isCreating={isCreating}
onSubmit={selectedOrder ?
(data) => {
async (data) => {
// Handle update logic here if needed
setShowPurchaseOrderForm(false);
setSelectedOrder(null);

View File

@@ -43,6 +43,8 @@ interface SupplierCostData extends SupplierSummary {
market_share_percentage: number;
cost_trend: 'increasing' | 'decreasing' | 'stable';
cost_efficiency_score: number;
total_amount: number;
quality_rating: number;
}
const SupplierCostAnalysis: React.FC = () => {

View File

@@ -12,7 +12,7 @@ import {
AlertCircle,
Package,
DollarSign,
Grid3X3,
Grid,
List
} from 'lucide-react';
@@ -422,7 +422,7 @@ const SupplierManagementPage: React.FC = () => {
onClick={() => setViewMode('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<Grid3X3 className="w-4 h-4" />
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}

View File

@@ -42,6 +42,10 @@ interface SupplierPerformance extends SupplierSummary {
cost_efficiency: number;
response_time: number;
quality_consistency: number;
total_orders: number;
total_amount: number;
quality_rating: number;
delivery_rating: number;
}
const SupplierPerformanceReport: React.FC = () => {

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader, TrendingUp } from 'lucide-react';
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader, TrendingUp } from 'lucide-react';
import toast from 'react-hot-toast';
import SimplifiedTrainingProgress from '../../components/SimplifiedTrainingProgress';
@@ -25,7 +25,7 @@ interface OnboardingPageProps {
interface BakeryData {
name: string;
address: string;
businessType: 'individual' | 'central_workshop';
businessType?: 'bakery' | 'coffee_shop' | 'pastry_shop' | 'restaurant'; // Will be auto-detected from sales data
coordinates?: { lat: number; lng: number };
products: string[];
hasHistoricalData: boolean;
@@ -51,7 +51,6 @@ const MADRID_PRODUCTS = [
const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) => {
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [useSmartImport, setUseSmartImport] = useState(true); // New state for smart import
const manualNavigation = useRef(false);
// Enhanced onboarding with progress tracking
@@ -64,7 +63,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
const [bakeryData, setBakeryData] = useState<BakeryData>({
name: '',
address: '',
businessType: 'individual',
// businessType will be auto-detected during smart data import
products: MADRID_PRODUCTS, // Automatically assign all products
hasHistoricalData: false
});
@@ -95,14 +94,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
estimatedTimeRemaining: 0
});
// Sales data validation state
const [validationStatus, setValidationStatus] = useState<{
status: 'idle' | 'validating' | 'valid' | 'invalid';
message?: string;
records?: number;
}>({
status: 'idle'
});
// Initialize current step from onboarding progress (only when not manually navigating)
useEffect(() => {
@@ -292,8 +283,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
try {
if (currentStep === 1) {
await createBakeryAndTenant();
} else if (currentStep === 2) {
await uploadAndValidateSalesData();
// Smart import step handles its own processing
}
} catch (error) {
toast.error('Error al procesar el paso. Por favor, inténtalo de nuevo.');
@@ -317,7 +307,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
const tenantData: TenantCreate = {
name: bakeryData.name,
address: bakeryData.address,
business_type: "individual",
business_type: 'bakery', // Default value - will be automatically updated after AI analyzes sales data
postal_code: "28010",
phone: "+34655334455",
coordinates: bakeryData.coordinates,
@@ -337,7 +327,8 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
await completeStep('bakery_registered', {
bakery_name: bakeryData.name,
bakery_address: bakeryData.address,
business_type: bakeryData.businessType,
business_type: 'bakery', // Default - will be auto-detected from sales data
auto_detect_business_type: true, // Flag indicating business type will be auto-detected
tenant_id: currentTenantId, // Use the correct tenant ID
user_id: user?.id
});
@@ -354,81 +345,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
}
};
// Validate sales data without importing
const validateSalesFile = async () => {
if (!tenantId || !bakeryData.csvFile) {
toast.error('Falta información del tenant o archivo');
return;
}
setValidationStatus({ status: 'validating' });
try {
const validationResult = await validateSalesData(tenantId, bakeryData.csvFile);
if (validationResult.is_valid) {
setValidationStatus({
status: 'valid',
message: validationResult.message,
records: validationResult.details?.total_records || 0
});
toast.success('¡Archivo validado correctamente!');
} else {
setValidationStatus({
status: 'invalid',
message: validationResult.message
});
toast.error(`Error en validación: ${validationResult.message}`);
}
} catch (error: any) {
setValidationStatus({
status: 'invalid',
message: 'Error al validar el archivo'
});
toast.error('Error al validar el archivo');
}
};
// Upload and validate sales data for step 2
const uploadAndValidateSalesData = async () => {
if (!tenantId || !bakeryData.csvFile) {
throw new Error('Falta información del tenant o archivo');
}
try {
// If not already validated, validate first
if (validationStatus.status !== 'valid') {
const validationResult = await validateSalesData(tenantId, bakeryData.csvFile);
if (!validationResult.is_valid) {
toast.error(`Error en validación: ${validationResult.message}`);
return;
}
}
// If validation passes, upload the data
const uploadResult = await uploadSalesHistory(tenantId, bakeryData.csvFile);
// Mark step as completed
await completeStep('sales_data_uploaded', {
file_name: bakeryData.csvFile.name,
file_size: bakeryData.csvFile.size,
records_imported: uploadResult.records_imported || 0,
validation_status: 'success',
tenant_id: tenantId,
user_id: user?.id
});
toast.success(`Datos importados correctamente (${uploadResult.records_imported} registros)`);
// Automatically start training after successful upload
await startTraining();
} catch (error: any) {
// Error uploading sales data
const message = error?.response?.data?.detail || error.message || 'Error al subir datos de ventas';
throw new Error(message);
}
};
// Helper function to mark current step as completed
const markStepCompleted = async () => {
@@ -450,7 +367,8 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
if (currentStep === 1) {
stepData.bakery_name = bakeryData.name;
stepData.bakery_address = bakeryData.address;
stepData.business_type = bakeryData.businessType;
stepData.business_type = 'bakery'; // Default - will be auto-detected
stepData.auto_detect_business_type = true;
stepData.products_count = bakeryData.products.length;
} else if (currentStep === 2) {
stepData.file_name = bakeryData.csvFile?.name;
@@ -479,39 +397,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
}
return true;
case 2:
// Skip validation if using smart import (it handles its own validation)
if (useSmartImport) {
return true;
}
if (!bakeryData.csvFile) {
toast.error('Por favor, selecciona un archivo con tus datos históricos');
return false;
}
// Validate file format
const fileName = bakeryData.csvFile.name.toLowerCase();
const supportedFormats = ['.csv', '.xlsx', '.xls', '.json'];
const isValidFormat = supportedFormats.some(format => fileName.endsWith(format));
if (!isValidFormat) {
toast.error('Formato de archivo no soportado. Usa CSV, Excel (.xlsx, .xls) o JSON');
return false;
}
// Validate file size (10MB limit as per backend)
const maxSize = 10 * 1024 * 1024;
if (bakeryData.csvFile.size > maxSize) {
toast.error('El archivo es demasiado grande. Máximo 10MB');
return false;
}
// Check if validation was successful
if (validationStatus.status !== 'valid') {
toast.error('Por favor, valida el archivo antes de continuar');
return false;
}
// Smart import handles its own validation
return true;
default:
return true;
@@ -672,37 +558,20 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Tipo de negocio
</label>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'individual' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'individual'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<Store className="h-5 w-5 mb-2" />
<div className="font-medium">Panadería Individual</div>
<div className="text-sm text-gray-500">Un solo punto de venta</div>
</button>
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'central_workshop' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'central_workshop'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<Factory className="h-5 w-5 mb-2" />
<div className="font-medium">Obrador Central</div>
<div className="text-sm text-gray-500">Múltiples puntos de venta</div>
</button>
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg">
<div className="flex items-start">
<div className="flex-shrink-0">
<Brain className="w-5 h-5 text-blue-500 mt-0.5" />
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-blue-900">
🤖 Detección automática del tipo de negocio
</h4>
<p className="text-sm text-blue-700 mt-1">
Nuestro sistema de IA analizará automáticamente tus datos de ventas para identificar
si eres una panadería, cafetería, pastelería o restaurante. No necesitas seleccionar nada manualmente.
</p>
</div>
</div>
</div>
</div>
@@ -724,360 +593,30 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
);
}
// Use Smart Import by default, with option to switch to traditional
if (useSmartImport) {
return (
<div className="space-y-6">
{/* Header with import mode toggle */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-semibold text-gray-900">
Importación Inteligente de Datos 🧠
</h3>
<p className="text-gray-600 mt-1">
Nuestra IA creará automáticamente tu inventario desde tus datos históricos
</p>
</div>
<button
onClick={() => setUseSmartImport(false)}
className="flex items-center space-x-2 px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<Upload className="w-4 h-4" />
<span>Importación tradicional</span>
</button>
</div>
{/* Smart Import Component */}
<SmartHistoricalDataImport
tenantId={tenantId}
onComplete={(result) => {
// Mark sales data as uploaded and proceed to training
completeStep('sales_data_uploaded', {
smart_import: true,
records_imported: result.successful_imports,
import_job_id: result.import_job_id,
tenant_id: tenantId,
user_id: user?.id
}).then(() => {
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
}).catch(() => {
// Continue even if step completion fails
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
});
}}
onBack={() => setUseSmartImport(false)}
/>
</div>
);
}
// Traditional import fallback
// Smart Import only
return (
<div className="space-y-6">
{/* Header with import mode toggle */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900">
Datos Históricos (Modo Tradicional)
</h3>
<p className="text-gray-600 mt-1">
Sube tus datos y configura tu inventario manualmente
</p>
</div>
<button
onClick={() => setUseSmartImport(true)}
className="flex items-center space-x-2 px-4 py-2 text-sm bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-lg hover:from-blue-600 hover:to-purple-600 transition-colors"
>
<Brain className="w-4 h-4" />
<span>Activar IA</span>
</button>
</div>
<p className="text-gray-600 mb-6">
Para obtener predicciones precisas, necesitamos tus datos históricos de ventas.
Puedes subir archivos en varios formatos.
</p>
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg mb-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">!</span>
</div>
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-blue-900">
Formatos soportados y estructura de datos
</h4>
<div className="mt-2 text-sm text-blue-700">
<p className="mb-3"><strong>Formatos aceptados:</strong></p>
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<p className="font-medium">📊 Hojas de cálculo:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.xlsx (Excel moderno)</li>
<li>.xls (Excel clásico)</li>
</ul>
</div>
<div>
<p className="font-medium">📄 Datos estructurados:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.csv (Valores separados por comas)</li>
<li>.json (Formato JSON)</li>
</ul>
</div>
</div>
<p className="mb-2"><strong>Columnas requeridas (en cualquier idioma):</strong></p>
<ul className="list-disc list-inside text-xs space-y-1">
<li><strong>Fecha</strong>: fecha, date, datum (formato: YYYY-MM-DD, DD/MM/YYYY, etc.)</li>
<li><strong>Producto</strong>: producto, product, item, articulo, nombre</li>
<li><strong>Cantidad</strong>: cantidad, quantity, cantidad_vendida, qty</li>
</ul>
</div>
</div>
</div>
</div>
<div className="mt-6 p-6 border-2 border-dashed border-gray-300 rounded-xl hover:border-primary-300 transition-colors">
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<label htmlFor="sales-file-upload" className="cursor-pointer">
<span className="mt-2 block text-sm font-medium text-gray-900">
Subir archivo de datos históricos
</span>
<span className="mt-1 block text-sm text-gray-500">
Arrastra y suelta tu archivo aquí, o haz clic para seleccionar
</span>
<span className="mt-1 block text-xs text-gray-400">
Máximo 10MB - CSV, Excel (.xlsx, .xls), JSON
</span>
</label>
<input
id="sales-file-upload"
type="file"
accept=".csv,.xlsx,.xls,.json"
required
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
// Validate file size (10MB limit)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
toast.error('El archivo es demasiado grande. Máximo 10MB.');
return;
}
// Update bakery data with the selected file
setBakeryData(prev => ({
...prev,
csvFile: file,
hasHistoricalData: true
}));
toast.success(`Archivo ${file.name} seleccionado correctamente`);
// Auto-validate the file after upload if tenantId exists
if (tenantId) {
setValidationStatus({ status: 'validating' });
try {
const validationResult = await validateSalesData(tenantId, file);
if (validationResult.is_valid) {
setValidationStatus({
status: 'valid',
message: validationResult.message,
records: validationResult.details?.total_records || 0
});
toast.success('¡Archivo validado correctamente!');
} else {
setValidationStatus({
status: 'invalid',
message: validationResult.message
});
toast.error(`Error en validación: ${validationResult.message}`);
}
} catch (error: any) {
setValidationStatus({
status: 'invalid',
message: 'Error al validar el archivo'
});
toast.error('Error al validar el archivo');
}
} else {
// If no tenantId yet, set to idle and wait for manual validation
setValidationStatus({ status: 'idle' });
}
}
}}
className="hidden"
/>
</div>
</div>
{bakeryData.csvFile ? (
<div className="mt-4 space-y-3">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-gray-500 mr-2" />
<div>
<p className="text-sm font-medium text-gray-700">
{bakeryData.csvFile.name}
</p>
<p className="text-xs text-gray-600">
{(bakeryData.csvFile.size / 1024).toFixed(1)} KB {bakeryData.csvFile.type || 'Archivo de datos'}
</p>
</div>
</div>
<button
onClick={() => {
setBakeryData(prev => ({ ...prev, csvFile: undefined }));
setValidationStatus({ status: 'idle' });
}}
className="text-red-600 hover:text-red-800 text-sm"
>
Quitar
</button>
</div>
</div>
{/* Validation Section */}
<div className="p-4 border rounded-lg">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-gray-900">
Validación de datos
</h4>
{validationStatus.status === 'validating' ? (
<Loader className="h-4 w-4 animate-spin text-blue-500" />
) : validationStatus.status === 'valid' ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : validationStatus.status === 'invalid' ? (
<AlertTriangle className="h-4 w-4 text-red-500" />
) : (
<Clock className="h-4 w-4 text-gray-400" />
)}
</div>
{validationStatus.status === 'idle' && tenantId ? (
<div className="space-y-2">
<p className="text-sm text-gray-600">
Valida tu archivo para verificar que tiene el formato correcto.
</p>
<button
onClick={validateSalesFile}
disabled={isLoading}
className="w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Validar archivo
</button>
</div>
) : (
<div className="space-y-2">
{!tenantId ? (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
No se ha encontrado la panadería registrada.
</p>
<p className="text-xs text-yellow-600 mt-1">
Ve al paso anterior para registrar tu panadería o espera mientras se carga desde el servidor.
</p>
</div>
) : validationStatus.status !== 'idle' ? (
<button
onClick={() => setValidationStatus({ status: 'idle' })}
className="px-4 py-2 bg-gray-500 text-white text-sm rounded-lg hover:bg-gray-600"
>
Resetear validación
</button>
) : null}
</div>
)}
{validationStatus.status === 'validating' && (
<p className="text-sm text-blue-600">
Validando archivo... Por favor espera.
</p>
)}
{validationStatus.status === 'valid' && (
<div className="space-y-2">
<p className="text-sm text-green-700">
Archivo validado correctamente
</p>
{validationStatus.records && (
<p className="text-xs text-green-600">
{validationStatus.records} registros encontrados
</p>
)}
{validationStatus.message && (
<p className="text-xs text-green-600">
{validationStatus.message}
</p>
)}
</div>
)}
{validationStatus.status === 'invalid' && (
<div className="space-y-2">
<p className="text-sm text-red-700">
Error en validación
</p>
{validationStatus.message && (
<p className="text-xs text-red-600">
{validationStatus.message}
</p>
)}
<button
onClick={validateSalesFile}
disabled={isLoading}
className="w-full px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Validar de nuevo
</button>
</div>
)}
</div>
</div>
) : (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-yellow-600 mr-2" />
<p className="text-sm text-yellow-800">
<strong>Archivo requerido:</strong> Selecciona un archivo con tus datos históricos de ventas
</p>
</div>
</div>
)}
</div>
{/* Show switch to smart import suggestion if traditional validation fails */}
{validationStatus.status === 'invalid' && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start space-x-3">
<Brain className="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div>
<h4 className="text-sm font-medium text-blue-900 mb-2">
💡 ¿Problemas con la validación?
</h4>
<p className="text-sm text-blue-700 mb-3">
Nuestra IA puede manejar archivos con formatos más flexibles y ayudarte a solucionar problemas automáticamente.
</p>
<button
onClick={() => setUseSmartImport(true)}
className="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
>
Probar importación inteligente
</button>
</div>
</div>
</div>
)}
{/* Smart Import Component */}
<SmartHistoricalDataImport
tenantId={tenantId}
onComplete={(result) => {
// Mark sales data as uploaded and proceed to training
completeStep('sales_data_uploaded', {
smart_import: true,
records_imported: result.successful_imports,
import_job_id: result.import_job_id,
tenant_id: tenantId,
user_id: user?.id
}).then(() => {
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
}).catch(() => {
// Continue even if step completion fails
setBakeryData(prev => ({ ...prev, hasHistoricalData: true }));
startTraining();
});
}}
/>
</div>
);
@@ -1369,7 +908,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
</main>
{/* Navigation with Enhanced Accessibility - Hidden during training (step 3), completion (step 4), and smart import */}
{currentStep < 3 && !(currentStep === 2 && useSmartImport) && (
{currentStep < 3 && currentStep !== 2 && (
<nav
className="flex justify-between items-center bg-white rounded-3xl shadow-sm p-6"
role="navigation"
@@ -1391,16 +930,15 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
<div className="text-sm text-gray-500">Paso {currentStep} de 4</div>
<div className="text-xs text-gray-400 mt-1">
{currentStep === 1 && "Configura tu panadería"}
{currentStep === 2 && "Sube datos de ventas"}
</div>
</div>
{/* Dynamic Next Button with contextual text */}
<button
onClick={handleNext}
disabled={isLoading || (currentStep === 2 && validationStatus?.status !== 'valid')}
disabled={isLoading}
className="flex items-center px-8 py-3 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-xl hover:from-primary-600 hover:to-primary-700 transition-all hover:shadow-lg hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none group focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
aria-label={`${currentStep === 1 ? 'Save bakery information and continue' : currentStep === 2 && validationStatus?.status === 'valid' ? 'Start AI training' : 'Continue to next step'}`}
aria-label={`${currentStep === 1 ? 'Save bakery information and continue' : 'Continue to next step'}`}
>
{isLoading ? (
<>
@@ -1412,11 +950,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
) : (
<>
{currentStep === 1 && "Guardar y Continuar"}
{currentStep === 2 && (
<>
{validationStatus?.status === 'valid' ? "Iniciar Entrenamiento" : "Continuar"}
</>
)}
<ChevronRight className="h-5 w-5 ml-2 group-hover:translate-x-0.5 transition-transform" aria-hidden="true" />
</>
)}