Fix new services implementation 2
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user