Add frontend loading imporvements 2

This commit is contained in:
Urtzi Alfaro
2025-12-28 22:29:27 +01:00
parent 54662dde79
commit 96d8576103
7 changed files with 206 additions and 70 deletions

View File

@@ -2,7 +2,7 @@
* Subscription hook for checking plan features and limits * Subscription hook for checking plan features and limits
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { subscriptionService } from '../services/subscription'; import { subscriptionService } from '../services/subscription';
import { import {
@@ -54,13 +54,14 @@ export const useSubscription = () => {
}); });
// Derive subscription info from query data or tenant fallback // Derive subscription info from query data or tenant fallback
const subscriptionInfo: SubscriptionInfo = { // IMPORTANT: Memoize to prevent infinite re-renders in dependent hooks
const subscriptionInfo: SubscriptionInfo = useMemo(() => ({
plan: usageSummary?.plan || initialPlan, plan: usageSummary?.plan || initialPlan,
status: usageSummary?.status || 'active', status: usageSummary?.status || 'active',
features: usageSummary?.usage || {}, features: usageSummary?.usage || {},
loading: isLoading, loading: isLoading,
error: error ? 'Failed to load subscription data' : undefined, error: error ? 'Failed to load subscription data' : undefined,
}; }), [usageSummary?.plan, usageSummary?.status, usageSummary?.usage, initialPlan, isLoading, error]);
// Check if user has a specific feature // Check if user has a specific feature
const hasFeature = useCallback(async (featureName: string): Promise<SubscriptionFeature> => { const hasFeature = useCallback(async (featureName: string): Promise<SubscriptionFeature> => {

View File

@@ -1,8 +1,6 @@
import React, { useState, useCallback, forwardRef } from 'react'; import React, { useState, useCallback, forwardRef } from 'react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { useAuthUser, useIsAuthenticated } from '../../../stores';
import { useTheme } from '../../../contexts/ThemeContext'; import { useTheme } from '../../../contexts/ThemeContext';
import { useTenantInitializer } from '../../../stores/useTenantInitializer';
import { useHasAccess } from '../../../hooks/useAccessControl'; import { useHasAccess } from '../../../hooks/useAccessControl';
import { Header } from '../Header'; import { Header } from '../Header';
import { Sidebar } from '../Sidebar'; import { Sidebar } from '../Sidebar';
@@ -80,9 +78,6 @@ export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const hasAccess = useHasAccess(); // Check both authentication and demo mode const hasAccess = useHasAccess(); // Check both authentication and demo mode
// Initialize tenant data for authenticated users
useTenantInitializer();
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(initialSidebarCollapsed); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(initialSidebarCollapsed);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);

View File

@@ -264,23 +264,6 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
const allUserRoles = [...globalUserRoles, ...tenantRoles]; const allUserRoles = [...globalUserRoles, ...tenantRoles];
const tenantPermissions = currentTenantAccess?.permissions || []; const tenantPermissions = currentTenantAccess?.permissions || [];
// Debug logging for analytics route
if (item.path === '/app/analytics') {
console.log('🔍 [Sidebar] Checking analytics menu item:', {
path: item.path,
requiredRoles: item.requiredRoles,
requiredPermissions: item.requiredPermissions,
globalUserRoles,
tenantRoles,
allUserRoles,
tenantPermissions,
isAuthenticated,
hasAccess,
user,
currentTenantAccess
});
}
// If no specific permissions/roles required, allow access // If no specific permissions/roles required, allow access
if (!item.requiredPermissions && !item.requiredRoles) { if (!item.requiredPermissions && !item.requiredRoles) {
return true; return true;
@@ -298,10 +281,6 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
tenantPermissions tenantPermissions
); );
if (item.path === '/app/analytics') {
console.log('🔍 [Sidebar] Analytics canAccessRoute result:', canAccessItem);
}
return canAccessItem; return canAccessItem;
}); });
}; };

View File

@@ -1041,10 +1041,14 @@
} }
}, },
"itemTypeSelector": { "itemTypeSelector": {
"searchPlaceholder": "Search by name or category...", "title": "Select Type",
"description": "Choose what you want to add",
"search": {
"placeholder": "Search by name or category...",
"noResults": "No results found", "noResults": "No results found",
"resultSingular": "result", "resultSingular": "result",
"resultPlural": "results", "resultPlural": "results"
},
"categories": { "categories": {
"all": "All", "all": "All",
"daily": "Daily", "daily": "Daily",
@@ -1056,6 +1060,63 @@
"daily": "Daily", "daily": "Daily",
"common": "Common", "common": "Common",
"setup": "Setup" "setup": "Setup"
},
"items": {
"inventory": {
"title": "Inventory",
"subtitle": "Add ingredients or products to your inventory",
"keywords": ["stock", "ingredients", "products", "warehouse"]
},
"supplier": {
"title": "Supplier",
"subtitle": "Add a new supplier or vendor",
"keywords": ["vendor", "purchases", "supplies"]
},
"recipe": {
"title": "Recipe",
"subtitle": "Create a new recipe or formula",
"keywords": ["formula", "preparation", "ingredients"]
},
"equipment": {
"title": "Equipment",
"subtitle": "Register bakery equipment or machinery",
"keywords": ["oven", "mixer", "equipment", "machine"]
},
"quality-template": {
"title": "Quality Template",
"subtitle": "Create a quality control template",
"keywords": ["control", "quality", "inspection", "verification"]
},
"customer-order": {
"title": "Customer Order",
"subtitle": "Create a new customer order",
"keywords": ["order", "customer", "sale"]
},
"customer": {
"title": "Customer",
"subtitle": "Add a new customer",
"keywords": ["customer", "buyer", "contact"]
},
"team-member": {
"title": "Team Member",
"subtitle": "Add a team member or employee",
"keywords": ["employee", "worker", "staff"]
},
"sales-entry": {
"title": "Sales Entry",
"subtitle": "Record a sales transaction",
"keywords": ["sale", "cash", "revenue", "transaction"]
},
"purchase-order": {
"title": "Purchase Order",
"subtitle": "Create a purchase order to supplier",
"keywords": ["purchase", "order", "supplier"]
},
"production-batch": {
"title": "Production Batch",
"subtitle": "Create a new production batch",
"keywords": ["production", "batch", "manufacturing"]
}
} }
} }
} }

View File

@@ -1426,10 +1426,14 @@
} }
}, },
"itemTypeSelector": { "itemTypeSelector": {
"searchPlaceholder": "Buscar por nombre o categoría...", "title": "Seleccionar Tipo",
"description": "Elige qué deseas agregar",
"search": {
"placeholder": "Buscar por nombre o categoría...",
"noResults": "No se encontraron resultados", "noResults": "No se encontraron resultados",
"resultSingular": "resultado", "resultSingular": "resultado",
"resultPlural": "resultados", "resultPlural": "resultados"
},
"categories": { "categories": {
"all": "Todos", "all": "Todos",
"daily": "Diario", "daily": "Diario",
@@ -1441,6 +1445,63 @@
"daily": "Diario", "daily": "Diario",
"common": "Común", "common": "Común",
"setup": "Configuración" "setup": "Configuración"
},
"items": {
"inventory": {
"title": "Inventario",
"subtitle": "Agregar ingredientes o productos a tu inventario",
"keywords": ["stock", "ingredientes", "productos", "almacén"]
},
"supplier": {
"title": "Proveedor",
"subtitle": "Agregar un nuevo proveedor o vendedor",
"keywords": ["vendedor", "compras", "suministros"]
},
"recipe": {
"title": "Receta",
"subtitle": "Crear una nueva receta o fórmula",
"keywords": ["fórmula", "preparación", "ingredientes"]
},
"equipment": {
"title": "Maquinaria",
"subtitle": "Registrar equipo o maquinaria de panadería",
"keywords": ["horno", "amasadora", "equipo", "máquina"]
},
"quality-template": {
"title": "Plantilla de Calidad",
"subtitle": "Crear una plantilla de control de calidad",
"keywords": ["control", "calidad", "inspección", "verificación"]
},
"customer-order": {
"title": "Pedido de Cliente",
"subtitle": "Crear un nuevo pedido de cliente",
"keywords": ["pedido", "orden", "cliente", "venta"]
},
"customer": {
"title": "Cliente",
"subtitle": "Agregar un nuevo cliente",
"keywords": ["cliente", "comprador", "contacto"]
},
"team-member": {
"title": "Miembro del Equipo",
"subtitle": "Agregar un miembro del equipo o empleado",
"keywords": ["empleado", "trabajador", "personal", "staff"]
},
"sales-entry": {
"title": "Registro de Ventas",
"subtitle": "Registrar una transacción de venta",
"keywords": ["venta", "caja", "ingreso", "transacción"]
},
"purchase-order": {
"title": "Orden de Compra",
"subtitle": "Crear una orden de compra a proveedor",
"keywords": ["compra", "orden", "proveedor", "pedido"]
},
"production-batch": {
"title": "Lote de Producción",
"subtitle": "Crear un nuevo lote de producción",
"keywords": ["producción", "lote", "fabricación", "elaboración"]
}
} }
} }
} }

View File

@@ -1035,10 +1035,14 @@
} }
}, },
"itemTypeSelector": { "itemTypeSelector": {
"searchPlaceholder": "Bilatu izenaren edo kategoriaren arabera...", "title": "Mota Hautatu",
"description": "Aukeratu zer gehitu nahi duzun",
"search": {
"placeholder": "Bilatu izenaren edo kategoriaren arabera...",
"noResults": "Ez da emaitzarik aurkitu", "noResults": "Ez da emaitzarik aurkitu",
"resultSingular": "emaitza", "resultSingular": "emaitza",
"resultPlural": "emaitzak", "resultPlural": "emaitzak"
},
"categories": { "categories": {
"all": "Guztiak", "all": "Guztiak",
"daily": "Egunerokoa", "daily": "Egunerokoa",
@@ -1050,6 +1054,63 @@
"daily": "Egunerokoa", "daily": "Egunerokoa",
"common": "Arrunta", "common": "Arrunta",
"setup": "Konfigurazioa" "setup": "Konfigurazioa"
},
"items": {
"inventory": {
"title": "Inbentarioa",
"subtitle": "Gehitu osagaiak edo produktuak zure inbentariora",
"keywords": ["stock", "osagaiak", "produktuak", "biltegi"]
},
"supplier": {
"title": "Hornitzailea",
"subtitle": "Gehitu hornitzaile edo saltzaile berri bat",
"keywords": ["saltzaile", "erosketak", "hornidura"]
},
"recipe": {
"title": "Errezeta",
"subtitle": "Sortu errezeta edo formula berri bat",
"keywords": ["formula", "prestaketa", "osagaiak"]
},
"equipment": {
"title": "Ekipamendua",
"subtitle": "Erregistratu okindegiaren ekipamendua edo makina",
"keywords": ["labea", "oramailea", "ekipamendua", "makina"]
},
"quality-template": {
"title": "Kalitate Txantiloia",
"subtitle": "Sortu kalitate kontrol txantiloi bat",
"keywords": ["kontrola", "kalitatea", "ikuskatzea", "egiaztapena"]
},
"customer-order": {
"title": "Bezeroaren Eskaera",
"subtitle": "Sortu bezero eskaera berri bat",
"keywords": ["eskaera", "bezeroa", "salmenta"]
},
"customer": {
"title": "Bezeroa",
"subtitle": "Gehitu bezero berri bat",
"keywords": ["bezeroa", "eroslea", "kontaktua"]
},
"team-member": {
"title": "Taldeko Kidea",
"subtitle": "Gehitu taldeko kide edo langile bat",
"keywords": ["langilea", "enplegatu", "pertsonal"]
},
"sales-entry": {
"title": "Salmenta Erregistroa",
"subtitle": "Erregistratu salmenta transakzio bat",
"keywords": ["salmenta", "kutxa", "diru-sarrera", "transakzioa"]
},
"purchase-order": {
"title": "Erosketa Eskaera",
"subtitle": "Sortu erosketa eskaera hornitzaileari",
"keywords": ["erosketa", "eskaera", "hornitzailea"]
},
"production-batch": {
"title": "Ekoizpen Lotea",
"subtitle": "Sortu ekoizpen lote berri bat",
"keywords": ["ekoizpena", "lotea", "fabrikazioa"]
}
} }
} }
} }

View File

@@ -69,18 +69,8 @@ export const useTenantInitializer = () => {
const virtualTenantId = localStorage.getItem('virtual_tenant_id'); const virtualTenantId = localStorage.getItem('virtual_tenant_id');
const storedTier = localStorage.getItem('subscription_tier'); const storedTier = localStorage.getItem('subscription_tier');
console.log('🔍 [TenantInitializer] Demo mode detected:', {
isDemoMode,
demoSessionId,
virtualTenantId,
demoAccountType,
storedTier,
currentTenant: currentTenant?.id
});
// Guard: If no virtual_tenant_id is available, skip tenant setup // Guard: If no virtual_tenant_id is available, skip tenant setup
if (!virtualTenantId) { if (!virtualTenantId) {
console.warn('⚠️ [TenantInitializer] No virtual_tenant_id found in localStorage');
return; return;
} }
@@ -90,8 +80,6 @@ export const useTenantInitializer = () => {
currentTenant.id === virtualTenantId; currentTenant.id === virtualTenantId;
if (!isValidDemoTenant) { if (!isValidDemoTenant) {
console.log('🔧 [TenantInitializer] Setting up demo tenant...');
// Determine the appropriate subscription tier based on stored value or account type // Determine the appropriate subscription tier based on stored value or account type
const subscriptionTier = storedTier as SubscriptionTier || getDemoTierForAccountType(demoAccountType); const subscriptionTier = storedTier as SubscriptionTier || getDemoTierForAccountType(demoAccountType);
@@ -111,50 +99,40 @@ export const useTenantInitializer = () => {
postal_code: '28001', postal_code: '28001',
phone: null, phone: null,
is_active: true, is_active: true,
subscription_plan: subscriptionTier, // New field name subscription_plan: subscriptionTier,
subscription_tier: subscriptionTier, // Deprecated but kept for backward compatibility subscription_tier: subscriptionTier,
ml_model_trained: false, ml_model_trained: false,
last_training_date: null, last_training_date: null,
owner_id: 'demo-user', owner_id: 'demo-user',
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}; };
console.log(`✅ [TenantInitializer] Setting up tenant with tier: ${subscriptionTier}`);
// Set the demo tenant as current // Set the demo tenant as current
setCurrentTenant(mockTenant); setCurrentTenant(mockTenant);
// **CRITICAL: Also set tenant ID in API client** // Set tenant ID in API client
// This ensures API requests include the tenant ID header
import('../api/client').then(({ apiClient }) => { import('../api/client').then(({ apiClient }) => {
apiClient.setTenantId(virtualTenantId); apiClient.setTenantId(virtualTenantId);
console.log('✅ [TenantInitializer] Set API client tenant ID:', virtualTenantId);
}); });
// For enterprise demos, load child tenants immediately (session is already ready when we navigate here) // For enterprise demos, load child tenants immediately
if (demoAccountType === 'enterprise') { if (demoAccountType === 'enterprise') {
console.log('🔄 [TenantInitializer] Loading available tenants for enterprise demo...');
const mockUserId = 'demo-user'; const mockUserId = 'demo-user';
import('../api/services/tenant').then(({ TenantService }) => { import('../api/services/tenant').then(({ TenantService }) => {
const tenantService = new TenantService(); const tenantService = new TenantService();
tenantService.getUserTenants(mockUserId) tenantService.getUserTenants(mockUserId)
.then(tenants => { .then(tenants => {
console.log('📋 [TenantInitializer] Loaded available tenants:', tenants.length);
if (tenants.length === 0) {
console.warn('⚠️ [TenantInitializer] No child tenants found yet - they may still be cloning');
}
// Update the tenant store with available tenants
import('../stores/tenant.store').then(({ useTenantStore }) => { import('../stores/tenant.store').then(({ useTenantStore }) => {
useTenantStore.getState().setAvailableTenants(tenants); useTenantStore.getState().setAvailableTenants(tenants);
}); });
}) })
.catch(error => { .catch(() => {
console.error('❌ [TenantInitializer] Failed to load available tenants:', error); // Silently handle error
}); });
}); });
} }
} }
} }
}, [isDemoMode, demoSessionId, demoAccountType, currentTenant, setCurrentTenant]); }, [isDemoMode, demoSessionId, demoAccountType, currentTenant, setCurrentTenant]);
};; };