From 0220da1725905afc2cce67decc87468a016abd28 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Sat, 1 Nov 2025 21:35:03 +0100 Subject: [PATCH] Improve the frontend 4 --- .../COMPLETION_CHECKLIST.md | 0 .../DELETION_ARCHITECTURE_DIAGRAM.md | 0 .../DELETION_IMPLEMENTATION_PROGRESS.md | 0 .../DELETION_REFACTORING_SUMMARY.md | 0 .../DELETION_SYSTEM_100_PERCENT_COMPLETE.md | 0 .../DELETION_SYSTEM_COMPLETE.md | 0 .../FINAL_IMPLEMENTATION_SUMMARY.md | 0 .../FINAL_PROJECT_SUMMARY.md | 0 .../FIXES_COMPLETE_SUMMARY.md | 0 .../FUNCTIONAL_TEST_RESULTS.md | 0 GETTING_STARTED.md => docs/GETTING_STARTED.md | 0 .../ORCHESTRATION_REFACTORING_COMPLETE.md | 0 ...ITY_ARCHITECTURE_IMPLEMENTATION_SUMMARY.md | 0 .../QUICK_REFERENCE_DELETION_SYSTEM.md | 0 .../QUICK_START_REMAINING_SERVICES.md | 0 .../QUICK_START_SERVICE_TOKENS.md | 0 .../README_DELETION_SYSTEM.md | 0 docs/ROLES_AND_PERMISSIONS_SYSTEM.md | 363 +++++++ .../SERVICE_TOKEN_CONFIGURATION.md | 0 .../SESSION_COMPLETE_FUNCTIONAL_TESTING.md | 0 .../SESSION_SUMMARY_SERVICE_TOKENS.md | 0 .../SMART_PROCUREMENT_IMPLEMENTATION.md | 0 .../TENANT_DELETION_IMPLEMENTATION_GUIDE.md | 0 .../TEST_RESULTS_DELETION_SYSTEM.md | 0 frontend/src/api/hooks/orders.ts | 402 -------- frontend/src/api/hooks/performance.ts | 926 ++++++++++++++++++ frontend/src/api/hooks/procurement.ts | 495 ++++++++++ frontend/src/api/hooks/subscription.ts | 2 +- frontend/src/api/hooks/tenant.ts | 42 +- frontend/src/api/index.ts | 54 +- frontend/src/api/services/orders.ts | 221 ----- .../src/api/services/procurement-service.ts | 335 +++++++ frontend/src/api/services/procurement.ts | 317 ------ frontend/src/api/services/tenant.ts | 15 + frontend/src/api/types/orders.ts | 366 ------- frontend/src/api/types/performance.ts | 192 ++++ frontend/src/api/types/procurement.ts | 634 ++++++++++++ .../domain/team/TransferOwnershipModal.tsx | 375 +++++++ frontend/src/components/ui/index.ts | 2 + .../contexts/SubscriptionEventsContext.tsx | 20 +- .../demo-onboarding/config/tour-steps.ts | 4 +- frontend/src/locales/es/landing.json | 12 +- .../analytics/ProcurementAnalyticsPage.tsx | 165 +++- .../performance/PerformanceAnalyticsPage.tsx | 912 ++++++++++------- .../ScenarioSimulationPage.tsx | 670 +++++++++++-- frontend/src/pages/public/DemoPage.tsx | 86 +- frontend/src/pages/public/LandingPage.tsx | 14 +- frontend/src/router/ProtectedRoute.tsx | 67 +- frontend/src/types/roles.ts | 42 +- frontend/src/utils/permissions.ts | 380 +++++++ gateway/app/routes/tenant.py | 31 +- .../demo_session/app/api/demo_accounts.py | 4 +- .../app/api/scenario_operations.py | 8 +- services/procurement/app/api/analytics.py | 82 ++ services/procurement/app/main.py | 2 + .../procurement_plan_repository.py | 48 + .../app/services/procurement_service.py | 329 ++++++- services/tenant/app/models/tenants.py | 32 +- todo.md | 6 + 59 files changed, 5785 insertions(+), 1870 deletions(-) rename COMPLETION_CHECKLIST.md => docs/COMPLETION_CHECKLIST.md (100%) rename DELETION_ARCHITECTURE_DIAGRAM.md => docs/DELETION_ARCHITECTURE_DIAGRAM.md (100%) rename DELETION_IMPLEMENTATION_PROGRESS.md => docs/DELETION_IMPLEMENTATION_PROGRESS.md (100%) rename DELETION_REFACTORING_SUMMARY.md => docs/DELETION_REFACTORING_SUMMARY.md (100%) rename DELETION_SYSTEM_100_PERCENT_COMPLETE.md => docs/DELETION_SYSTEM_100_PERCENT_COMPLETE.md (100%) rename DELETION_SYSTEM_COMPLETE.md => docs/DELETION_SYSTEM_COMPLETE.md (100%) rename FINAL_IMPLEMENTATION_SUMMARY.md => docs/FINAL_IMPLEMENTATION_SUMMARY.md (100%) rename FINAL_PROJECT_SUMMARY.md => docs/FINAL_PROJECT_SUMMARY.md (100%) rename FIXES_COMPLETE_SUMMARY.md => docs/FIXES_COMPLETE_SUMMARY.md (100%) rename FUNCTIONAL_TEST_RESULTS.md => docs/FUNCTIONAL_TEST_RESULTS.md (100%) rename GETTING_STARTED.md => docs/GETTING_STARTED.md (100%) rename ORCHESTRATION_REFACTORING_COMPLETE.md => docs/ORCHESTRATION_REFACTORING_COMPLETE.md (100%) rename QUALITY_ARCHITECTURE_IMPLEMENTATION_SUMMARY.md => docs/QUALITY_ARCHITECTURE_IMPLEMENTATION_SUMMARY.md (100%) rename QUICK_REFERENCE_DELETION_SYSTEM.md => docs/QUICK_REFERENCE_DELETION_SYSTEM.md (100%) rename QUICK_START_REMAINING_SERVICES.md => docs/QUICK_START_REMAINING_SERVICES.md (100%) rename QUICK_START_SERVICE_TOKENS.md => docs/QUICK_START_SERVICE_TOKENS.md (100%) rename README_DELETION_SYSTEM.md => docs/README_DELETION_SYSTEM.md (100%) create mode 100644 docs/ROLES_AND_PERMISSIONS_SYSTEM.md rename SERVICE_TOKEN_CONFIGURATION.md => docs/SERVICE_TOKEN_CONFIGURATION.md (100%) rename SESSION_COMPLETE_FUNCTIONAL_TESTING.md => docs/SESSION_COMPLETE_FUNCTIONAL_TESTING.md (100%) rename SESSION_SUMMARY_SERVICE_TOKENS.md => docs/SESSION_SUMMARY_SERVICE_TOKENS.md (100%) rename SMART_PROCUREMENT_IMPLEMENTATION.md => docs/SMART_PROCUREMENT_IMPLEMENTATION.md (100%) rename TENANT_DELETION_IMPLEMENTATION_GUIDE.md => docs/TENANT_DELETION_IMPLEMENTATION_GUIDE.md (100%) rename TEST_RESULTS_DELETION_SYSTEM.md => docs/TEST_RESULTS_DELETION_SYSTEM.md (100%) create mode 100644 frontend/src/api/hooks/performance.ts create mode 100644 frontend/src/api/hooks/procurement.ts create mode 100644 frontend/src/api/services/procurement-service.ts delete mode 100644 frontend/src/api/services/procurement.ts create mode 100644 frontend/src/api/types/performance.ts create mode 100644 frontend/src/api/types/procurement.ts create mode 100644 frontend/src/components/domain/team/TransferOwnershipModal.tsx create mode 100644 frontend/src/utils/permissions.ts create mode 100644 services/procurement/app/api/analytics.py create mode 100644 todo.md diff --git a/COMPLETION_CHECKLIST.md b/docs/COMPLETION_CHECKLIST.md similarity index 100% rename from COMPLETION_CHECKLIST.md rename to docs/COMPLETION_CHECKLIST.md diff --git a/DELETION_ARCHITECTURE_DIAGRAM.md b/docs/DELETION_ARCHITECTURE_DIAGRAM.md similarity index 100% rename from DELETION_ARCHITECTURE_DIAGRAM.md rename to docs/DELETION_ARCHITECTURE_DIAGRAM.md diff --git a/DELETION_IMPLEMENTATION_PROGRESS.md b/docs/DELETION_IMPLEMENTATION_PROGRESS.md similarity index 100% rename from DELETION_IMPLEMENTATION_PROGRESS.md rename to docs/DELETION_IMPLEMENTATION_PROGRESS.md diff --git a/DELETION_REFACTORING_SUMMARY.md b/docs/DELETION_REFACTORING_SUMMARY.md similarity index 100% rename from DELETION_REFACTORING_SUMMARY.md rename to docs/DELETION_REFACTORING_SUMMARY.md diff --git a/DELETION_SYSTEM_100_PERCENT_COMPLETE.md b/docs/DELETION_SYSTEM_100_PERCENT_COMPLETE.md similarity index 100% rename from DELETION_SYSTEM_100_PERCENT_COMPLETE.md rename to docs/DELETION_SYSTEM_100_PERCENT_COMPLETE.md diff --git a/DELETION_SYSTEM_COMPLETE.md b/docs/DELETION_SYSTEM_COMPLETE.md similarity index 100% rename from DELETION_SYSTEM_COMPLETE.md rename to docs/DELETION_SYSTEM_COMPLETE.md diff --git a/FINAL_IMPLEMENTATION_SUMMARY.md b/docs/FINAL_IMPLEMENTATION_SUMMARY.md similarity index 100% rename from FINAL_IMPLEMENTATION_SUMMARY.md rename to docs/FINAL_IMPLEMENTATION_SUMMARY.md diff --git a/FINAL_PROJECT_SUMMARY.md b/docs/FINAL_PROJECT_SUMMARY.md similarity index 100% rename from FINAL_PROJECT_SUMMARY.md rename to docs/FINAL_PROJECT_SUMMARY.md diff --git a/FIXES_COMPLETE_SUMMARY.md b/docs/FIXES_COMPLETE_SUMMARY.md similarity index 100% rename from FIXES_COMPLETE_SUMMARY.md rename to docs/FIXES_COMPLETE_SUMMARY.md diff --git a/FUNCTIONAL_TEST_RESULTS.md b/docs/FUNCTIONAL_TEST_RESULTS.md similarity index 100% rename from FUNCTIONAL_TEST_RESULTS.md rename to docs/FUNCTIONAL_TEST_RESULTS.md diff --git a/GETTING_STARTED.md b/docs/GETTING_STARTED.md similarity index 100% rename from GETTING_STARTED.md rename to docs/GETTING_STARTED.md diff --git a/ORCHESTRATION_REFACTORING_COMPLETE.md b/docs/ORCHESTRATION_REFACTORING_COMPLETE.md similarity index 100% rename from ORCHESTRATION_REFACTORING_COMPLETE.md rename to docs/ORCHESTRATION_REFACTORING_COMPLETE.md diff --git a/QUALITY_ARCHITECTURE_IMPLEMENTATION_SUMMARY.md b/docs/QUALITY_ARCHITECTURE_IMPLEMENTATION_SUMMARY.md similarity index 100% rename from QUALITY_ARCHITECTURE_IMPLEMENTATION_SUMMARY.md rename to docs/QUALITY_ARCHITECTURE_IMPLEMENTATION_SUMMARY.md diff --git a/QUICK_REFERENCE_DELETION_SYSTEM.md b/docs/QUICK_REFERENCE_DELETION_SYSTEM.md similarity index 100% rename from QUICK_REFERENCE_DELETION_SYSTEM.md rename to docs/QUICK_REFERENCE_DELETION_SYSTEM.md diff --git a/QUICK_START_REMAINING_SERVICES.md b/docs/QUICK_START_REMAINING_SERVICES.md similarity index 100% rename from QUICK_START_REMAINING_SERVICES.md rename to docs/QUICK_START_REMAINING_SERVICES.md diff --git a/QUICK_START_SERVICE_TOKENS.md b/docs/QUICK_START_SERVICE_TOKENS.md similarity index 100% rename from QUICK_START_SERVICE_TOKENS.md rename to docs/QUICK_START_SERVICE_TOKENS.md diff --git a/README_DELETION_SYSTEM.md b/docs/README_DELETION_SYSTEM.md similarity index 100% rename from README_DELETION_SYSTEM.md rename to docs/README_DELETION_SYSTEM.md diff --git a/docs/ROLES_AND_PERMISSIONS_SYSTEM.md b/docs/ROLES_AND_PERMISSIONS_SYSTEM.md new file mode 100644 index 00000000..b643b38c --- /dev/null +++ b/docs/ROLES_AND_PERMISSIONS_SYSTEM.md @@ -0,0 +1,363 @@ +# Roles and Permissions System + +## Overview + +The Bakery IA platform implements a **dual role system** that provides fine-grained access control across both platform-wide and organization-specific operations. + +## Architecture + +### Two Distinct Role Systems + +#### 1. Global User Roles (Auth Service) + +**Purpose:** System-wide permissions across the entire platform +**Service:** Auth Service +**Storage:** `User` model +**Scope:** Cross-tenant, platform-level access control + +**Roles:** +- `super_admin` - Full platform access, can perform any operation +- `admin` - System administrator, platform management capabilities +- `manager` - Mid-level management access +- `user` - Basic authenticated user + +**Use Cases:** +- Platform administration +- Cross-tenant operations +- System-wide features +- User management at platform level + +#### 2. Tenant-Specific Roles (Tenant Service) + +**Purpose:** Organization/tenant-level permissions +**Service:** Tenant Service +**Storage:** `TenantMember` model +**Scope:** Per-tenant access control + +**Roles:** +- `owner` - Full control of the tenant, can transfer ownership, manage all aspects +- `admin` - Tenant administrator, can manage team members and most operations +- `member` - Standard team member, regular operational access +- `viewer` - Read-only observer, view-only access to tenant data + +**Use Cases:** +- Team management +- Organization-specific operations +- Resource access within a tenant +- Most application features + +## Role Mapping + +When users are created through tenant management (pilot phase), tenant roles are automatically mapped to appropriate global roles: + +``` +Tenant Role → Global Role │ Rationale +───────────────────────────────────────────────── +admin → admin │ Administrative access +member → manager │ Management-level access +viewer → user │ Basic user access +owner → (no mapping) │ Owner is tenant-specific only +``` + +**Implementation:** +- Frontend: `frontend/src/types/roles.ts` +- Backend: `services/tenant/app/api/tenant_members.py` (lines 68-76) + +## Permission Checking + +### Unified Permission System + +Location: `frontend/src/utils/permissions.ts` + +The unified permission system provides centralized functions for checking permissions: + +#### Functions + +1. **`checkGlobalPermission(user, options)`** + - Check platform-wide permissions + - Used for: System settings, platform admin features + +2. **`checkTenantPermission(tenantAccess, options)`** + - Check tenant-specific permissions + - Used for: Team management, tenant resources + +3. **`checkCombinedPermission(user, tenantAccess, options)`** + - Check either global OR tenant permissions + - Used for: Mixed access scenarios + +4. **Helper Functions:** + - `canManageTeam()` - Check team management permission + - `isTenantOwner()` - Check if user is tenant owner + - `canPerformAdminActions()` - Check admin permissions + - `getEffectivePermissions()` - Get all permission flags + +### Usage Examples + +```typescript +// Check if user can manage platform users (global only) +checkGlobalPermission(user, { requiredRole: 'admin' }) + +// Check if user can manage tenant team (tenant only) +checkTenantPermission(tenantAccess, { requiredRole: 'owner' }) + +// Check if user can access a feature (either global admin OR tenant owner) +checkCombinedPermission(user, tenantAccess, { + globalRoles: ['admin', 'super_admin'], + tenantRoles: ['owner'] +}) +``` + +## Route Protection + +### Protected Routes + +Location: `frontend/src/router/ProtectedRoute.tsx` + +All protected routes now use the unified permission system: + +```typescript +// Admin Route: Global admin OR tenant owner/admin + + + + +// Manager Route: Global admin/manager OR tenant admin/owner/member + + + + +// Owner Route: Super admin OR tenant owner only + + + +``` + +## Team Management + +### Core Features + +#### 1. Add Team Members +- **Permission Required:** Tenant Owner or Admin +- **Options:** + - Add existing user to tenant + - Create new user and add to tenant (pilot phase) +- **Subscription Limits:** Checked before adding members + +#### 2. Update Member Roles +- **Permission Required:** Context-dependent + - Viewer → Member: Any admin + - Member → Admin: Owner only + - Admin → Member: Owner only +- **Restrictions:** Cannot change Owner role via standard UI + +#### 3. Remove Members +- **Permission Required:** Owner only +- **Restrictions:** Cannot remove the Owner + +#### 4. Transfer Ownership +- **Permission Required:** Owner only +- **Requirements:** + - New owner must be an existing Admin + - Two-step confirmation process + - Irreversible operation +- **Changes:** + - New user becomes Owner + - Previous owner becomes Admin + +### Team Page + +Location: `frontend/src/pages/app/settings/team/TeamPage.tsx` + +**Features:** +- Team member list with role indicators +- Filter by role +- Search by name/email +- Member details modal +- Activity tracking +- Transfer ownership modal +- Error recovery for missing user data + +**Security:** +- Removed insecure owner_id fallback +- Proper access validation through backend +- Permission-based UI rendering + +## Backend Implementation + +### Tenant Member Endpoints + +Location: `services/tenant/app/api/tenant_members.py` + +**Endpoints:** +1. `POST /tenants/{tenant_id}/members/with-user` - Add member with optional user creation +2. `POST /tenants/{tenant_id}/members` - Add existing user +3. `GET /tenants/{tenant_id}/members` - List members +4. `PUT /tenants/{tenant_id}/members/{user_id}/role` - Update role +5. `DELETE /tenants/{tenant_id}/members/{user_id}` - Remove member +6. `POST /tenants/{tenant_id}/transfer-ownership` - Transfer ownership +7. `GET /tenants/{tenant_id}/admins` - Get tenant admins +8. `DELETE /tenants/user/{user_id}/memberships` - Delete user memberships (internal) + +### Member Enrichment + +The backend enriches tenant members with user data from the Auth service: +- User full name +- Email +- Phone +- Last login +- Language/timezone preferences + +**Error Handling:** +- Graceful degradation if Auth service unavailable +- Fallback to user_id if enrichment fails +- Frontend displays warning for incomplete data + +## Best Practices + +### When to Use Which Permission Check + +1. **Global Permission Check:** + - Platform administration + - Cross-tenant operations + - System-wide features + - User management at platform level + +2. **Tenant Permission Check:** + - Team management + - Organization-specific resources + - Tenant settings + - Most application features + +3. **Combined Permission Check:** + - Features requiring elevated access + - Admin-only operations that can be done by either global or tenant admins + - Owner-specific operations with super_admin override + +### Security Considerations + +1. **Never use client-side owner_id comparison as fallback** + - Always validate through backend + - Use proper access endpoints + +2. **Always validate permissions on the backend** + - Frontend checks are for UX only + - Backend is source of truth + +3. **Use unified permission system** + - Consistent permission checking + - Clear documentation + - Type-safe + +4. **Audit critical operations** + - Log role changes + - Track ownership transfers + - Monitor member additions/removals + +## Future Enhancements + +### Planned Features + +1. **Role Change History** + - Audit trail for role changes + - Display who changed roles and when + - Integrated into member details modal + +2. **Fine-grained Permissions** + - Custom permission sets + - Permission groups + - Resource-level permissions + +3. **Invitation Flow** + - Replace direct user creation + - Email-based invitations + - Invitation expiration + +4. **Member Status Management** + - Activate/deactivate members + - Suspend access temporarily + - Bulk status updates + +5. **Advanced Team Features** + - Sub-teams/departments + - Role templates + - Bulk role assignments + +## Troubleshooting + +### Common Issues + +#### "Permission Denied" Errors +- **Cause:** User lacks required role or permission +- **Solution:** Verify user's tenant membership and role +- **Check:** `currentTenantAccess` in tenant store + +#### Missing User Data in Team List +- **Cause:** Auth service enrichment failed +- **Solution:** Check Auth service connectivity +- **Workaround:** Frontend displays warning and fallback data + +#### Cannot Transfer Ownership +- **Cause:** No eligible admins +- **Solution:** Promote a member to admin first +- **Requirement:** New owner must be an existing admin + +#### Access Validation Stuck Loading +- **Cause:** Tenant access endpoint not responding +- **Solution:** Reload page or check backend logs +- **Prevention:** Backend health monitoring + +## API Reference + +### Frontend + +**Permission Functions:** `frontend/src/utils/permissions.ts` +**Protected Routes:** `frontend/src/router/ProtectedRoute.tsx` +**Role Types:** `frontend/src/types/roles.ts` +**Team Management:** `frontend/src/pages/app/settings/team/TeamPage.tsx` +**Transfer Modal:** `frontend/src/components/domain/team/TransferOwnershipModal.tsx` + +### Backend + +**Tenant Members API:** `services/tenant/app/api/tenant_members.py` +**Tenant Models:** `services/tenant/app/models/tenants.py` +**Tenant Service:** `services/tenant/app/services/tenant_service.py` + +## Migration Notes + +### From Single Role System + +If migrating from a single role system: + +1. **Audit existing roles** + - Map old roles to new structure + - Identify tenant vs global roles + +2. **Update permission checks** + - Replace old checks with unified system + - Test all protected routes + +3. **Migrate user data** + - Set appropriate global roles + - Create tenant memberships + - Ensure owners are properly set + +4. **Update frontend components** + - Use new permission functions + - Update route guards + - Test all scenarios + +## Support + +For issues or questions about the roles and permissions system: + +1. **Check this documentation** +2. **Review code comments** in permission utilities +3. **Check backend logs** for permission errors +4. **Verify tenant membership** in database +5. **Test with different user roles** to isolate issues + +--- + +**Last Updated:** 2025-10-31 +**Version:** 1.0.0 +**Status:** ✅ Production Ready diff --git a/SERVICE_TOKEN_CONFIGURATION.md b/docs/SERVICE_TOKEN_CONFIGURATION.md similarity index 100% rename from SERVICE_TOKEN_CONFIGURATION.md rename to docs/SERVICE_TOKEN_CONFIGURATION.md diff --git a/SESSION_COMPLETE_FUNCTIONAL_TESTING.md b/docs/SESSION_COMPLETE_FUNCTIONAL_TESTING.md similarity index 100% rename from SESSION_COMPLETE_FUNCTIONAL_TESTING.md rename to docs/SESSION_COMPLETE_FUNCTIONAL_TESTING.md diff --git a/SESSION_SUMMARY_SERVICE_TOKENS.md b/docs/SESSION_SUMMARY_SERVICE_TOKENS.md similarity index 100% rename from SESSION_SUMMARY_SERVICE_TOKENS.md rename to docs/SESSION_SUMMARY_SERVICE_TOKENS.md diff --git a/SMART_PROCUREMENT_IMPLEMENTATION.md b/docs/SMART_PROCUREMENT_IMPLEMENTATION.md similarity index 100% rename from SMART_PROCUREMENT_IMPLEMENTATION.md rename to docs/SMART_PROCUREMENT_IMPLEMENTATION.md diff --git a/TENANT_DELETION_IMPLEMENTATION_GUIDE.md b/docs/TENANT_DELETION_IMPLEMENTATION_GUIDE.md similarity index 100% rename from TENANT_DELETION_IMPLEMENTATION_GUIDE.md rename to docs/TENANT_DELETION_IMPLEMENTATION_GUIDE.md diff --git a/TEST_RESULTS_DELETION_SYSTEM.md b/docs/TEST_RESULTS_DELETION_SYSTEM.md similarity index 100% rename from TEST_RESULTS_DELETION_SYSTEM.md rename to docs/TEST_RESULTS_DELETION_SYSTEM.md diff --git a/frontend/src/api/hooks/orders.ts b/frontend/src/api/hooks/orders.ts index e1a117c9..dcf5a358 100644 --- a/frontend/src/api/hooks/orders.ts +++ b/frontend/src/api/hooks/orders.ts @@ -18,22 +18,6 @@ import { GetCustomersParams, UpdateOrderStatusParams, GetDemandRequirementsParams, - // Procurement types - ProcurementPlanResponse, - ProcurementPlanCreate, - ProcurementPlanUpdate, - ProcurementRequirementResponse, - ProcurementRequirementUpdate, - ProcurementDashboardData, - GeneratePlanRequest, - GeneratePlanResponse, - PaginatedProcurementPlans, - GetProcurementPlansParams, - CreatePOsResult, - LinkRequirementToPORequest, - UpdateDeliveryStatusRequest, - GetPlanRequirementsParams, - UpdatePlanStatusParams, } from '../types/orders'; import { ApiError } from '../client/apiClient'; @@ -58,17 +42,6 @@ export const ordersKeys = { // Status status: (tenantId: string) => [...ordersKeys.all, 'status', tenantId] as const, - - // Procurement - procurement: () => [...ordersKeys.all, 'procurement'] as const, - procurementPlans: (params: GetProcurementPlansParams) => [...ordersKeys.procurement(), 'plans', params] as const, - procurementPlan: (tenantId: string, planId: string) => [...ordersKeys.procurement(), 'plan', tenantId, planId] as const, - procurementPlanByDate: (tenantId: string, date: string) => [...ordersKeys.procurement(), 'plan-by-date', tenantId, date] as const, - currentProcurementPlan: (tenantId: string) => [...ordersKeys.procurement(), 'current-plan', tenantId] as const, - procurementDashboard: (tenantId: string) => [...ordersKeys.procurement(), 'dashboard', tenantId] as const, - planRequirements: (params: GetPlanRequirementsParams) => [...ordersKeys.procurement(), 'requirements', params] as const, - criticalRequirements: (tenantId: string) => [...ordersKeys.procurement(), 'critical-requirements', tenantId] as const, - procurementHealth: (tenantId: string) => [...ordersKeys.procurement(), 'health', tenantId] as const, } as const; // ===== Order Queries ===== @@ -360,378 +333,3 @@ export const useInvalidateOrders = () => { }, }; }; - -// ===== Procurement Queries ===== - -export const useProcurementPlans = ( - params: GetProcurementPlansParams, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery({ - queryKey: ordersKeys.procurementPlans(params), - queryFn: () => OrdersService.getProcurementPlans(params), - staleTime: 5 * 60 * 1000, // 5 minutes - enabled: !!params.tenant_id, - ...options, - }); -}; - -export const useProcurementPlan = ( - tenantId: string, - planId: string, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery({ - queryKey: ordersKeys.procurementPlan(tenantId, planId), - queryFn: () => OrdersService.getProcurementPlanById(tenantId, planId), - staleTime: 2 * 60 * 1000, // 2 minutes - enabled: !!tenantId && !!planId, - ...options, - }); -}; - -export const useProcurementPlanByDate = ( - tenantId: string, - planDate: string, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery({ - queryKey: ordersKeys.procurementPlanByDate(tenantId, planDate), - queryFn: () => OrdersService.getProcurementPlanByDate(tenantId, planDate), - staleTime: 2 * 60 * 1000, // 2 minutes - enabled: !!tenantId && !!planDate, - ...options, - }); -}; - -export const useCurrentProcurementPlan = ( - tenantId: string, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery({ - queryKey: ordersKeys.currentProcurementPlan(tenantId), - queryFn: () => OrdersService.getCurrentProcurementPlan(tenantId), - staleTime: 1 * 60 * 1000, // 1 minute - enabled: !!tenantId, - ...options, - }); -}; - -export const useProcurementDashboard = ( - tenantId: string, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery({ - queryKey: ordersKeys.procurementDashboard(tenantId), - queryFn: () => OrdersService.getProcurementDashboard(tenantId), - staleTime: 2 * 60 * 1000, // 2 minutes - enabled: !!tenantId, - ...options, - }); -}; - -export const usePlanRequirements = ( - params: GetPlanRequirementsParams, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery({ - queryKey: ordersKeys.planRequirements(params), - queryFn: () => OrdersService.getPlanRequirements(params), - staleTime: 2 * 60 * 1000, // 2 minutes - enabled: !!params.tenant_id && !!params.plan_id, - ...options, - }); -}; - -export const useCriticalRequirements = ( - tenantId: string, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery({ - queryKey: ordersKeys.criticalRequirements(tenantId), - queryFn: () => OrdersService.getCriticalRequirements(tenantId), - staleTime: 1 * 60 * 1000, // 1 minute - enabled: !!tenantId, - ...options, - }); -}; - -export const useProcurementHealth = ( - tenantId: string, - options?: Omit, 'queryKey' | 'queryFn'> -) => { - return useQuery<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }, ApiError>({ - queryKey: ordersKeys.procurementHealth(tenantId), - queryFn: () => OrdersService.getProcurementHealth(tenantId), - staleTime: 30 * 1000, // 30 seconds - enabled: !!tenantId, - ...options, - }); -}; - -// ===== Procurement Mutations ===== - -export const useGenerateProcurementPlan = ( - options?: UseMutationOptions -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ tenantId, request }) => OrdersService.generateProcurementPlan(tenantId, request), - onSuccess: (data, variables) => { - // Invalidate all procurement queries for this tenant - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - return JSON.stringify(query.queryKey).includes(variables.tenantId); - }, - }); - - // If plan was generated successfully, cache it - if (data.success && data.plan) { - queryClient.setQueryData( - ordersKeys.procurementPlan(variables.tenantId, data.plan.id), - data.plan - ); - } - }, - ...options, - }); -}; - -export const useUpdateProcurementPlanStatus = ( - options?: UseMutationOptions -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (params) => OrdersService.updateProcurementPlanStatus(params), - onSuccess: (data, variables) => { - // Update the specific plan in cache - queryClient.setQueryData( - ordersKeys.procurementPlan(variables.tenant_id, variables.plan_id), - data - ); - - // Invalidate plans list - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - const queryKey = query.queryKey as string[]; - return queryKey.includes('plans') && - JSON.stringify(queryKey).includes(variables.tenant_id); - }, - }); - - // Invalidate dashboard - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurementDashboard(variables.tenant_id), - }); - }, - ...options, - }); -}; - -export const useTriggerDailyScheduler = ( - options?: UseMutationOptions<{ success: boolean; message: string; tenant_id: string }, ApiError, string> -) => { - const queryClient = useQueryClient(); - - return useMutation<{ success: boolean; message: string; tenant_id: string }, ApiError, string>({ - mutationFn: (tenantId) => OrdersService.triggerDailyScheduler(tenantId), - onSuccess: (data, tenantId) => { - // Invalidate all procurement data for this tenant - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - return JSON.stringify(query.queryKey).includes(tenantId); - }, - }); - }, - ...options, - }); -}; - -// ===== NEW PROCUREMENT FEATURE HOOKS ===== - -/** - * Hook to recalculate a procurement plan - */ -export const useRecalculateProcurementPlan = ( - options?: UseMutationOptions -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ tenantId, planId }) => OrdersService.recalculateProcurementPlan(tenantId, planId), - onSuccess: (data, variables) => { - if (data.plan) { - // Update the specific plan in cache - queryClient.setQueryData( - ordersKeys.procurementPlan(variables.tenantId, variables.planId), - data.plan - ); - } - - // Invalidate plans list and dashboard - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - return JSON.stringify(query.queryKey).includes(variables.tenantId); - }, - }); - }, - ...options, - }); -}; - -/** - * Hook to approve a procurement plan - */ -export const useApproveProcurementPlan = ( - options?: UseMutationOptions -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ tenantId, planId, approval_notes }) => - OrdersService.approveProcurementPlan(tenantId, planId, { approval_notes }), - onSuccess: (data, variables) => { - // Update the specific plan in cache - queryClient.setQueryData( - ordersKeys.procurementPlan(variables.tenantId, variables.planId), - data - ); - - // Invalidate plans list and dashboard - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - return JSON.stringify(query.queryKey).includes(variables.tenantId); - }, - }); - }, - ...options, - }); -}; - -/** - * Hook to reject a procurement plan - */ -export const useRejectProcurementPlan = ( - options?: UseMutationOptions -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ tenantId, planId, rejection_notes }) => - OrdersService.rejectProcurementPlan(tenantId, planId, { rejection_notes }), - onSuccess: (data, variables) => { - // Update the specific plan in cache - queryClient.setQueryData( - ordersKeys.procurementPlan(variables.tenantId, variables.planId), - data - ); - - // Invalidate plans list and dashboard - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - return JSON.stringify(query.queryKey).includes(variables.tenantId); - }, - }); - }, - ...options, - }); -}; - -/** - * Hook to create purchase orders from procurement plan - */ -export const useCreatePurchaseOrdersFromPlan = ( - options?: UseMutationOptions -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ tenantId, planId, autoApprove = false }) => - OrdersService.createPurchaseOrdersFromPlan(tenantId, planId, autoApprove), - onSuccess: (data, variables) => { - // Invalidate procurement plan to refresh requirements status - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurementPlan(variables.tenantId, variables.planId), - }); - - // Invalidate dashboard - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurementDashboard(variables.tenantId), - }); - }, - ...options, - }); -}; - -/** - * Hook to link a requirement to a purchase order - */ -export const useLinkRequirementToPurchaseOrder = ( - options?: UseMutationOptions< - { success: boolean; message: string; requirement_id: string; purchase_order_id: string }, - ApiError, - { tenantId: string; requirementId: string; request: LinkRequirementToPORequest } - > -) => { - const queryClient = useQueryClient(); - - return useMutation< - { success: boolean; message: string; requirement_id: string; purchase_order_id: string }, - ApiError, - { tenantId: string; requirementId: string; request: LinkRequirementToPORequest } - >({ - mutationFn: ({ tenantId, requirementId, request }) => - OrdersService.linkRequirementToPurchaseOrder(tenantId, requirementId, request), - onSuccess: (data, variables) => { - // Invalidate procurement data to refresh requirements - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - return JSON.stringify(query.queryKey).includes(variables.tenantId); - }, - }); - }, - ...options, - }); -}; - -/** - * Hook to update delivery status for a requirement - */ -export const useUpdateRequirementDeliveryStatus = ( - options?: UseMutationOptions< - { success: boolean; message: string; requirement_id: string; delivery_status: string }, - ApiError, - { tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest } - > -) => { - const queryClient = useQueryClient(); - - return useMutation< - { success: boolean; message: string; requirement_id: string; delivery_status: string }, - ApiError, - { tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest } - >({ - mutationFn: ({ tenantId, requirementId, request }) => - OrdersService.updateRequirementDeliveryStatus(tenantId, requirementId, request), - onSuccess: (data, variables) => { - // Invalidate procurement data to refresh requirements - queryClient.invalidateQueries({ - queryKey: ordersKeys.procurement(), - predicate: (query) => { - return JSON.stringify(query.queryKey).includes(variables.tenantId); - }, - }); - }, - ...options, - }); -}; - diff --git a/frontend/src/api/hooks/performance.ts b/frontend/src/api/hooks/performance.ts new file mode 100644 index 00000000..0ded5dcd --- /dev/null +++ b/frontend/src/api/hooks/performance.ts @@ -0,0 +1,926 @@ +/** + * Performance Analytics Hooks + * React Query hooks for fetching real-time performance data across all departments + */ + +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { + PerformanceOverview, + DepartmentPerformance, + KPIMetric, + PerformanceAlert, + HourlyProductivity, + ProductionPerformance, + InventoryPerformance, + SalesPerformance, + ProcurementPerformance, + TimePeriod, +} from '../types/performance'; +import { useProductionDashboard } from './production'; +import { useInventoryDashboard } from './dashboard'; +import { useSalesAnalytics } from './sales'; +import { useProcurementDashboard } from './procurement'; +import { useOrdersDashboard } from './orders'; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +const getDateRangeForPeriod = (period: TimePeriod): { startDate: string; endDate: string } => { + const endDate = new Date(); + const startDate = new Date(); + + switch (period) { + case 'day': + startDate.setDate(endDate.getDate() - 1); + break; + case 'week': + startDate.setDate(endDate.getDate() - 7); + break; + case 'month': + startDate.setMonth(endDate.getMonth() - 1); + break; + case 'quarter': + startDate.setMonth(endDate.getMonth() - 3); + break; + case 'year': + startDate.setFullYear(endDate.getFullYear() - 1); + break; + } + + return { + startDate: startDate.toISOString().split('T')[0], + endDate: endDate.toISOString().split('T')[0], + }; +}; + +const calculateTrend = (current: number, previous: number): 'up' | 'down' | 'stable' => { + const change = ((current - previous) / previous) * 100; + if (Math.abs(change) < 1) return 'stable'; + return change > 0 ? 'up' : 'down'; +}; + +const calculateStatus = (current: number, target: number): 'good' | 'warning' | 'critical' => { + const percentage = (current / target) * 100; + if (percentage >= 95) return 'good'; + if (percentage >= 85) return 'warning'; + return 'critical'; +}; + +// ============================================================================ +// Production Performance Hook +// ============================================================================ + +export const useProductionPerformance = (tenantId: string, period: TimePeriod = 'week') => { + const { data: dashboard, isLoading: dashboardLoading } = useProductionDashboard(tenantId); + + // Extract primitive values to prevent unnecessary recalculations + const efficiencyPercentage = dashboard?.efficiency_percentage || 0; + const qualityScore = dashboard?.average_quality_score || 0; + const capacityUtilization = dashboard?.capacity_utilization || 0; + const onTimeCompletionRate = dashboard?.on_time_completion_rate || 0; + + const performance: ProductionPerformance | undefined = useMemo(() => { + if (!dashboard) return undefined; + + return { + efficiency: efficiencyPercentage, + average_batch_time: 0, // Not available in dashboard + quality_rate: qualityScore, + waste_percentage: 0, // Would need production-trends endpoint + capacity_utilization: capacityUtilization, + equipment_efficiency: capacityUtilization, + on_time_completion_rate: onTimeCompletionRate, + yield_rate: 0, // Would need production-trends endpoint + }; + }, [efficiencyPercentage, qualityScore, capacityUtilization, onTimeCompletionRate]); + + return { + data: performance, + isLoading: dashboardLoading, + }; +}; + +// ============================================================================ +// Inventory Performance Hook +// ============================================================================ + +export const useInventoryPerformance = (tenantId: string) => { + const { data: dashboard, isLoading: dashboardLoading } = useInventoryDashboard(tenantId); + + // Extract primitive values to prevent unnecessary recalculations + const totalItems = dashboard?.total_ingredients || 1; + const lowStockCount = dashboard?.low_stock_items || 0; + const outOfStockCount = dashboard?.out_of_stock_items || 0; + const foodSafetyAlertsActive = dashboard?.food_safety_alerts_active || 0; + const expiringItems = dashboard?.expiring_soon_items || 0; + const stockValue = dashboard?.total_stock_value || 0; + + const performance: InventoryPerformance | undefined = useMemo(() => { + if (!dashboard) return undefined; + + return { + stock_accuracy: 100 - ((lowStockCount + outOfStockCount) / totalItems * 100), + turnover_rate: 0, // TODO: Not available in dashboard + waste_rate: 0, // TODO: Derive from stock movements if available + low_stock_count: lowStockCount, + compliance_rate: foodSafetyAlertsActive === 0 ? 100 : 90, // Simplified compliance + expiring_items_count: expiringItems, + stock_value: stockValue, + }; + }, [totalItems, lowStockCount, outOfStockCount, foodSafetyAlertsActive, expiringItems, stockValue]); + + return { + data: performance, + isLoading: dashboardLoading, + }; +}; + +// ============================================================================ +// Sales Performance Hook +// ============================================================================ + +export const useSalesPerformance = (tenantId: string, period: TimePeriod = 'week') => { + const { startDate, endDate } = getDateRangeForPeriod(period); + + const { data: salesData, isLoading: salesLoading } = useSalesAnalytics( + tenantId, + startDate, + endDate + ); + + // Extract primitive values to prevent unnecessary recalculations + const totalRevenue = salesData?.total_revenue || 0; + const totalTransactions = salesData?.total_transactions || 0; + const avgTransactionValue = salesData?.average_transaction_value || 0; + const topProductsString = salesData?.top_products ? JSON.stringify(salesData.top_products) : '[]'; + + const performance: SalesPerformance | undefined = useMemo(() => { + if (!salesData) return undefined; + + const topProducts = JSON.parse(topProductsString); + + return { + total_revenue: totalRevenue, + total_transactions: totalTransactions, + average_transaction_value: avgTransactionValue, + growth_rate: 0, // TODO: Calculate from trends + channel_performance: [], // TODO: Parse from sales_by_channel if needed + top_products: Array.isArray(topProducts) + ? topProducts.map((product: any) => ({ + product_id: product.inventory_product_id || '', + product_name: product.product_name || '', + sales: product.total_quantity || 0, + revenue: product.total_revenue || 0, + })) + : [], + }; + }, [totalRevenue, totalTransactions, avgTransactionValue, topProductsString]); + + return { + data: performance, + isLoading: salesLoading, + }; +}; + +// ============================================================================ +// Procurement Performance Hook +// ============================================================================ + +export const useProcurementPerformance = (tenantId: string) => { + const { data: dashboard, isLoading } = useProcurementDashboard(tenantId); + + // Extract primitive values to prevent unnecessary recalculations + const avgFulfillmentRate = dashboard?.performance_metrics?.average_fulfillment_rate || 0; + const avgOnTimeDelivery = dashboard?.performance_metrics?.average_on_time_delivery || 0; + const costAccuracy = dashboard?.performance_metrics?.cost_accuracy || 0; + const supplierPerformance = dashboard?.performance_metrics?.supplier_performance || 0; + const totalPlans = dashboard?.summary?.total_plans || 0; + const lowStockCount = dashboard?.low_stock_alerts?.length || 0; + const overdueCount = dashboard?.overdue_requirements?.length || 0; + + const performance: ProcurementPerformance | undefined = useMemo(() => { + if (!dashboard) return undefined; + + return { + fulfillment_rate: avgFulfillmentRate, + on_time_delivery_rate: avgOnTimeDelivery, + cost_accuracy: costAccuracy, + supplier_performance_score: supplierPerformance, + active_plans: totalPlans, + critical_requirements: lowStockCount + overdueCount, + }; + }, [avgFulfillmentRate, avgOnTimeDelivery, costAccuracy, supplierPerformance, totalPlans, lowStockCount, overdueCount]); + + return { + data: performance, + isLoading, + }; +}; + +// ============================================================================ +// Performance Overview Hook (Aggregates All Departments) +// ============================================================================ + +export const usePerformanceOverview = (tenantId: string, period: TimePeriod = 'week') => { + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + const { data: sales, isLoading: salesLoading } = useSalesPerformance(tenantId, period); + const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId); + const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId); + + const overview: PerformanceOverview | undefined = useMemo(() => { + if (!production || !inventory || !sales || !procurement || !orders) return undefined; + + // Calculate customer satisfaction from order fulfillment and delivery performance + const totalOrders = orders.total_orders_today || 1; + const deliveredOrders = orders.delivered_orders || 0; + const orderFulfillmentRate = (deliveredOrders / totalOrders) * 100; + + const customerSatisfaction = (orderFulfillmentRate + procurement.on_time_delivery_rate) / 2; + + return { + overall_efficiency: production.efficiency, + average_production_time: production.average_batch_time, + quality_score: production.quality_rate, + employee_productivity: production.capacity_utilization, + customer_satisfaction: customerSatisfaction, + resource_utilization: production.equipment_efficiency || production.capacity_utilization, + }; + }, [production, inventory, sales, procurement, orders]); + + return { + data: overview, + isLoading: productionLoading || inventoryLoading || salesLoading || procurementLoading || ordersLoading, + }; +}; + +// ============================================================================ +// Department Performance Hook +// ============================================================================ + +export const useDepartmentPerformance = (tenantId: string, period: TimePeriod = 'week') => { + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + const { data: sales, isLoading: salesLoading } = useSalesPerformance(tenantId, period); + const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId); + + // Extract primitive values before useMemo to prevent unnecessary recalculations + const productionEfficiency = production?.efficiency || 0; + const productionAvgBatchTime = production?.average_batch_time || 0; + const productionQualityRate = production?.quality_rate || 0; + const productionWastePercentage = production?.waste_percentage || 0; + const salesTotalRevenue = sales?.total_revenue || 0; + const salesGrowthRate = sales?.growth_rate || 0; + const salesTotalTransactions = sales?.total_transactions || 0; + const salesAvgTransactionValue = sales?.average_transaction_value || 0; + const inventoryStockAccuracy = inventory?.stock_accuracy || 0; + const inventoryLowStockCount = inventory?.low_stock_count || 0; + const inventoryTurnoverRate = inventory?.turnover_rate || 0; + const procurementFulfillmentRate = procurement?.fulfillment_rate || 0; + const procurementOnTimeDeliveryRate = procurement?.on_time_delivery_rate || 0; + const procurementCostAccuracy = procurement?.cost_accuracy || 0; + + const departments: DepartmentPerformance[] | undefined = useMemo(() => { + if (!production || !inventory || !sales || !procurement) return undefined; + + return [ + { + department_id: 'production', + department_name: 'Producción', + efficiency: productionEfficiency, + trend: productionEfficiency >= 85 ? 'up' : productionEfficiency >= 75 ? 'stable' : 'down', + metrics: { + primary_metric: { + label: 'Tiempo promedio de lote', + value: productionAvgBatchTime, + unit: 'h', + }, + secondary_metric: { + label: 'Tasa de calidad', + value: productionQualityRate, + unit: '%', + }, + tertiary_metric: { + label: 'Desperdicio', + value: productionWastePercentage, + unit: '%', + }, + }, + }, + { + department_id: 'sales', + department_name: 'Ventas', + efficiency: (salesTotalRevenue / 10000) * 100, // Normalize to percentage + trend: salesGrowthRate > 0 ? 'up' : salesGrowthRate < 0 ? 'down' : 'stable', + metrics: { + primary_metric: { + label: 'Ingresos totales', + value: salesTotalRevenue, + unit: '€', + }, + secondary_metric: { + label: 'Transacciones', + value: salesTotalTransactions, + unit: '', + }, + tertiary_metric: { + label: 'Valor promedio', + value: salesAvgTransactionValue, + unit: '€', + }, + }, + }, + { + department_id: 'inventory', + department_name: 'Inventario', + efficiency: inventoryStockAccuracy, + trend: inventoryLowStockCount < 5 ? 'up' : inventoryLowStockCount < 10 ? 'stable' : 'down', + metrics: { + primary_metric: { + label: 'Rotación de stock', + value: inventoryTurnoverRate, + unit: 'x', + }, + secondary_metric: { + label: 'Precisión', + value: inventoryStockAccuracy, + unit: '%', + }, + tertiary_metric: { + label: 'Items con bajo stock', + value: inventoryLowStockCount, + unit: '', + }, + }, + }, + { + department_id: 'administration', + department_name: 'Administración', + efficiency: procurementFulfillmentRate, + trend: procurementOnTimeDeliveryRate >= 95 ? 'up' : procurementOnTimeDeliveryRate >= 85 ? 'stable' : 'down', + metrics: { + primary_metric: { + label: 'Tasa de cumplimiento', + value: procurementFulfillmentRate, + unit: '%', + }, + secondary_metric: { + label: 'Entrega a tiempo', + value: procurementOnTimeDeliveryRate, + unit: '%', + }, + tertiary_metric: { + label: 'Precisión de costos', + value: procurementCostAccuracy, + unit: '%', + }, + }, + }, + ]; + }, [ + productionEfficiency, + productionAvgBatchTime, + productionQualityRate, + productionWastePercentage, + salesTotalRevenue, + salesGrowthRate, + salesTotalTransactions, + salesAvgTransactionValue, + inventoryStockAccuracy, + inventoryLowStockCount, + inventoryTurnoverRate, + procurementFulfillmentRate, + procurementOnTimeDeliveryRate, + procurementCostAccuracy, + ]); + + return { + data: departments, + isLoading: productionLoading || inventoryLoading || salesLoading || procurementLoading, + }; +}; + +// ============================================================================ +// KPI Metrics Hook +// ============================================================================ + +export const useKPIMetrics = (tenantId: string, period: TimePeriod = 'week') => { + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId); + + const kpis: KPIMetric[] | undefined = useMemo(() => { + if (!production || !inventory || !procurement) return undefined; + + // TODO: Get previous period data for accurate trends + const previousProduction = production.efficiency * 0.95; // Mock previous value + const previousInventory = inventory.stock_accuracy * 0.98; + const previousProcurement = procurement.on_time_delivery_rate * 0.97; + const previousQuality = production.quality_rate * 0.96; + + return [ + { + id: 'overall-efficiency', + name: 'Eficiencia General', + current_value: production.efficiency, + target_value: 90, + previous_value: previousProduction, + unit: '%', + trend: calculateTrend(production.efficiency, previousProduction), + status: calculateStatus(production.efficiency, 90), + }, + { + id: 'quality-rate', + name: 'Tasa de Calidad', + current_value: production.quality_rate, + target_value: 95, + previous_value: previousQuality, + unit: '%', + trend: calculateTrend(production.quality_rate, previousQuality), + status: calculateStatus(production.quality_rate, 95), + }, + { + id: 'on-time-delivery', + name: 'Entrega a Tiempo', + current_value: procurement.on_time_delivery_rate, + target_value: 95, + previous_value: previousProcurement, + unit: '%', + trend: calculateTrend(procurement.on_time_delivery_rate, previousProcurement), + status: calculateStatus(procurement.on_time_delivery_rate, 95), + }, + { + id: 'inventory-accuracy', + name: 'Precisión de Inventario', + current_value: inventory.stock_accuracy, + target_value: 98, + previous_value: previousInventory, + unit: '%', + trend: calculateTrend(inventory.stock_accuracy, previousInventory), + status: calculateStatus(inventory.stock_accuracy, 98), + }, + ]; + }, [production, inventory, procurement]); + + return { + data: kpis, + isLoading: productionLoading || inventoryLoading || procurementLoading, + }; +}; + +// ============================================================================ +// Performance Alerts Hook +// ============================================================================ + +export const usePerformanceAlerts = (tenantId: string) => { + const { data: inventory, isLoading: inventoryLoading } = useInventoryDashboard(tenantId); + const { data: procurement, isLoading: procurementLoading } = useProcurementDashboard(tenantId); + + // Extract primitive values to prevent unnecessary recalculations + const lowStockCount = inventory?.low_stock_items || 0; + const outOfStockCount = inventory?.out_of_stock_items || 0; + const foodSafetyAlerts = inventory?.food_safety_alerts_active || 0; + const expiringCount = inventory?.expiring_soon_items || 0; + const lowStockAlertsCount = procurement?.low_stock_alerts?.length || 0; + const overdueReqsCount = procurement?.overdue_requirements?.length || 0; + + const alerts: PerformanceAlert[] | undefined = useMemo(() => { + if (!inventory || !procurement) return undefined; + + const alertsList: PerformanceAlert[] = []; + + // Low stock alerts + if (lowStockCount > 0) { + alertsList.push({ + id: `low-stock-${Date.now()}`, + type: 'warning', + department: 'Inventario', + message: `${lowStockCount} ingredientes con stock bajo`, + timestamp: new Date().toISOString(), + metric_affected: 'Stock', + current_value: lowStockCount, + }); + } + + // Out of stock alerts + if (outOfStockCount > 0) { + alertsList.push({ + id: `out-of-stock-${Date.now()}`, + type: 'critical', + department: 'Inventario', + message: `${outOfStockCount} ingredientes sin stock`, + timestamp: new Date().toISOString(), + metric_affected: 'Stock', + current_value: outOfStockCount, + }); + } + + // Food safety alerts + if (foodSafetyAlerts > 0) { + alertsList.push({ + id: `food-safety-${Date.now()}`, + type: 'critical', + department: 'Inventario', + message: `${foodSafetyAlerts} alertas de seguridad alimentaria activas`, + timestamp: new Date().toISOString(), + metric_affected: 'Seguridad Alimentaria', + current_value: foodSafetyAlerts, + }); + } + + // Expiring items alerts + if (expiringCount > 0) { + alertsList.push({ + id: `expiring-${Date.now()}`, + type: 'warning', + department: 'Inventario', + message: `${expiringCount} ingredientes próximos a vencer`, + timestamp: new Date().toISOString(), + metric_affected: 'Caducidad', + current_value: expiringCount, + }); + } + + // Critical procurement requirements + const criticalCount = lowStockAlertsCount + overdueReqsCount; + + if (criticalCount > 0) { + alertsList.push({ + id: `procurement-critical-${Date.now()}`, + type: 'warning', + department: 'Administración', + message: `${criticalCount} requisitos de compra críticos`, + timestamp: new Date().toISOString(), + metric_affected: 'Aprovisionamiento', + current_value: criticalCount, + }); + } + + // Sort by severity: critical > warning > info + return alertsList.sort((a, b) => { + const severityOrder = { critical: 0, warning: 1, info: 2 }; + return severityOrder[a.type] - severityOrder[b.type]; + }); + }, [lowStockCount, outOfStockCount, foodSafetyAlerts, expiringCount, lowStockAlertsCount, overdueReqsCount]); + + return { + data: alerts || [], + isLoading: inventoryLoading || procurementLoading, + }; +}; + +// ============================================================================ +// Hourly Productivity Hook +// ============================================================================ + +export const useHourlyProductivity = (tenantId: string) => { + // TODO: This requires time-series data aggregation from production batches + // For now, returning empty until backend provides hourly aggregation endpoint + + return useQuery({ + queryKey: ['performance', 'hourly', tenantId], + queryFn: async () => { + // Placeholder - backend endpoint needed for real hourly data + return []; + }, + enabled: false, // Disable until backend endpoint is ready + }); +}; + +// ============================================================================ +// Cross-Functional Performance Metrics +// ============================================================================ + +/** + * Cycle Time: Order-to-Delivery + * Measures the complete time from order creation to delivery + */ +export const useCycleTimeMetrics = (tenantId: string, period: TimePeriod = 'week') => { + const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId); + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + + // Extract primitive values before useMemo to prevent unnecessary recalculations + const totalOrders = orders?.total_orders_today || 1; + const deliveredOrders = orders?.delivered_orders || 0; + const pendingOrders = orders?.pending_orders || 0; + const avgProductionTime = production?.average_batch_time || 2; + const onTimeCompletionRate = production?.on_time_completion_rate || 0; + + const cycleTime = useMemo(() => { + if (!orders || !production) return undefined; + + // Estimate average cycle time based on fulfillment rate and production efficiency + const fulfillmentRate = (deliveredOrders / totalOrders) * 100; + + // Estimated total cycle time includes: order processing + production + delivery + const estimatedCycleTime = avgProductionTime + (pendingOrders > 0 ? 1.5 : 0.5); // Add wait time + + return { + average_cycle_time: estimatedCycleTime, + order_to_production_time: 0.5, // Order processing time + production_time: avgProductionTime, + production_to_delivery_time: pendingOrders > 0 ? 1.0 : 0.3, + fulfillment_rate: fulfillmentRate, + on_time_delivery_rate: onTimeCompletionRate, + }; + }, [totalOrders, deliveredOrders, pendingOrders, avgProductionTime, onTimeCompletionRate]); + + return { + data: cycleTime, + isLoading: ordersLoading || productionLoading, + }; +}; + +/** + * Process Efficiency Score + * Combined efficiency across all departments + */ +export const useProcessEfficiencyScore = (tenantId: string, period: TimePeriod = 'week') => { + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId); + const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId); + + // Extract primitive values before useMemo to prevent unnecessary recalculations + const productionEfficiency = production?.efficiency || 0; + const inventoryStockAccuracy = inventory?.stock_accuracy || 0; + const procurementFulfillmentRate = procurement?.fulfillment_rate || 0; + const totalOrders = orders?.total_orders_today || 1; + const deliveredOrders = orders?.delivered_orders || 0; + + const score = useMemo(() => { + if (!production || !inventory || !procurement || !orders) return undefined; + + // Weighted efficiency score across departments + const productionWeight = 0.35; + const inventoryWeight = 0.25; + const procurementWeight = 0.25; + const ordersWeight = 0.15; + + const orderEfficiency = (deliveredOrders / totalOrders) * 100; + + const overallScore = + (productionEfficiency * productionWeight) + + (inventoryStockAccuracy * inventoryWeight) + + (procurementFulfillmentRate * procurementWeight) + + (orderEfficiency * ordersWeight); + + return { + overall_score: overallScore, + production_efficiency: productionEfficiency, + inventory_efficiency: inventoryStockAccuracy, + procurement_efficiency: procurementFulfillmentRate, + order_efficiency: orderEfficiency, + breakdown: { + production: { value: productionEfficiency, weight: productionWeight * 100 }, + inventory: { value: inventoryStockAccuracy, weight: inventoryWeight * 100 }, + procurement: { value: procurementFulfillmentRate, weight: procurementWeight * 100 }, + orders: { value: orderEfficiency, weight: ordersWeight * 100 }, + }, + }; + }, [productionEfficiency, inventoryStockAccuracy, procurementFulfillmentRate, totalOrders, deliveredOrders]); + + return { + data: score, + isLoading: productionLoading || inventoryLoading || procurementLoading || ordersLoading, + }; +}; + +/** + * Resource Utilization Rate + * Cross-departmental resource balance and utilization + */ +export const useResourceUtilization = (tenantId: string, period: TimePeriod = 'week') => { + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + + // Extract primitive values before useMemo to prevent unnecessary recalculations + const equipmentEfficiency = production?.equipment_efficiency || 0; + const turnoverRate = inventory?.turnover_rate || 0; + const stockAccuracy = inventory?.stock_accuracy || 0; + const capacityUtilization = production?.capacity_utilization || 0; + + const utilization = useMemo(() => { + if (!production || !inventory) return undefined; + + // Equipment utilization from production + const equipmentUtilization = equipmentEfficiency; + + // Inventory utilization based on turnover and stock levels + const inventoryUtilization = turnoverRate > 0 + ? Math.min(turnoverRate * 10, 100) // Normalize turnover to percentage + : stockAccuracy; + + // Combined resource utilization + const overallUtilization = (equipmentUtilization + inventoryUtilization) / 2; + + return { + overall_utilization: overallUtilization, + equipment_utilization: equipmentUtilization, + inventory_utilization: inventoryUtilization, + capacity_used: capacityUtilization, + resource_balance: Math.abs(equipmentUtilization - inventoryUtilization) < 10 ? 'balanced' : 'imbalanced', + }; + }, [equipmentEfficiency, turnoverRate, stockAccuracy, capacityUtilization]); + + return { + data: utilization, + isLoading: productionLoading || inventoryLoading, + }; +}; + +/** + * Cost-to-Revenue Ratio + * Overall profitability metric + */ +export const useCostRevenueRatio = (tenantId: string, period: TimePeriod = 'week') => { + const { data: sales, isLoading: salesLoading } = useSalesPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + + // Extract primitive values before useMemo to prevent unnecessary recalculations + const totalRevenue = sales?.total_revenue || 0; + const stockValue = inventory?.stock_value || 0; + const wastePercentage = production?.waste_percentage || 0; + + const ratio = useMemo(() => { + if (!sales || !inventory || !production) return undefined; + + // Estimate costs from inventory value and waste + const inventoryCosts = stockValue * 0.1; // Approximate monthly inventory cost + const wasteCosts = (stockValue * wastePercentage) / 100; + const estimatedTotalCosts = inventoryCosts + wasteCosts; + + const costRevenueRatio = totalRevenue > 0 ? (estimatedTotalCosts / totalRevenue) * 100 : 0; + const profitMargin = totalRevenue > 0 ? ((totalRevenue - estimatedTotalCosts) / totalRevenue) * 100 : 0; + + return { + cost_revenue_ratio: costRevenueRatio, + profit_margin: profitMargin, + total_revenue: totalRevenue, + estimated_costs: estimatedTotalCosts, + inventory_costs: inventoryCosts, + waste_costs: wasteCosts, + }; + }, [totalRevenue, stockValue, wastePercentage]); + + return { + data: ratio, + isLoading: salesLoading || inventoryLoading || productionLoading, + }; +}; + +/** + * Quality Impact Index + * Quality issues across all departments + */ +export const useQualityImpactIndex = (tenantId: string, period: TimePeriod = 'week') => { + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + + // Extract primitive values before useMemo to prevent unnecessary recalculations + const productionQuality = production?.quality_rate || 0; + const wasteImpact = production?.waste_percentage || 0; + const expiringItems = inventory?.expiring_items_count || 0; + const lowStockItems = inventory?.low_stock_count || 0; + + const qualityIndex = useMemo(() => { + if (!production || !inventory) return undefined; + + // Inventory quality + const totalItems = (expiringItems + lowStockItems) || 1; + const inventoryQualityScore = 100 - ((expiringItems + lowStockItems) / totalItems * 10); + + // Combined quality index (weighted average) + const overallQuality = (productionQuality * 0.7) + (inventoryQualityScore * 0.3); + + return { + overall_quality_index: overallQuality, + production_quality: productionQuality, + inventory_quality: inventoryQualityScore, + waste_impact: wasteImpact, + quality_issues: { + production_defects: 100 - productionQuality, + waste_percentage: wasteImpact, + expiring_items: expiringItems, + low_stock_affecting_quality: lowStockItems, + }, + }; + }, [productionQuality, wasteImpact, expiringItems, lowStockItems]); + + return { + data: qualityIndex, + isLoading: productionLoading || inventoryLoading, + }; +}; + +/** + * Critical Bottlenecks + * Identifies process bottlenecks across operations + */ +export const useCriticalBottlenecks = (tenantId: string, period: TimePeriod = 'week') => { + const { data: production, isLoading: productionLoading } = useProductionPerformance(tenantId, period); + const { data: inventory, isLoading: inventoryLoading } = useInventoryPerformance(tenantId); + const { data: procurement, isLoading: procurementLoading } = useProcurementPerformance(tenantId); + const { data: orders, isLoading: ordersLoading } = useOrdersDashboard(tenantId); + + // Extract primitive values before useMemo to prevent unnecessary recalculations + const capacityUtilization = production?.capacity_utilization || 0; + const onTimeCompletionRate = production?.on_time_completion_rate || 0; + const lowStockCount = inventory?.low_stock_count || 0; + const criticalRequirements = procurement?.critical_requirements || 0; + const onTimeDeliveryRate = procurement?.on_time_delivery_rate || 0; + const totalOrders = orders?.total_orders_today || 1; + const pendingOrders = orders?.pending_orders || 0; + + const bottlenecks = useMemo(() => { + if (!production || !inventory || !procurement || !orders) return undefined; + + const bottlenecksList = []; + + // Production bottlenecks + if (capacityUtilization > 90) { + bottlenecksList.push({ + area: 'production', + severity: 'high', + description: 'Capacidad de producción al límite', + metric: 'capacity_utilization', + value: capacityUtilization, + }); + } + + if (onTimeCompletionRate < 85) { + bottlenecksList.push({ + area: 'production', + severity: 'medium', + description: 'Retrasos en completitud de producción', + metric: 'on_time_completion', + value: onTimeCompletionRate, + }); + } + + // Inventory bottlenecks + if (lowStockCount > 10) { + bottlenecksList.push({ + area: 'inventory', + severity: 'high', + description: 'Alto número de ingredientes con stock bajo', + metric: 'low_stock_count', + value: lowStockCount, + }); + } + + // Procurement bottlenecks + if (criticalRequirements > 5) { + bottlenecksList.push({ + area: 'procurement', + severity: 'high', + description: 'Requisitos de compra críticos pendientes', + metric: 'critical_requirements', + value: criticalRequirements, + }); + } + + if (onTimeDeliveryRate < 85) { + bottlenecksList.push({ + area: 'procurement', + severity: 'medium', + description: 'Entregas de proveedores retrasadas', + metric: 'on_time_delivery', + value: onTimeDeliveryRate, + }); + } + + // Orders bottlenecks + const pendingRate = (pendingOrders / totalOrders) * 100; + + if (pendingRate > 30) { + bottlenecksList.push({ + area: 'orders', + severity: 'medium', + description: 'Alto volumen de pedidos pendientes', + metric: 'pending_orders', + value: pendingOrders, + }); + } + + return { + total_bottlenecks: bottlenecksList.length, + critical_count: bottlenecksList.filter(b => b.severity === 'high').length, + bottlenecks: bottlenecksList, + most_critical_area: bottlenecksList.length > 0 + ? bottlenecksList.sort((a, b) => { + const severityOrder = { high: 0, medium: 1, low: 2 }; + return severityOrder[a.severity as 'high' | 'medium' | 'low'] - severityOrder[b.severity as 'high' | 'medium' | 'low']; + })[0].area + : null, + }; + }, [capacityUtilization, onTimeCompletionRate, lowStockCount, criticalRequirements, onTimeDeliveryRate, totalOrders, pendingOrders]); + + return { + data: bottlenecks, + isLoading: productionLoading || inventoryLoading || procurementLoading || ordersLoading, + }; +}; diff --git a/frontend/src/api/hooks/procurement.ts b/frontend/src/api/hooks/procurement.ts new file mode 100644 index 00000000..87952927 --- /dev/null +++ b/frontend/src/api/hooks/procurement.ts @@ -0,0 +1,495 @@ +/** + * Procurement React Query hooks + * All hooks use the ProcurementService which connects to the standalone Procurement Service backend + */ +import { + useMutation, + useQuery, + useQueryClient, + UseQueryOptions, + UseMutationOptions, +} from '@tanstack/react-query'; +import { ProcurementService } from '../services/procurement-service'; +import { + // Response types + ProcurementPlanResponse, + ProcurementRequirementResponse, + ProcurementDashboardData, + ProcurementTrendsData, + PaginatedProcurementPlans, + GeneratePlanResponse, + CreatePOsResult, + + // Request types + GeneratePlanRequest, + AutoGenerateProcurementRequest, + AutoGenerateProcurementResponse, + LinkRequirementToPORequest, + UpdateDeliveryStatusRequest, + + // Query param types + GetProcurementPlansParams, + GetPlanRequirementsParams, + UpdatePlanStatusParams, +} from '../types/procurement'; +import { ApiError } from '../client/apiClient'; + +// =================================================================== +// QUERY KEYS +// =================================================================== + +export const procurementKeys = { + all: ['procurement'] as const, + + // Analytics & Dashboard + analytics: (tenantId: string) => [...procurementKeys.all, 'analytics', tenantId] as const, + trends: (tenantId: string, days: number) => [...procurementKeys.all, 'trends', tenantId, days] as const, + + // Plans + plans: () => [...procurementKeys.all, 'plans'] as const, + plansList: (params: GetProcurementPlansParams) => [...procurementKeys.plans(), 'list', params] as const, + plan: (tenantId: string, planId: string) => [...procurementKeys.plans(), 'detail', tenantId, planId] as const, + planByDate: (tenantId: string, date: string) => [...procurementKeys.plans(), 'by-date', tenantId, date] as const, + currentPlan: (tenantId: string) => [...procurementKeys.plans(), 'current', tenantId] as const, + + // Requirements + requirements: () => [...procurementKeys.all, 'requirements'] as const, + planRequirements: (params: GetPlanRequirementsParams) => + [...procurementKeys.requirements(), 'plan', params] as const, + criticalRequirements: (tenantId: string) => + [...procurementKeys.requirements(), 'critical', tenantId] as const, +} as const; + +// =================================================================== +// ANALYTICS & DASHBOARD QUERIES +// =================================================================== + +/** + * Get procurement analytics dashboard data + */ +export const useProcurementDashboard = ( + tenantId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.analytics(tenantId), + queryFn: () => ProcurementService.getProcurementAnalytics(tenantId), + staleTime: 2 * 60 * 1000, // 2 minutes + enabled: !!tenantId, + ...options, + }); +}; + +/** + * Get procurement time-series trends for charts + */ +export const useProcurementTrends = ( + tenantId: string, + days: number = 7, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.trends(tenantId, days), + queryFn: () => ProcurementService.getProcurementTrends(tenantId, days), + staleTime: 5 * 60 * 1000, // 5 minutes + enabled: !!tenantId, + ...options, + }); +}; + +// =================================================================== +// PROCUREMENT PLAN QUERIES +// =================================================================== + +/** + * Get list of procurement plans with pagination and filtering + */ +export const useProcurementPlans = ( + params: GetProcurementPlansParams, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.plansList(params), + queryFn: () => ProcurementService.getProcurementPlans(params), + staleTime: 5 * 60 * 1000, // 5 minutes + enabled: !!params.tenant_id, + ...options, + }); +}; + +/** + * Get a single procurement plan by ID + */ +export const useProcurementPlan = ( + tenantId: string, + planId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.plan(tenantId, planId), + queryFn: () => ProcurementService.getProcurementPlanById(tenantId, planId), + staleTime: 2 * 60 * 1000, // 2 minutes + enabled: !!tenantId && !!planId, + ...options, + }); +}; + +/** + * Get procurement plan for a specific date + */ +export const useProcurementPlanByDate = ( + tenantId: string, + planDate: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.planByDate(tenantId, planDate), + queryFn: () => ProcurementService.getProcurementPlanByDate(tenantId, planDate), + staleTime: 2 * 60 * 1000, // 2 minutes + enabled: !!tenantId && !!planDate, + ...options, + }); +}; + +/** + * Get the current day's procurement plan + */ +export const useCurrentProcurementPlan = ( + tenantId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.currentPlan(tenantId), + queryFn: () => ProcurementService.getCurrentProcurementPlan(tenantId), + staleTime: 1 * 60 * 1000, // 1 minute + enabled: !!tenantId, + ...options, + }); +}; + +// =================================================================== +// PROCUREMENT REQUIREMENTS QUERIES +// =================================================================== + +/** + * Get requirements for a specific procurement plan + */ +export const usePlanRequirements = ( + params: GetPlanRequirementsParams, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.planRequirements(params), + queryFn: () => ProcurementService.getPlanRequirements(params), + staleTime: 2 * 60 * 1000, // 2 minutes + enabled: !!params.tenant_id && !!params.plan_id, + ...options, + }); +}; + +/** + * Get critical requirements across all plans + */ +export const useCriticalRequirements = ( + tenantId: string, + options?: Omit, 'queryKey' | 'queryFn'> +) => { + return useQuery({ + queryKey: procurementKeys.criticalRequirements(tenantId), + queryFn: () => ProcurementService.getCriticalRequirements(tenantId), + staleTime: 1 * 60 * 1000, // 1 minute + enabled: !!tenantId, + ...options, + }); +}; + +// =================================================================== +// PROCUREMENT PLAN MUTATIONS +// =================================================================== + +/** + * Generate a new procurement plan (manual/UI-driven) + */ +export const useGenerateProcurementPlan = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ tenantId, request }) => ProcurementService.generateProcurementPlan(tenantId, request), + onSuccess: (data, variables) => { + // Invalidate all procurement queries for this tenant + queryClient.invalidateQueries({ + queryKey: procurementKeys.all, + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenantId); + }, + }); + + // If plan was generated successfully, cache it + if (data.success && data.plan) { + queryClient.setQueryData(procurementKeys.plan(variables.tenantId, data.plan.id), data.plan); + } + }, + ...options, + }); +}; + +/** + * Auto-generate procurement plan from forecast data (Orchestrator integration) + */ +export const useAutoGenerateProcurement = ( + options?: UseMutationOptions< + AutoGenerateProcurementResponse, + ApiError, + { tenantId: string; request: AutoGenerateProcurementRequest } + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + AutoGenerateProcurementResponse, + ApiError, + { tenantId: string; request: AutoGenerateProcurementRequest } + >({ + mutationFn: ({ tenantId, request }) => ProcurementService.autoGenerateProcurement(tenantId, request), + onSuccess: (data, variables) => { + // Invalidate all procurement queries + queryClient.invalidateQueries({ + queryKey: procurementKeys.all, + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenantId); + }, + }); + + // If plan was created successfully, cache it + if (data.success && data.plan_id) { + queryClient.invalidateQueries({ + queryKey: procurementKeys.currentPlan(variables.tenantId), + }); + } + }, + ...options, + }); +}; + +/** + * Update procurement plan status + */ +export const useUpdateProcurementPlanStatus = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (params) => ProcurementService.updateProcurementPlanStatus(params), + onSuccess: (data, variables) => { + // Update the specific plan in cache + queryClient.setQueryData(procurementKeys.plan(variables.tenant_id, variables.plan_id), data); + + // Invalidate plans list + queryClient.invalidateQueries({ + queryKey: procurementKeys.plans(), + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenant_id); + }, + }); + }, + ...options, + }); +}; + +/** + * Recalculate an existing procurement plan + */ +export const useRecalculateProcurementPlan = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ tenantId, planId }) => ProcurementService.recalculateProcurementPlan(tenantId, planId), + onSuccess: (data, variables) => { + if (data.plan) { + // Update the specific plan in cache + queryClient.setQueryData(procurementKeys.plan(variables.tenantId, variables.planId), data.plan); + } + + // Invalidate plans list and dashboard + queryClient.invalidateQueries({ + queryKey: procurementKeys.all, + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenantId); + }, + }); + }, + ...options, + }); +}; + +/** + * Approve a procurement plan + */ +export const useApproveProcurementPlan = ( + options?: UseMutationOptions< + ProcurementPlanResponse, + ApiError, + { tenantId: string; planId: string; approval_notes?: string } + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + ProcurementPlanResponse, + ApiError, + { tenantId: string; planId: string; approval_notes?: string } + >({ + mutationFn: ({ tenantId, planId, approval_notes }) => + ProcurementService.approveProcurementPlan(tenantId, planId, { approval_notes }), + onSuccess: (data, variables) => { + // Update the specific plan in cache + queryClient.setQueryData(procurementKeys.plan(variables.tenantId, variables.planId), data); + + // Invalidate plans list and dashboard + queryClient.invalidateQueries({ + queryKey: procurementKeys.all, + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenantId); + }, + }); + }, + ...options, + }); +}; + +/** + * Reject a procurement plan + */ +export const useRejectProcurementPlan = ( + options?: UseMutationOptions< + ProcurementPlanResponse, + ApiError, + { tenantId: string; planId: string; rejection_notes?: string } + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + ProcurementPlanResponse, + ApiError, + { tenantId: string; planId: string; rejection_notes?: string } + >({ + mutationFn: ({ tenantId, planId, rejection_notes }) => + ProcurementService.rejectProcurementPlan(tenantId, planId, { rejection_notes }), + onSuccess: (data, variables) => { + // Update the specific plan in cache + queryClient.setQueryData(procurementKeys.plan(variables.tenantId, variables.planId), data); + + // Invalidate plans list and dashboard + queryClient.invalidateQueries({ + queryKey: procurementKeys.all, + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenantId); + }, + }); + }, + ...options, + }); +}; + +// =================================================================== +// PURCHASE ORDER MUTATIONS +// =================================================================== + +/** + * Create purchase orders from procurement plan + */ +export const useCreatePurchaseOrdersFromPlan = ( + options?: UseMutationOptions +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ tenantId, planId, autoApprove = false }) => + ProcurementService.createPurchaseOrdersFromPlan(tenantId, planId, autoApprove), + onSuccess: (data, variables) => { + // Invalidate procurement plan to refresh requirements status + queryClient.invalidateQueries({ + queryKey: procurementKeys.plan(variables.tenantId, variables.planId), + }); + + // Invalidate plan requirements + queryClient.invalidateQueries({ + queryKey: procurementKeys.requirements(), + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.planId); + }, + }); + }, + ...options, + }); +}; + +/** + * Link a procurement requirement to a purchase order + */ +export const useLinkRequirementToPurchaseOrder = ( + options?: UseMutationOptions< + { success: boolean; message: string; requirement_id: string; purchase_order_id: string }, + ApiError, + { tenantId: string; requirementId: string; request: LinkRequirementToPORequest } + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + { success: boolean; message: string; requirement_id: string; purchase_order_id: string }, + ApiError, + { tenantId: string; requirementId: string; request: LinkRequirementToPORequest } + >({ + mutationFn: ({ tenantId, requirementId, request }) => + ProcurementService.linkRequirementToPurchaseOrder(tenantId, requirementId, request), + onSuccess: (data, variables) => { + // Invalidate procurement data to refresh requirements + queryClient.invalidateQueries({ + queryKey: procurementKeys.requirements(), + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenantId); + }, + }); + }, + ...options, + }); +}; + +/** + * Update delivery status for a requirement + */ +export const useUpdateRequirementDeliveryStatus = ( + options?: UseMutationOptions< + { success: boolean; message: string; requirement_id: string; delivery_status: string }, + ApiError, + { tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest } + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + { success: boolean; message: string; requirement_id: string; delivery_status: string }, + ApiError, + { tenantId: string; requirementId: string; request: UpdateDeliveryStatusRequest } + >({ + mutationFn: ({ tenantId, requirementId, request }) => + ProcurementService.updateRequirementDeliveryStatus(tenantId, requirementId, request), + onSuccess: (data, variables) => { + // Invalidate procurement data to refresh requirements + queryClient.invalidateQueries({ + queryKey: procurementKeys.requirements(), + predicate: (query) => { + return JSON.stringify(query.queryKey).includes(variables.tenantId); + }, + }); + }, + ...options, + }); +}; diff --git a/frontend/src/api/hooks/subscription.ts b/frontend/src/api/hooks/subscription.ts index 017d9ed8..894e65bd 100644 --- a/frontend/src/api/hooks/subscription.ts +++ b/frontend/src/api/hooks/subscription.ts @@ -72,7 +72,7 @@ export const useSubscription = () => { error: 'Failed to load subscription data' })); } - }, [tenantId, notifySubscriptionChanged]); + }, [tenantId]); // Removed notifySubscriptionChanged - it's now stable from context useEffect(() => { loadSubscriptionData(); diff --git a/frontend/src/api/hooks/tenant.ts b/frontend/src/api/hooks/tenant.ts index 0da44c0b..e1048631 100644 --- a/frontend/src/api/hooks/tenant.ts +++ b/frontend/src/api/hooks/tenant.ts @@ -319,16 +319,16 @@ export const useUpdateMemberRole = ( export const useRemoveTeamMember = ( options?: UseMutationOptions< - { success: boolean; message: string }, - ApiError, + { success: boolean; message: string }, + ApiError, { tenantId: string; memberUserId: string } > ) => { const queryClient = useQueryClient(); - + return useMutation< - { success: boolean; message: string }, - ApiError, + { success: boolean; message: string }, + ApiError, { tenantId: string; memberUserId: string } >({ mutationFn: ({ tenantId, memberUserId }) => tenantService.removeTeamMember(tenantId, memberUserId), @@ -338,4 +338,36 @@ export const useRemoveTeamMember = ( }, ...options, }); +}; + +/** + * Hook to transfer tenant ownership to another admin + * This is a critical operation that changes the tenant owner + */ +export const useTransferOwnership = ( + options?: UseMutationOptions< + TenantResponse, + ApiError, + { tenantId: string; newOwnerId: string } + > +) => { + const queryClient = useQueryClient(); + + return useMutation< + TenantResponse, + ApiError, + { tenantId: string; newOwnerId: string } + >({ + mutationFn: ({ tenantId, newOwnerId }) => tenantService.transferOwnership(tenantId, newOwnerId), + onSuccess: (data, { tenantId }) => { + // Invalidate all tenant-related queries since ownership changed + queryClient.invalidateQueries({ queryKey: tenantKeys.detail(tenantId) }); + queryClient.invalidateQueries({ queryKey: tenantKeys.members(tenantId) }); + queryClient.invalidateQueries({ queryKey: tenantKeys.userTenants('') }); + queryClient.invalidateQueries({ queryKey: tenantKeys.userOwnedTenants('') }); + // Invalidate access queries for all users since roles changed + queryClient.invalidateQueries({ queryKey: tenantKeys.access(tenantId, '') }); + }, + ...options, + }); }; \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 1ab80751..2330e89e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -27,7 +27,7 @@ export { posService } from './services/pos'; export { recipesService } from './services/recipes'; // NEW: Sprint 2 & 3 Services -export * as procurementService from './services/procurement'; +export { ProcurementService } from './services/procurement-service'; export * as orchestratorService from './services/orchestrator'; // Types - Auth @@ -289,31 +289,55 @@ export type { GetCustomersParams, UpdateOrderStatusParams, GetDemandRequirementsParams, - // Procurement types +} from './types/orders'; + +// Types - Procurement +export type { + // Enums ProcurementPlanType, ProcurementStrategy, RiskLevel, RequirementStatus, PlanStatus, - DeliveryStatus, + DeliveryStatus as ProcurementDeliveryStatus, + PriorityLevel as ProcurementPriorityLevel, + BusinessModel as ProcurementBusinessModel, + + // Requirement types ProcurementRequirementBase, ProcurementRequirementCreate, ProcurementRequirementUpdate, ProcurementRequirementResponse, + + // Plan types ProcurementPlanBase, ProcurementPlanCreate, ProcurementPlanUpdate, ProcurementPlanResponse, + ApprovalWorkflowEntry, + + // Dashboard & Analytics ProcurementSummary, ProcurementDashboardData, + + // Request/Response types GeneratePlanRequest, GeneratePlanResponse, + AutoGenerateProcurementRequest, + AutoGenerateProcurementResponse, + CreatePOsResult, + LinkRequirementToPORequest, + UpdateDeliveryStatusRequest, + ApprovalRequest, + RejectionRequest, PaginatedProcurementPlans, - ForecastRequest, + ForecastRequest as ProcurementForecastRequest, + + // Query params GetProcurementPlansParams, GetPlanRequirementsParams, UpdatePlanStatusParams, -} from './types/orders'; +} from './types/procurement'; // Types - Forecasting export type { @@ -609,26 +633,34 @@ export { useCreateCustomer, useUpdateCustomer, useInvalidateOrders, - // Procurement hooks + ordersKeys, +} from './hooks/orders'; + +// Hooks - Procurement +export { + // Queries + useProcurementDashboard, useProcurementPlans, useProcurementPlan, useProcurementPlanByDate, useCurrentProcurementPlan, - useProcurementDashboard, usePlanRequirements, useCriticalRequirements, - useProcurementHealth, + + // Mutations useGenerateProcurementPlan, + useAutoGenerateProcurement, useUpdateProcurementPlanStatus, - useTriggerDailyScheduler, useRecalculateProcurementPlan, useApproveProcurementPlan, useRejectProcurementPlan, useCreatePurchaseOrdersFromPlan, useLinkRequirementToPurchaseOrder, useUpdateRequirementDeliveryStatus, - ordersKeys, -} from './hooks/orders'; + + // Query keys + procurementKeys, +} from './hooks/procurement'; // Hooks - Forecasting export { diff --git a/frontend/src/api/services/orders.ts b/frontend/src/api/services/orders.ts index 19d328da..593b028d 100644 --- a/frontend/src/api/services/orders.ts +++ b/frontend/src/api/services/orders.ts @@ -28,24 +28,6 @@ import { GetCustomersParams, UpdateOrderStatusParams, GetDemandRequirementsParams, - // Procurement types - ProcurementPlanResponse, - ProcurementPlanCreate, - ProcurementPlanUpdate, - ProcurementRequirementResponse, - ProcurementRequirementUpdate, - ProcurementDashboardData, - GeneratePlanRequest, - GeneratePlanResponse, - PaginatedProcurementPlans, - GetProcurementPlansParams, - GetPlanRequirementsParams, - UpdatePlanStatusParams, - CreatePOsResult, - LinkRequirementToPORequest, - UpdateDeliveryStatusRequest, - ApprovalRequest, - RejectionRequest, } from '../types/orders'; export class OrdersService { @@ -209,209 +191,6 @@ export class OrdersService { return apiClient.get(`/tenants/${tenantId}/orders/operations/status`); } - // =================================================================== - // OPERATIONS: Procurement Planning - // Backend: services/orders/app/api/procurement_operations.py - // =================================================================== - - /** - * Get current procurement plan for today - * GET /tenants/{tenant_id}/orders/operations/procurement/plans/current - */ - static async getCurrentProcurementPlan(tenantId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/orders/operations/procurement/plans/current`); - } - - /** - * Get procurement plan by specific date - * GET /tenants/{tenant_id}/orders/operations/procurement/plans/date/{plan_date} - */ - static async getProcurementPlanByDate(tenantId: string, planDate: string): Promise { - return apiClient.get(`/tenants/${tenantId}/orders/operations/procurement/plans/date/${planDate}`); - } - - /** - * Get procurement plan by ID - * GET /tenants/{tenant_id}/orders/operations/procurement/plans/id/{plan_id} - */ - static async getProcurementPlanById(tenantId: string, planId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/orders/operations/procurement/plans/id/${planId}`); - } - - /** - * List procurement plans with filtering - * GET /tenants/{tenant_id}/orders/operations/procurement/plans/ - */ - static async getProcurementPlans(params: GetProcurementPlansParams): Promise { - const { tenant_id, status, start_date, end_date, limit = 50, offset = 0 } = params; - - const queryParams = new URLSearchParams({ - limit: limit.toString(), - offset: offset.toString(), - }); - - if (status) queryParams.append('status', status); - if (start_date) queryParams.append('start_date', start_date); - if (end_date) queryParams.append('end_date', end_date); - - return apiClient.get( - `/tenants/${tenant_id}/orders/operations/procurement/plans?${queryParams.toString()}` - ); - } - - /** - * Generate a new procurement plan - * POST /tenants/{tenant_id}/orders/operations/procurement/plans/generate - */ - static async generateProcurementPlan(tenantId: string, request: GeneratePlanRequest): Promise { - return apiClient.post(`/tenants/${tenantId}/orders/operations/procurement/plans/generate`, request); - } - - /** - * Update procurement plan status - * PUT /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/status - */ - static async updateProcurementPlanStatus(params: UpdatePlanStatusParams): Promise { - const { tenant_id, plan_id, status } = params; - - const queryParams = new URLSearchParams({ status }); - - return apiClient.put( - `/tenants/${tenant_id}/orders/operations/procurement/plans/${plan_id}/status?${queryParams.toString()}`, - {} - ); - } - - /** - * Get procurement dashboard data - * GET /tenants/{tenant_id}/orders/dashboard/procurement - */ - static async getProcurementDashboard(tenantId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/orders/dashboard/procurement`); - } - - /** - * Get requirements for a specific plan - * GET /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/requirements - */ - static async getPlanRequirements(params: GetPlanRequirementsParams): Promise { - const { tenant_id, plan_id, status, priority } = params; - - const queryParams = new URLSearchParams(); - if (status) queryParams.append('status', status); - if (priority) queryParams.append('priority', priority); - - const url = `/tenants/${tenant_id}/orders/operations/procurement/plans/${plan_id}/requirements${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; - - return apiClient.get(url); - } - - /** - * Get critical requirements across all plans - * GET /tenants/{tenant_id}/orders/operations/procurement/requirements/critical - */ - static async getCriticalRequirements(tenantId: string): Promise { - return apiClient.get(`/tenants/${tenantId}/orders/operations/procurement/requirements/critical`); - } - - /** - * Trigger daily scheduler manually - * POST /tenants/{tenant_id}/orders/operations/procurement/scheduler/trigger - */ - static async triggerDailyScheduler(tenantId: string): Promise<{ success: boolean; message: string; tenant_id: string }> { - return apiClient.post<{ success: boolean; message: string; tenant_id: string }>( - `/tenants/${tenantId}/orders/operations/procurement/scheduler/trigger`, - {} - ); - } - - /** - * Get procurement service health - * GET /tenants/{tenant_id}/orders/base/procurement/health - */ - static async getProcurementHealth(tenantId: string): Promise<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }> { - return apiClient.get<{ status: string; service: string; procurement_enabled: boolean; timestamp: string }>(`/tenants/${tenantId}/orders/base/procurement/health`); - } - - // =================================================================== - // OPERATIONS: Advanced Procurement Features - // Backend: services/orders/app/api/procurement_operations.py - // =================================================================== - - /** - * Recalculate an existing procurement plan - * POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/recalculate - */ - static async recalculateProcurementPlan(tenantId: string, planId: string): Promise { - return apiClient.post( - `/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/recalculate`, - {} - ); - } - - /** - * Approve a procurement plan with notes - * POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/approve - */ - static async approveProcurementPlan(tenantId: string, planId: string, request?: ApprovalRequest): Promise { - return apiClient.post( - `/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/approve`, - request || {} - ); - } - - /** - * Reject a procurement plan with notes - * POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/reject - */ - static async rejectProcurementPlan(tenantId: string, planId: string, request?: RejectionRequest): Promise { - return apiClient.post( - `/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/reject`, - request || {} - ); - } - - /** - * Create purchase orders automatically from procurement plan - * POST /tenants/{tenant_id}/orders/operations/procurement/plans/{plan_id}/create-purchase-orders - */ - static async createPurchaseOrdersFromPlan(tenantId: string, planId: string, autoApprove: boolean = false): Promise { - return apiClient.post( - `/tenants/${tenantId}/orders/operations/procurement/plans/${planId}/create-purchase-orders`, - { auto_approve: autoApprove } - ); - } - - /** - * Link a procurement requirement to a purchase order - * POST /tenants/{tenant_id}/orders/operations/procurement/requirements/{requirement_id}/link-purchase-order - */ - static async linkRequirementToPurchaseOrder( - tenantId: string, - requirementId: string, - request: LinkRequirementToPORequest - ): Promise<{ success: boolean; message: string; requirement_id: string; purchase_order_id: string }> { - return apiClient.post<{ success: boolean; message: string; requirement_id: string; purchase_order_id: string }>( - `/tenants/${tenantId}/orders/operations/procurement/requirements/${requirementId}/link-purchase-order`, - request - ); - } - - /** - * Update delivery status for a requirement - * PUT /tenants/{tenant_id}/orders/operations/procurement/requirements/{requirement_id}/delivery-status - */ - static async updateRequirementDeliveryStatus( - tenantId: string, - requirementId: string, - request: UpdateDeliveryStatusRequest - ): Promise<{ success: boolean; message: string; requirement_id: string; delivery_status: string }> { - return apiClient.put<{ success: boolean; message: string; requirement_id: string; delivery_status: string }>( - `/tenants/${tenantId}/orders/operations/procurement/requirements/${requirementId}/delivery-status`, - request - ); - } - } export default OrdersService; diff --git a/frontend/src/api/services/procurement-service.ts b/frontend/src/api/services/procurement-service.ts new file mode 100644 index 00000000..37dc4e33 --- /dev/null +++ b/frontend/src/api/services/procurement-service.ts @@ -0,0 +1,335 @@ +// ================================================================ +// frontend/src/api/services/procurement-service.ts +// ================================================================ +/** + * Procurement Service - Fully aligned with backend Procurement Service API + * + * Backend API: services/procurement/app/api/ + * - procurement_plans.py: Plan CRUD and generation + * - analytics.py: Analytics and dashboard + * - purchase_orders.py: PO creation from plans + * + * Base URL: /api/v1/tenants/{tenant_id}/procurement/* + * + * Last Updated: 2025-10-31 + * Status: ✅ Complete - 100% backend alignment + */ + +import { apiClient } from '../client/apiClient'; +import { + // Procurement Plan types + ProcurementPlanResponse, + ProcurementPlanCreate, + ProcurementPlanUpdate, + PaginatedProcurementPlans, + + // Procurement Requirement types + ProcurementRequirementResponse, + ProcurementRequirementUpdate, + + // Dashboard & Analytics types + ProcurementDashboardData, + ProcurementTrendsData, + + // Request/Response types + GeneratePlanRequest, + GeneratePlanResponse, + AutoGenerateProcurementRequest, + AutoGenerateProcurementResponse, + CreatePOsResult, + LinkRequirementToPORequest, + UpdateDeliveryStatusRequest, + ApprovalRequest, + RejectionRequest, + + // Query parameter types + GetProcurementPlansParams, + GetPlanRequirementsParams, + UpdatePlanStatusParams, +} from '../types/procurement'; + +/** + * Procurement Service + * All methods use the standalone Procurement Service backend API + */ +export class ProcurementService { + + // =================================================================== + // ANALYTICS & DASHBOARD + // Backend: services/procurement/app/api/analytics.py + // =================================================================== + + /** + * Get procurement analytics dashboard data + * GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement + */ + static async getProcurementAnalytics(tenantId: string): Promise { + return apiClient.get(`/tenants/${tenantId}/procurement/analytics/procurement`); + } + + /** + * Get procurement time-series trends for charts + * GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement/trends + */ + static async getProcurementTrends(tenantId: string, days: number = 7): Promise { + return apiClient.get(`/tenants/${tenantId}/procurement/analytics/procurement/trends?days=${days}`); + } + + // =================================================================== + // PROCUREMENT PLAN GENERATION + // Backend: services/procurement/app/api/procurement_plans.py + // =================================================================== + + /** + * Auto-generate procurement plan from forecast data (Orchestrator integration) + * POST /api/v1/tenants/{tenant_id}/procurement/auto-generate + * + * Called by Orchestrator Service to create procurement plans based on forecast data + */ + static async autoGenerateProcurement( + tenantId: string, + request: AutoGenerateProcurementRequest + ): Promise { + return apiClient.post( + `/tenants/${tenantId}/procurement/auto-generate`, + request + ); + } + + /** + * Generate a new procurement plan (manual/UI-driven) + * POST /api/v1/tenants/{tenant_id}/procurement/plans/generate + */ + static async generateProcurementPlan( + tenantId: string, + request: GeneratePlanRequest + ): Promise { + return apiClient.post( + `/tenants/${tenantId}/procurement/plans/generate`, + request + ); + } + + // =================================================================== + // PROCUREMENT PLAN CRUD + // Backend: services/procurement/app/api/procurement_plans.py + // =================================================================== + + /** + * Get the current day's procurement plan + * GET /api/v1/tenants/{tenant_id}/procurement/plans/current + */ + static async getCurrentProcurementPlan(tenantId: string): Promise { + return apiClient.get( + `/tenants/${tenantId}/procurement/plans/current` + ); + } + + /** + * Get procurement plan by ID + * GET /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id} + */ + static async getProcurementPlanById( + tenantId: string, + planId: string + ): Promise { + return apiClient.get( + `/tenants/${tenantId}/procurement/plans/${planId}` + ); + } + + /** + * Get procurement plan for a specific date + * GET /api/v1/tenants/{tenant_id}/procurement/plans/date/{plan_date} + */ + static async getProcurementPlanByDate( + tenantId: string, + planDate: string + ): Promise { + return apiClient.get( + `/tenants/${tenantId}/procurement/plans/date/${planDate}` + ); + } + + /** + * List all procurement plans for tenant with pagination and filtering + * GET /api/v1/tenants/{tenant_id}/procurement/plans + */ + static async getProcurementPlans(params: GetProcurementPlansParams): Promise { + const { tenant_id, status, start_date, end_date, limit = 50, offset = 0 } = params; + + const queryParams = new URLSearchParams({ + limit: limit.toString(), + offset: offset.toString(), + }); + + if (status) queryParams.append('status', status); + if (start_date) queryParams.append('start_date', start_date); + if (end_date) queryParams.append('end_date', end_date); + + return apiClient.get( + `/tenants/${tenant_id}/procurement/plans?${queryParams.toString()}` + ); + } + + /** + * Update procurement plan status + * PATCH /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/status + */ + static async updateProcurementPlanStatus(params: UpdatePlanStatusParams): Promise { + const { tenant_id, plan_id, status, notes } = params; + + const queryParams = new URLSearchParams({ status }); + if (notes) queryParams.append('notes', notes); + + return apiClient.patch( + `/tenants/${tenant_id}/procurement/plans/${plan_id}/status?${queryParams.toString()}`, + {} + ); + } + + // =================================================================== + // PROCUREMENT REQUIREMENTS + // Backend: services/procurement/app/api/procurement_plans.py + // =================================================================== + + /** + * Get all requirements for a procurement plan + * GET /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/requirements + */ + static async getPlanRequirements(params: GetPlanRequirementsParams): Promise { + const { tenant_id, plan_id, status, priority } = params; + + const queryParams = new URLSearchParams(); + if (status) queryParams.append('status', status); + if (priority) queryParams.append('priority', priority); + + const url = `/tenants/${tenant_id}/procurement/plans/${plan_id}/requirements${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + return apiClient.get(url); + } + + /** + * Get critical requirements across all plans + * GET /api/v1/tenants/{tenant_id}/procurement/requirements/critical + */ + static async getCriticalRequirements(tenantId: string): Promise { + return apiClient.get( + `/tenants/${tenantId}/procurement/requirements/critical` + ); + } + + /** + * Link a procurement requirement to a purchase order + * POST /api/v1/tenants/{tenant_id}/procurement/requirements/{requirement_id}/link-purchase-order + */ + static async linkRequirementToPurchaseOrder( + tenantId: string, + requirementId: string, + request: LinkRequirementToPORequest + ): Promise<{ success: boolean; message: string; requirement_id: string; purchase_order_id: string }> { + return apiClient.post<{ + success: boolean; + message: string; + requirement_id: string; + purchase_order_id: string; + }>( + `/tenants/${tenantId}/procurement/requirements/${requirementId}/link-purchase-order`, + request + ); + } + + /** + * Update delivery status for a requirement + * PUT /api/v1/tenants/{tenant_id}/procurement/requirements/{requirement_id}/delivery-status + */ + static async updateRequirementDeliveryStatus( + tenantId: string, + requirementId: string, + request: UpdateDeliveryStatusRequest + ): Promise<{ success: boolean; message: string; requirement_id: string; delivery_status: string }> { + return apiClient.put<{ + success: boolean; + message: string; + requirement_id: string; + delivery_status: string; + }>( + `/tenants/${tenantId}/procurement/requirements/${requirementId}/delivery-status`, + request + ); + } + + // =================================================================== + // ADVANCED PROCUREMENT OPERATIONS + // Backend: services/procurement/app/api/procurement_plans.py + // =================================================================== + + /** + * Recalculate an existing procurement plan + * POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/recalculate + */ + static async recalculateProcurementPlan( + tenantId: string, + planId: string + ): Promise { + return apiClient.post( + `/tenants/${tenantId}/procurement/plans/${planId}/recalculate`, + {} + ); + } + + /** + * Approve a procurement plan with optional notes + * POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/approve + */ + static async approveProcurementPlan( + tenantId: string, + planId: string, + request?: ApprovalRequest + ): Promise { + return apiClient.post( + `/tenants/${tenantId}/procurement/plans/${planId}/approve`, + request || {} + ); + } + + /** + * Reject a procurement plan with optional notes + * POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/reject + */ + static async rejectProcurementPlan( + tenantId: string, + planId: string, + request?: RejectionRequest + ): Promise { + return apiClient.post( + `/tenants/${tenantId}/procurement/plans/${planId}/reject`, + request || {} + ); + } + + // =================================================================== + // PURCHASE ORDERS + // Backend: services/procurement/app/api/purchase_orders.py + // =================================================================== + + /** + * Create purchase orders from procurement plan requirements + * Groups requirements by supplier and creates POs + * POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/create-purchase-orders + */ + static async createPurchaseOrdersFromPlan( + tenantId: string, + planId: string, + autoApprove: boolean = false + ): Promise { + return apiClient.post( + `/tenants/${tenantId}/procurement/plans/${planId}/create-purchase-orders`, + { auto_approve: autoApprove } + ); + } +} + +export default ProcurementService; diff --git a/frontend/src/api/services/procurement.ts b/frontend/src/api/services/procurement.ts deleted file mode 100644 index bf0e776f..00000000 --- a/frontend/src/api/services/procurement.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** - * Procurement Service API Client - * Handles procurement planning and purchase order management - * - * NEW in Sprint 3: Procurement Service now owns all procurement operations - * Previously these were split between Orders Service and Suppliers Service - */ - -import { apiClient } from '../client'; - -// ============================================================================ -// PROCUREMENT PLAN TYPES -// ============================================================================ - -export interface ProcurementRequirement { - id: string; - ingredient_id: string; - ingredient_name?: string; - ingredient_sku?: string; - required_quantity: number; - current_stock: number; - quantity_to_order: number; - unit_of_measure: string; - estimated_cost: string; // Decimal as string - priority: 'urgent' | 'high' | 'normal' | 'low'; - reason: string; - supplier_id?: string; - supplier_name?: string; - expected_delivery_date?: string; - // NEW: Local production support - is_locally_produced?: boolean; - recipe_id?: string; - parent_requirement_id?: string; - bom_explosion_level?: number; -} - -export interface ProcurementPlanSummary { - id: string; - plan_date: string; - status: 'DRAFT' | 'PENDING_APPROVAL' | 'APPROVED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'; - total_requirements: number; - total_estimated_cost: string; // Decimal as string - planning_horizon_days: number; - auto_generated: boolean; - // NEW: Orchestrator integration - forecast_id?: string; - production_schedule_id?: string; - created_at: string; - created_by?: string; -} - -export interface ProcurementPlanDetail extends ProcurementPlanSummary { - requirements: ProcurementRequirement[]; - notes?: string; - approved_by?: string; - approved_at?: string; - updated_at: string; -} - -// ============================================================================ -// AUTO-GENERATE PROCUREMENT TYPES (Orchestrator Integration) -// ============================================================================ - -export interface AutoGenerateProcurementRequest { - forecast_data: Record; // From Forecasting Service - production_schedule_id?: string; - target_date?: string; // YYYY-MM-DD - planning_horizon_days?: number; // Default: 14 - safety_stock_percentage?: number; // Default: 20.00 - auto_create_pos?: boolean; // Default: true - auto_approve_pos?: boolean; // Default: false -} - -export interface AutoGenerateProcurementResponse { - success: boolean; - plan?: ProcurementPlanDetail; - purchase_orders_created?: number; - purchase_orders_auto_approved?: number; - purchase_orders_pending_approval?: number; - recipe_explosion_applied?: boolean; - recipe_explosion_metadata?: { - total_requirements_before: number; - total_requirements_after: number; - explosion_levels: number; - locally_produced_ingredients: number; - }; - warnings?: string[]; - errors?: string[]; - execution_time_ms?: number; -} - -// ============================================================================ -// PROCUREMENT PLAN API FUNCTIONS -// ============================================================================ - -/** - * Get list of procurement plans with optional filters - */ -export async function listProcurementPlans( - tenantId: string, - params?: { - status?: ProcurementPlanSummary['status']; - date_from?: string; - date_to?: string; - limit?: number; - offset?: number; - } -): Promise { - return apiClient.get( - `/tenants/${tenantId}/procurement/plans`, - { params } - ); -} - -/** - * Get a single procurement plan by ID with full details - */ -export async function getProcurementPlan( - tenantId: string, - planId: string -): Promise { - return apiClient.get( - `/tenants/${tenantId}/procurement/plans/${planId}` - ); -} - -/** - * Create a new procurement plan (manual) - */ -export async function createProcurementPlan( - tenantId: string, - data: { - plan_date: string; - planning_horizon_days?: number; - include_safety_stock?: boolean; - safety_stock_percentage?: number; - notes?: string; - } -): Promise { - return apiClient.post( - `/tenants/${tenantId}/procurement/plans`, - data - ); -} - -/** - * Update procurement plan - */ -export async function updateProcurementPlan( - tenantId: string, - planId: string, - data: { - status?: ProcurementPlanSummary['status']; - notes?: string; - } -): Promise { - return apiClient.put( - `/tenants/${tenantId}/procurement/plans/${planId}`, - data - ); -} - -/** - * Delete procurement plan - */ -export async function deleteProcurementPlan( - tenantId: string, - planId: string -): Promise<{ message: string }> { - return apiClient.delete<{ message: string }>( - `/tenants/${tenantId}/procurement/plans/${planId}` - ); -} - -/** - * Approve procurement plan - */ -export async function approveProcurementPlan( - tenantId: string, - planId: string, - notes?: string -): Promise { - return apiClient.post( - `/tenants/${tenantId}/procurement/plans/${planId}/approve`, - { notes } - ); -} - -// ============================================================================ -// AUTO-GENERATE PROCUREMENT (ORCHESTRATOR INTEGRATION) -// ============================================================================ - -/** - * Auto-generate procurement plan from forecast data - * This is the main entry point for orchestrated procurement planning - * - * NEW in Sprint 3: Called by Orchestrator Service to create procurement plans - * based on forecast data and production schedules - * - * Features: - * - Receives forecast data from Forecasting Service (via Orchestrator) - * - Calculates procurement requirements using smart calculator - * - Applies Recipe Explosion for locally-produced ingredients - * - Optionally creates purchase orders - * - Optionally auto-approves qualifying POs - */ -export async function autoGenerateProcurement( - tenantId: string, - request: AutoGenerateProcurementRequest -): Promise { - return apiClient.post( - `/tenants/${tenantId}/procurement/auto-generate`, - request - ); -} - -/** - * Test auto-generate with sample forecast data (for development/testing) - */ -export async function testAutoGenerateProcurement( - tenantId: string, - targetDate?: string -): Promise { - return apiClient.post( - `/tenants/${tenantId}/procurement/auto-generate/test`, - { target_date: targetDate } - ); -} - -// ============================================================================ -// PROCUREMENT REQUIREMENTS API FUNCTIONS -// ============================================================================ - -/** - * Add requirement to procurement plan - */ -export async function addProcurementRequirement( - tenantId: string, - planId: string, - requirement: { - ingredient_id: string; - required_quantity: number; - quantity_to_order: number; - priority: ProcurementRequirement['priority']; - reason: string; - supplier_id?: string; - expected_delivery_date?: string; - } -): Promise { - return apiClient.post( - `/tenants/${tenantId}/procurement/plans/${planId}/requirements`, - requirement - ); -} - -/** - * Update procurement requirement - */ -export async function updateProcurementRequirement( - tenantId: string, - planId: string, - requirementId: string, - data: { - quantity_to_order?: number; - priority?: ProcurementRequirement['priority']; - supplier_id?: string; - expected_delivery_date?: string; - } -): Promise { - return apiClient.put( - `/tenants/${tenantId}/procurement/plans/${planId}/requirements/${requirementId}`, - data - ); -} - -/** - * Delete procurement requirement - */ -export async function deleteProcurementRequirement( - tenantId: string, - planId: string, - requirementId: string -): Promise<{ message: string }> { - return apiClient.delete<{ message: string }>( - `/tenants/${tenantId}/procurement/plans/${planId}/requirements/${requirementId}` - ); -} - -// ============================================================================ -// PURCHASE ORDERS FROM PLAN -// ============================================================================ - -/** - * Create purchase orders from procurement plan - * Groups requirements by supplier and creates POs - */ -export async function createPurchaseOrdersFromPlan( - tenantId: string, - planId: string, - options?: { - auto_approve?: boolean; - group_by_supplier?: boolean; - delivery_date?: string; - } -): Promise<{ - success: boolean; - purchase_orders_created: number; - purchase_orders_auto_approved?: number; - purchase_orders_pending_approval?: number; - purchase_order_ids: string[]; - message?: string; -}> { - return apiClient.post( - `/tenants/${tenantId}/procurement/plans/${planId}/create-purchase-orders`, - options - ); -} diff --git a/frontend/src/api/services/tenant.ts b/frontend/src/api/services/tenant.ts index d06ac088..c5e17f5c 100644 --- a/frontend/src/api/services/tenant.ts +++ b/frontend/src/api/services/tenant.ts @@ -168,6 +168,21 @@ export class TenantService { return apiClient.delete<{ success: boolean; message: string }>(`${this.baseUrl}/${tenantId}/members/${memberUserId}`); } + /** + * Transfer tenant ownership to another admin + * Backend: services/tenant/app/api/tenant_members.py - transfer_ownership endpoint + * + * @param tenantId - The tenant ID + * @param newOwnerId - The user ID of the new owner (must be an existing admin) + * @returns Updated tenant with new owner + */ + async transferOwnership(tenantId: string, newOwnerId: string): Promise { + return apiClient.post( + `${this.baseUrl}/${tenantId}/transfer-ownership`, + { new_owner_id: newOwnerId } + ); + } + // =================================================================== // OPERATIONS: Statistics & Admin // Backend: services/tenant/app/api/tenant_operations.py diff --git a/frontend/src/api/types/orders.ts b/frontend/src/api/types/orders.ts index 0f9229f8..74069a8d 100644 --- a/frontend/src/api/types/orders.ts +++ b/frontend/src/api/types/orders.ts @@ -365,369 +365,3 @@ export interface GetDemandRequirementsParams { tenant_id: string; target_date: string; } - -// ================================================================ -// PROCUREMENT ENUMS -// ================================================================ - -/** - * Procurement plan types - * Backend: ProcurementPlanType enum in models/enums.py (lines 104-108) - */ -export enum ProcurementPlanType { - REGULAR = 'regular', - EMERGENCY = 'emergency', - SEASONAL = 'seasonal' -} - -/** - * Procurement strategies - * Backend: ProcurementStrategy enum in models/enums.py (lines 111-115) - */ -export enum ProcurementStrategy { - JUST_IN_TIME = 'just_in_time', - BULK = 'bulk', - MIXED = 'mixed' -} - -/** - * Risk level classifications - * Backend: RiskLevel enum in models/enums.py (lines 118-123) - */ -export enum RiskLevel { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - CRITICAL = 'critical' -} - -/** - * Procurement requirement status - * Backend: RequirementStatus enum in models/enums.py (lines 126-133) - */ -export enum RequirementStatus { - PENDING = 'pending', - APPROVED = 'approved', - ORDERED = 'ordered', - PARTIALLY_RECEIVED = 'partially_received', - RECEIVED = 'received', - CANCELLED = 'cancelled' -} - -/** - * Procurement plan status - * Backend: PlanStatus enum in models/enums.py (lines 136-143) - */ -export enum PlanStatus { - DRAFT = 'draft', - PENDING_APPROVAL = 'pending_approval', - APPROVED = 'approved', - IN_EXECUTION = 'in_execution', - COMPLETED = 'completed', - CANCELLED = 'cancelled' -} - -/** - * Delivery status for procurement - * Backend: DeliveryStatus enum in models/enums.py (lines 146-151) - */ -export enum DeliveryStatus { - PENDING = 'pending', - IN_TRANSIT = 'in_transit', - DELIVERED = 'delivered', - DELAYED = 'delayed', - CANCELLED = 'cancelled' -} - -// ================================================================ -// PROCUREMENT TYPES -// ================================================================ - -// Procurement Requirement Types -export interface ProcurementRequirementBase { - product_id: string; - product_name: string; - product_sku?: string; - product_category?: string; - product_type: string; - required_quantity: number; - unit_of_measure: string; - safety_stock_quantity: number; - total_quantity_needed: number; - current_stock_level: number; - reserved_stock: number; - available_stock: number; - net_requirement: number; - order_demand: number; - production_demand: number; - forecast_demand: number; - buffer_demand: number; - required_by_date: string; - lead_time_buffer_days: number; - suggested_order_date: string; - latest_order_date: string; - priority: PriorityLevel; - risk_level: RiskLevel; - preferred_supplier_id?: string; - backup_supplier_id?: string; - supplier_name?: string; - supplier_lead_time_days?: number; - minimum_order_quantity?: number; - estimated_unit_cost?: number; - estimated_total_cost?: number; - last_purchase_cost?: number; -} - -export interface ProcurementRequirementCreate extends ProcurementRequirementBase { - special_requirements?: string; - storage_requirements?: string; - shelf_life_days?: number; - quality_specifications?: Record; - procurement_notes?: string; -} - -export interface ProcurementRequirementUpdate { - status?: RequirementStatus; - priority?: PriorityLevel; - approved_quantity?: number; - approved_cost?: number; - purchase_order_id?: string; - purchase_order_number?: string; - ordered_quantity?: number; - expected_delivery_date?: string; - actual_delivery_date?: string; - received_quantity?: number; - delivery_status?: DeliveryStatus; - procurement_notes?: string; -} - -export interface ProcurementRequirementResponse extends ProcurementRequirementBase { - id: string; - plan_id: string; - requirement_number: string; - status: RequirementStatus; - created_at: string; - updated_at: string; - purchase_order_id?: string; - purchase_order_number?: string; - ordered_quantity: number; - ordered_at?: string; - expected_delivery_date?: string; - actual_delivery_date?: string; - received_quantity: number; - delivery_status: DeliveryStatus; - fulfillment_rate?: number; - on_time_delivery?: boolean; - quality_rating?: number; - approved_quantity?: number; - approved_cost?: number; - approved_at?: string; - approved_by?: string; - special_requirements?: string; - storage_requirements?: string; - shelf_life_days?: number; - quality_specifications?: Record; - procurement_notes?: string; - - // Smart procurement calculation metadata - calculation_method?: string; - ai_suggested_quantity?: number; - adjusted_quantity?: number; - adjustment_reason?: string; - price_tier_applied?: Record; - supplier_minimum_applied?: boolean; - storage_limit_applied?: boolean; - reorder_rule_applied?: boolean; -} - -// Procurement Plan Types -export interface ProcurementPlanBase { - plan_date: string; - plan_period_start: string; - plan_period_end: string; - planning_horizon_days: number; - plan_type: ProcurementPlanType; - priority: PriorityLevel; - business_model?: BusinessModel; - procurement_strategy: ProcurementStrategy; - safety_stock_buffer: number; - supply_risk_level: RiskLevel; - demand_forecast_confidence?: number; - seasonality_adjustment: number; - special_requirements?: string; -} - -export interface ProcurementPlanCreate extends ProcurementPlanBase { - tenant_id: string; - requirements?: ProcurementRequirementCreate[]; -} - -export interface ProcurementPlanUpdate { - status?: PlanStatus; - priority?: PriorityLevel; - approved_at?: string; - approved_by?: string; - execution_started_at?: string; - execution_completed_at?: string; - special_requirements?: string; - seasonal_adjustments?: Record; -} - -export interface ApprovalWorkflowEntry { - timestamp: string; - from_status: string; - to_status: string; - user_id?: string; - notes?: string; -} - -export interface ProcurementPlanResponse extends ProcurementPlanBase { - id: string; - tenant_id: string; - plan_number: string; - status: PlanStatus; - total_requirements: number; - total_estimated_cost: number; - total_approved_cost: number; - cost_variance: number; - total_demand_orders: number; - total_demand_quantity: number; - total_production_requirements: number; - primary_suppliers_count: number; - backup_suppliers_count: number; - supplier_diversification_score?: number; - approved_at?: string; - approved_by?: string; - execution_started_at?: string; - execution_completed_at?: string; - fulfillment_rate?: number; - on_time_delivery_rate?: number; - cost_accuracy?: number; - quality_score?: number; - approval_workflow?: ApprovalWorkflowEntry[]; - created_at: string; - updated_at: string; - created_by?: string; - updated_by?: string; - requirements: ProcurementRequirementResponse[]; -} - -// Summary and Dashboard Types -export interface ProcurementSummary { - total_plans: number; - active_plans: number; - total_requirements: number; - pending_requirements: number; - critical_requirements: number; - total_estimated_cost: number; - total_approved_cost: number; - cost_variance: number; - average_fulfillment_rate?: number; - average_on_time_delivery?: number; - top_suppliers: Record[]; - critical_items: Record[]; -} - -export interface ProcurementDashboardData { - current_plan?: ProcurementPlanResponse; - summary: ProcurementSummary; - upcoming_deliveries: Record[]; - overdue_requirements: Record[]; - low_stock_alerts: Record[]; - performance_metrics: Record; -} - -// Request and Response Types -export interface GeneratePlanRequest { - plan_date?: string; - force_regenerate: boolean; - planning_horizon_days: number; - include_safety_stock: boolean; - safety_stock_percentage: number; -} - -export interface GeneratePlanResponse { - success: boolean; - message: string; - plan?: ProcurementPlanResponse; - warnings: string[]; - errors: string[]; -} - -// New Feature Types -export interface CreatePOsResult { - success: boolean; - created_pos: { - po_id: string; - po_number: string; - supplier_id: string; - items_count: number; - total_amount: number; - }[]; - failed_pos: { - supplier_id: string; - error: string; - }[]; - total_created: number; - total_failed: number; -} - -export interface LinkRequirementToPORequest { - purchase_order_id: string; - purchase_order_number: string; - ordered_quantity: number; - expected_delivery_date?: string; -} - -export interface UpdateDeliveryStatusRequest { - delivery_status: string; - received_quantity?: number; - actual_delivery_date?: string; - quality_rating?: number; -} - -export interface ApprovalRequest { - approval_notes?: string; -} - -export interface RejectionRequest { - rejection_notes?: string; -} - -export interface PaginatedProcurementPlans { - plans: ProcurementPlanResponse[]; - total: number; - page: number; - limit: number; - has_more: boolean; -} - -export interface ForecastRequest { - target_date: string; - horizon_days: number; - include_confidence_intervals: boolean; - product_ids?: string[]; -} - -// Query Parameter Types for Procurement -export interface GetProcurementPlansParams { - tenant_id: string; - status?: string; - start_date?: string; - end_date?: string; - limit?: number; - offset?: number; -} - -export interface GetPlanRequirementsParams { - tenant_id: string; - plan_id: string; - status?: string; - priority?: string; -} - -export interface UpdatePlanStatusParams { - tenant_id: string; - plan_id: string; - status: PlanStatus; -} \ No newline at end of file diff --git a/frontend/src/api/types/performance.ts b/frontend/src/api/types/performance.ts new file mode 100644 index 00000000..487e3595 --- /dev/null +++ b/frontend/src/api/types/performance.ts @@ -0,0 +1,192 @@ +/** + * Performance Analytics Types + * Comprehensive types for performance monitoring across all departments + */ + +// ============================================================================ +// Overview Metrics +// ============================================================================ + +export interface PerformanceOverview { + overall_efficiency: number; + average_production_time: number; + quality_score: number; + employee_productivity: number; + customer_satisfaction: number; + resource_utilization: number; +} + +// ============================================================================ +// Department Performance +// ============================================================================ + +export interface DepartmentPerformance { + department_id: string; + department_name: string; + efficiency: number; + trend: 'up' | 'down' | 'stable'; + metrics: DepartmentMetrics; +} + +export interface DepartmentMetrics { + primary_metric: MetricValue; + secondary_metric: MetricValue; + tertiary_metric: MetricValue; +} + +export interface MetricValue { + label: string; + value: number; + unit: string; + trend?: number; +} + +// Production Department +export interface ProductionPerformance { + efficiency: number; + average_batch_time: number; + quality_rate: number; + waste_percentage: number; + capacity_utilization: number; + equipment_efficiency: number; + on_time_completion_rate: number; + yield_rate: number; +} + +// Inventory Department +export interface InventoryPerformance { + stock_accuracy: number; + turnover_rate: number; + waste_rate: number; + low_stock_count: number; + compliance_rate: number; + expiring_items_count: number; + stock_value: number; +} + +// Sales Department +export interface SalesPerformance { + total_revenue: number; + total_transactions: number; + average_transaction_value: number; + growth_rate: number; + channel_performance: ChannelPerformance[]; + top_products: ProductPerformance[]; +} + +export interface ChannelPerformance { + channel: string; + revenue: number; + transactions: number; + percentage: number; +} + +export interface ProductPerformance { + product_id: string; + product_name: string; + sales: number; + revenue: number; +} + +// Procurement/Administration Department +export interface ProcurementPerformance { + fulfillment_rate: number; + on_time_delivery_rate: number; + cost_accuracy: number; + supplier_performance_score: number; + active_plans: number; + critical_requirements: number; +} + +// ============================================================================ +// KPI Tracking +// ============================================================================ + +export interface KPIMetric { + id: string; + name: string; + current_value: number; + target_value: number; + previous_value: number; + unit: string; + trend: 'up' | 'down' | 'stable'; + status: 'good' | 'warning' | 'critical'; +} + +// ============================================================================ +// Performance Alerts +// ============================================================================ + +export interface PerformanceAlert { + id: string; + type: 'warning' | 'critical' | 'info'; + department: string; + message: string; + timestamp: string; + metric_affected: string; + current_value?: number; + threshold_value?: number; +} + +// ============================================================================ +// Time-Series Data +// ============================================================================ + +export interface TimeSeriesData { + timestamp: string; + value: number; + label?: string; +} + +export interface HourlyProductivity { + hour: string; + efficiency: number; + production_count: number; + sales_count: number; +} + +// ============================================================================ +// Aggregated Dashboard Data +// ============================================================================ + +export interface PerformanceDashboard { + overview: PerformanceOverview; + departments: DepartmentPerformance[]; + kpis: KPIMetric[]; + alerts: PerformanceAlert[]; + hourly_data: HourlyProductivity[]; + last_updated: string; +} + +// ============================================================================ +// Filter and Query Parameters +// ============================================================================ + +export type TimePeriod = 'day' | 'week' | 'month' | 'quarter' | 'year'; +export type MetricType = 'efficiency' | 'productivity' | 'quality' | 'satisfaction'; + +export interface PerformanceFilters { + period: TimePeriod; + metric_type?: MetricType; + start_date?: string; + end_date?: string; + departments?: string[]; +} + +// ============================================================================ +// Trend Analysis +// ============================================================================ + +export interface TrendData { + date: string; + value: number; + comparison_value?: number; +} + +export interface PerformanceTrend { + metric_name: string; + current_period: TrendData[]; + previous_period: TrendData[]; + change_percentage: number; + trend_direction: 'up' | 'down' | 'stable'; +} diff --git a/frontend/src/api/types/procurement.ts b/frontend/src/api/types/procurement.ts new file mode 100644 index 00000000..333dfa9f --- /dev/null +++ b/frontend/src/api/types/procurement.ts @@ -0,0 +1,634 @@ +/** + * TypeScript types for Procurement Service + * Mirrored from backend schemas: services/procurement/app/schemas/procurement_schemas.py + * Backend enums: services/shared/app/models/enums.py + * Backend API: services/procurement/app/api/ + * + * Coverage: + * - Procurement Plans (MRP-style procurement planning) + * - Procurement Requirements (demand-driven purchasing) + * - Purchase Orders creation from plans + * - Analytics & Dashboard + * - Auto-generation (Orchestrator integration) + */ + +// ================================================================ +// ENUMS +// ================================================================ + +/** + * Procurement plan types + * Backend: ProcurementPlanType enum in models/enums.py + */ +export enum ProcurementPlanType { + REGULAR = 'regular', + EMERGENCY = 'emergency', + SEASONAL = 'seasonal' +} + +/** + * Procurement strategies + * Backend: ProcurementStrategy enum in models/enums.py + */ +export enum ProcurementStrategy { + JUST_IN_TIME = 'just_in_time', + BULK = 'bulk', + MIXED = 'mixed' +} + +/** + * Risk level classifications + * Backend: RiskLevel enum in models/enums.py + */ +export enum RiskLevel { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical' +} + +/** + * Procurement requirement status + * Backend: RequirementStatus enum in models/enums.py + */ +export enum RequirementStatus { + PENDING = 'pending', + APPROVED = 'approved', + ORDERED = 'ordered', + PARTIALLY_RECEIVED = 'partially_received', + RECEIVED = 'received', + CANCELLED = 'cancelled' +} + +/** + * Procurement plan status + * Backend: PlanStatus enum in models/enums.py + */ +export enum PlanStatus { + DRAFT = 'draft', + PENDING_APPROVAL = 'pending_approval', + APPROVED = 'approved', + IN_EXECUTION = 'in_execution', + COMPLETED = 'completed', + CANCELLED = 'cancelled' +} + +/** + * Delivery status for procurement + * Backend: DeliveryStatus enum in models/enums.py + */ +export enum DeliveryStatus { + PENDING = 'pending', + IN_TRANSIT = 'in_transit', + DELIVERED = 'delivered', + DELAYED = 'delayed', + CANCELLED = 'cancelled' +} + +/** + * Priority level (shared enum) + * Backend: PriorityLevel enum in models/enums.py + */ +export enum PriorityLevel { + HIGH = 'high', + NORMAL = 'normal', + LOW = 'low' +} + +/** + * Business model (shared enum) + * Backend: BusinessModel enum in models/enums.py + */ +export enum BusinessModel { + INDIVIDUAL_BAKERY = 'individual_bakery', + CENTRAL_BAKERY = 'central_bakery' +} + +// ================================================================ +// PROCUREMENT REQUIREMENT TYPES +// ================================================================ + +/** + * Base procurement requirement + * Backend: ProcurementRequirementBase schema + */ +export interface ProcurementRequirementBase { + product_id: string; + product_name: string; + product_sku?: string; + product_category?: string; + product_type: string; + required_quantity: number; + unit_of_measure: string; + safety_stock_quantity: number; + total_quantity_needed: number; + current_stock_level: number; + reserved_stock: number; + available_stock: number; + net_requirement: number; + order_demand: number; + production_demand: number; + forecast_demand: number; + buffer_demand: number; + required_by_date: string; + lead_time_buffer_days: number; + suggested_order_date: string; + latest_order_date: string; + priority: PriorityLevel; + risk_level: RiskLevel; + preferred_supplier_id?: string; + backup_supplier_id?: string; + supplier_name?: string; + supplier_lead_time_days?: number; + minimum_order_quantity?: number; + estimated_unit_cost?: number; + estimated_total_cost?: number; + last_purchase_cost?: number; +} + +/** + * Create procurement requirement request + * Backend: ProcurementRequirementCreate schema + */ +export interface ProcurementRequirementCreate extends ProcurementRequirementBase { + special_requirements?: string; + storage_requirements?: string; + shelf_life_days?: number; + quality_specifications?: Record; + procurement_notes?: string; + + // Smart procurement calculation metadata + calculation_method?: string; + ai_suggested_quantity?: number; + adjusted_quantity?: number; + adjustment_reason?: string; + price_tier_applied?: Record; + supplier_minimum_applied?: boolean; + storage_limit_applied?: boolean; + reorder_rule_applied?: boolean; + + // Local production support fields + is_locally_produced?: boolean; + recipe_id?: string; + parent_requirement_id?: string; + bom_explosion_level?: number; +} + +/** + * Update procurement requirement request + * Backend: ProcurementRequirementUpdate schema + */ +export interface ProcurementRequirementUpdate { + status?: RequirementStatus; + priority?: PriorityLevel; + approved_quantity?: number; + approved_cost?: number; + purchase_order_id?: string; + purchase_order_number?: string; + ordered_quantity?: number; + expected_delivery_date?: string; + actual_delivery_date?: string; + received_quantity?: number; + delivery_status?: DeliveryStatus; + procurement_notes?: string; +} + +/** + * Procurement requirement response + * Backend: ProcurementRequirementResponse schema + */ +export interface ProcurementRequirementResponse extends ProcurementRequirementBase { + id: string; + plan_id: string; + requirement_number: string; + status: RequirementStatus; + created_at: string; + updated_at: string; + purchase_order_id?: string; + purchase_order_number?: string; + ordered_quantity: number; + ordered_at?: string; + expected_delivery_date?: string; + actual_delivery_date?: string; + received_quantity: number; + delivery_status: DeliveryStatus; + fulfillment_rate?: number; + on_time_delivery?: boolean; + quality_rating?: number; + approved_quantity?: number; + approved_cost?: number; + approved_at?: string; + approved_by?: string; + special_requirements?: string; + storage_requirements?: string; + shelf_life_days?: number; + quality_specifications?: Record; + procurement_notes?: string; + + // Smart procurement calculation metadata + calculation_method?: string; + ai_suggested_quantity?: number; + adjusted_quantity?: number; + adjustment_reason?: string; + price_tier_applied?: Record; + supplier_minimum_applied?: boolean; + storage_limit_applied?: boolean; + reorder_rule_applied?: boolean; + + // Local production support fields + is_locally_produced?: boolean; + recipe_id?: string; + parent_requirement_id?: string; + bom_explosion_level?: number; +} + +// ================================================================ +// PROCUREMENT PLAN TYPES +// ================================================================ + +/** + * Base procurement plan + * Backend: ProcurementPlanBase schema + */ +export interface ProcurementPlanBase { + plan_date: string; + plan_period_start: string; + plan_period_end: string; + planning_horizon_days: number; + plan_type: ProcurementPlanType; + priority: PriorityLevel; + business_model?: BusinessModel; + procurement_strategy: ProcurementStrategy; + safety_stock_buffer: number; + supply_risk_level: RiskLevel; + demand_forecast_confidence?: number; + seasonality_adjustment: number; + special_requirements?: string; +} + +/** + * Create procurement plan request + * Backend: ProcurementPlanCreate schema + */ +export interface ProcurementPlanCreate extends ProcurementPlanBase { + tenant_id: string; + requirements?: ProcurementRequirementCreate[]; +} + +/** + * Update procurement plan request + * Backend: ProcurementPlanUpdate schema + */ +export interface ProcurementPlanUpdate { + status?: PlanStatus; + priority?: PriorityLevel; + approved_at?: string; + approved_by?: string; + execution_started_at?: string; + execution_completed_at?: string; + special_requirements?: string; + seasonal_adjustments?: Record; +} + +/** + * Approval workflow entry + * Backend: ApprovalWorkflowEntry (embedded in plan) + */ +export interface ApprovalWorkflowEntry { + timestamp: string; + from_status: string; + to_status: string; + user_id?: string; + notes?: string; +} + +/** + * Procurement plan response + * Backend: ProcurementPlanResponse schema + */ +export interface ProcurementPlanResponse extends ProcurementPlanBase { + id: string; + tenant_id: string; + plan_number: string; + status: PlanStatus; + total_requirements: number; + total_estimated_cost: number; + total_approved_cost: number; + cost_variance: number; + total_demand_orders: number; + total_demand_quantity: number; + total_production_requirements: number; + primary_suppliers_count: number; + backup_suppliers_count: number; + supplier_diversification_score?: number; + approved_at?: string; + approved_by?: string; + execution_started_at?: string; + execution_completed_at?: string; + fulfillment_rate?: number; + on_time_delivery_rate?: number; + cost_accuracy?: number; + quality_score?: number; + approval_workflow?: ApprovalWorkflowEntry[]; + created_at: string; + updated_at: string; + created_by?: string; + updated_by?: string; + requirements: ProcurementRequirementResponse[]; +} + +// ================================================================ +// ANALYTICS & DASHBOARD TYPES +// ================================================================ + +/** + * Procurement summary metrics + * Backend: Returned by analytics endpoints + */ +export interface ProcurementSummary { + total_plans: number; + active_plans: number; + total_requirements: number; + pending_requirements: number; + critical_requirements: number; + total_estimated_cost: number; + total_approved_cost: number; + cost_variance: number; + average_fulfillment_rate?: number; + average_on_time_delivery?: number; + top_suppliers: Record[]; + critical_items: Record[]; +} + +/** + * Procurement dashboard data + * Backend: GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement + */ +export interface ProcurementDashboardData { + current_plan?: ProcurementPlanResponse; + summary: { + total_plans: number; + total_estimated_cost: number; + total_approved_cost: number; + cost_variance: number; + }; + upcoming_deliveries?: Record[]; + overdue_requirements?: Record[]; + low_stock_alerts?: Record[]; + performance_metrics: { + average_fulfillment_rate: number; + average_on_time_delivery: number; + cost_accuracy: number; + supplier_performance: number; + fulfillment_trend?: number; + on_time_trend?: number; + cost_variance_trend?: number; + }; + + plan_status_distribution?: Array<{ + status: string; + count: number; + }>; + critical_requirements?: { + low_stock: number; + overdue: number; + high_priority: number; + }; + recent_plans?: Array<{ + id: string; + plan_number: string; + plan_date: string; + status: string; + total_requirements: number; + total_estimated_cost: number; + created_at: string; + }>; + supplier_performance?: Array<{ + id: string; + name: string; + total_orders: number; + fulfillment_rate: number; + on_time_rate: number; + quality_score: number; + }>; + cost_by_category?: Array<{ + name: string; + amount: number; + }>; + quality_metrics?: { + avg_score: number; + high_quality_count: number; + low_quality_count: number; + }; +} + +/** + * Procurement trends data for time-series charts + * Backend: GET /api/v1/tenants/{tenant_id}/procurement/analytics/procurement/trends + */ +export interface ProcurementTrendsData { + performance_trend: Array<{ + date: string; + fulfillment_rate: number; + on_time_rate: number; + }>; + quality_trend: Array<{ + date: string; + quality_score: number; + }>; + period_days: number; + start_date: string; + end_date: string; +} + +// ================================================================ +// REQUEST & RESPONSE TYPES +// ================================================================ + +/** + * Generate procurement plan request + * Backend: POST /api/v1/tenants/{tenant_id}/procurement/plans/generate + */ +export interface GeneratePlanRequest { + plan_date?: string; + force_regenerate: boolean; + planning_horizon_days: number; + include_safety_stock: boolean; + safety_stock_percentage: number; +} + +/** + * Generate procurement plan response + * Backend: POST /api/v1/tenants/{tenant_id}/procurement/plans/generate + */ +export interface GeneratePlanResponse { + success: boolean; + message: string; + plan?: ProcurementPlanResponse; + warnings: string[]; + errors: string[]; +} + +/** + * Auto-generate procurement request (Orchestrator integration) + * Backend: POST /api/v1/tenants/{tenant_id}/procurement/auto-generate + */ +export interface AutoGenerateProcurementRequest { + forecast_data: Record; + production_schedule_id?: string; + target_date?: string; + planning_horizon_days: number; + safety_stock_percentage: number; + auto_create_pos: boolean; + auto_approve_pos: boolean; + + // Cached data from Orchestrator + inventory_data?: Record; + suppliers_data?: Record; + recipes_data?: Record; +} + +/** + * Auto-generate procurement response + * Backend: POST /api/v1/tenants/{tenant_id}/procurement/auto-generate + */ +export interface AutoGenerateProcurementResponse { + success: boolean; + message: string; + plan_id?: string; + plan_number?: string; + requirements_created: number; + purchase_orders_created: number; + purchase_orders_auto_approved: number; + total_estimated_cost: number; + warnings: string[]; + errors: string[]; + created_pos: Array<{ + po_id: string; + po_number: string; + supplier_id: string; + items_count: number; + total_amount: number; + }>; +} + +/** + * Create purchase orders from plan result + * Backend: POST /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/create-purchase-orders + */ +export interface CreatePOsResult { + success: boolean; + created_pos: { + po_id: string; + po_number: string; + supplier_id: string; + items_count: number; + total_amount: number; + }[]; + failed_pos: { + supplier_id: string; + error: string; + }[]; + total_created: number; + total_failed: number; +} + +/** + * Link requirement to purchase order request + * Backend: Used in requirement linking operations + */ +export interface LinkRequirementToPORequest { + purchase_order_id: string; + purchase_order_number: string; + ordered_quantity: number; + expected_delivery_date?: string; +} + +/** + * Update delivery status request + * Backend: Used in delivery status updates + */ +export interface UpdateDeliveryStatusRequest { + delivery_status: string; + received_quantity?: number; + actual_delivery_date?: string; + quality_rating?: number; +} + +/** + * Approval request + * Backend: Used in plan approval operations + */ +export interface ApprovalRequest { + approval_notes?: string; +} + +/** + * Rejection request + * Backend: Used in plan rejection operations + */ +export interface RejectionRequest { + rejection_notes?: string; +} + +/** + * Paginated procurement plans + * Backend: GET /api/v1/tenants/{tenant_id}/procurement/plans + */ +export interface PaginatedProcurementPlans { + plans: ProcurementPlanResponse[]; + total: number; + page: number; + limit: number; + has_more: boolean; +} + +/** + * Forecast request + * Backend: Used in forecasting operations + */ +export interface ForecastRequest { + target_date: string; + horizon_days: number; + include_confidence_intervals: boolean; + product_ids?: string[]; +} + +// ================================================================ +// QUERY PARAMETER TYPES +// ================================================================ + +/** + * Get procurement plans query parameters + * Backend: GET /api/v1/tenants/{tenant_id}/procurement/plans + */ +export interface GetProcurementPlansParams { + tenant_id: string; + status?: string; + start_date?: string; + end_date?: string; + limit?: number; + offset?: number; +} + +/** + * Get plan requirements query parameters + * Backend: GET /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/requirements + */ +export interface GetPlanRequirementsParams { + tenant_id: string; + plan_id: string; + status?: string; + priority?: string; +} + +/** + * Update plan status parameters + * Backend: PATCH /api/v1/tenants/{tenant_id}/procurement/plans/{plan_id}/status + */ +export interface UpdatePlanStatusParams { + tenant_id: string; + plan_id: string; + status: PlanStatus; + notes?: string; +} diff --git a/frontend/src/components/domain/team/TransferOwnershipModal.tsx b/frontend/src/components/domain/team/TransferOwnershipModal.tsx new file mode 100644 index 00000000..b379cc9f --- /dev/null +++ b/frontend/src/components/domain/team/TransferOwnershipModal.tsx @@ -0,0 +1,375 @@ +/** + * Transfer Ownership Modal Component + * + * Allows the current tenant owner to transfer ownership to another admin. + * This is a critical operation with multiple confirmation steps. + */ + +import React, { useState } from 'react'; +import { Crown, AlertTriangle, Shield, ChevronRight } from 'lucide-react'; +import { StatusModal } from '../../ui'; +import { TENANT_ROLES } from '../../../types/roles'; + +export interface TeamMember { + id: string; + user_id: string; + role: string; + is_active: boolean; + user_email?: string | null; + user_full_name?: string | null; + joined_at?: string | null; +} + +interface TransferOwnershipModalProps { + isOpen: boolean; + onClose: () => void; + onTransfer: (newOwnerId: string) => Promise; + currentOwner: TeamMember | null; + eligibleMembers: TeamMember[]; // Only admins should be eligible + tenantName: string; +} + +const TransferOwnershipModal: React.FC = ({ + isOpen, + onClose, + onTransfer, + currentOwner, + eligibleMembers, + tenantName, +}) => { + const [selectedMemberId, setSelectedMemberId] = useState(''); + const [confirmationStep, setConfirmationStep] = useState<1 | 2>(1); + const [confirmationText, setConfirmationText] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + + const selectedMember = eligibleMembers.find(m => m.user_id === selectedMemberId); + const requiredConfirmationText = 'TRANSFERIR PROPIEDAD'; + + const handleReset = () => { + setSelectedMemberId(''); + setConfirmationStep(1); + setConfirmationText(''); + setError(null); + setIsProcessing(false); + }; + + const handleClose = () => { + handleReset(); + onClose(); + }; + + const handleNextStep = () => { + if (!selectedMember) { + setError('Por favor selecciona un miembro'); + return; + } + setError(null); + setConfirmationStep(2); + }; + + const handleTransfer = async () => { + if (confirmationText !== requiredConfirmationText) { + setError(`Por favor escribe "${requiredConfirmationText}" para confirmar`); + return; + } + + if (!selectedMember) { + setError('No se ha seleccionado ningún miembro'); + return; + } + + try { + setIsProcessing(true); + setError(null); + await onTransfer(selectedMember.user_id); + handleClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Error al transferir la propiedad'); + setIsProcessing(false); + } + }; + + return ( + +
+ {/* Warning Banner */} +
+
+ +
+

+ Acción Irreversible +

+

+ Transferir la propiedad es permanente. El nuevo propietario tendrá control total + de la organización y podrá cambiar todos los permisos, incluyendo los tuyos. +

+
+
+
+ + {/* Step 1: Select New Owner */} + {confirmationStep === 1 && ( +
+
+

+ Paso 1: Selecciona el Nuevo Propietario +

+

+ Solo puedes transferir la propiedad a un administrador actual de la organización. +

+
+ + {/* Current Owner Info */} +
+
+ + + Propietario Actual + +
+
+

+ {currentOwner?.user_full_name || currentOwner?.user_email || 'Usuario actual'} +

+

+ {currentOwner?.user_email} +

+
+
+ + {/* Eligible Members Selection */} +
+ + {eligibleMembers.length === 0 ? ( +
+ +

+ No hay administradores disponibles para la transferencia. +

+

+ Primero promociona a un miembro a administrador. +

+
+ ) : ( +
+ {eligibleMembers.map((member) => ( + + ))} +
+ )} +
+ + {error && ( +
+

{error}

+
+ )} + + {/* Actions */} +
+ + +
+
+ )} + + {/* Step 2: Confirmation */} + {confirmationStep === 2 && selectedMember && ( +
+
+

+ Paso 2: Confirma la Transferencia +

+

+ Por favor revisa cuidadosamente antes de confirmar. +

+
+ + {/* Transfer Summary */} +
+
+
+ +
+

De

+

+ {currentOwner?.user_full_name || currentOwner?.user_email} +

+
+
+ +
+ +
+

A

+

+ {selectedMember.user_full_name || selectedMember.user_email} +

+
+
+
+
+ + {/* Consequences List */} +
+

+ Qué sucederá: +

+
    +
  • + + + {selectedMember.user_full_name || selectedMember.user_email} será + el nuevo propietario con control total + +
  • +
  • + + Tu rol cambiará a Administrador +
  • +
  • + + El nuevo propietario podrá modificar tu rol o remover tu acceso +
  • +
  • + + Esta acción no se puede deshacer +
  • +
+
+ + {/* Confirmation Input */} +
+ + { + setConfirmationText(e.target.value); + setError(null); + }} + placeholder={requiredConfirmationText} + className="input w-full font-mono" + disabled={isProcessing} + autoComplete="off" + /> +
+ + {error && ( +
+

{error}

+
+ )} + + {/* Actions */} +
+ + +
+
+ )} +
+
+ ); +}; + +export default TransferOwnershipModal; diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index 91998843..db3acc98 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -20,6 +20,7 @@ export { StatsCard, StatsGrid } from './Stats'; export { StatusCard, getStatusColor } from './StatusCard'; export { EditViewModal } from './EditViewModal'; export { AddModal } from './AddModal'; +export { StatusModal } from './StatusModal'; export { DialogModal, showInfoDialog, showWarningDialog, showErrorDialog, showSuccessDialog, showConfirmDialog } from './DialogModal'; export { TenantSwitcher } from './TenantSwitcher'; export { LanguageSelector, CompactLanguageSelector } from './LanguageSelector'; @@ -51,6 +52,7 @@ export type { StatsCardProps, StatsCardVariant, StatsCardSize, StatsGridProps } export type { StatusCardProps, StatusIndicatorConfig } from './StatusCard'; export type { EditViewModalProps, EditViewModalField, EditViewModalSection, EditViewModalAction } from './EditViewModal'; export type { AddModalProps, AddModalField, AddModalSection } from './AddModal'; +export type { StatusModalProps, StatusModalField, StatusModalSection, StatusModalAction } from './StatusModal/StatusModal'; export type { DialogModalProps, DialogModalAction } from './DialogModal'; export type { LoadingSpinnerProps } from './LoadingSpinner'; export type { EmptyStateProps } from './EmptyState'; diff --git a/frontend/src/contexts/SubscriptionEventsContext.tsx b/frontend/src/contexts/SubscriptionEventsContext.tsx index ae4bfb19..09bbe439 100644 --- a/frontend/src/contexts/SubscriptionEventsContext.tsx +++ b/frontend/src/contexts/SubscriptionEventsContext.tsx @@ -10,35 +10,27 @@ const SubscriptionEventsContext = createContext = ({ children }) => { const [subscriptionVersion, setSubscriptionVersion] = useState(0); - const [subscribers, setSubscribers] = useState void>>(new Set()); + const subscribersRef = React.useRef void>>(new Set()); const notifySubscriptionChanged = useCallback(() => { setSubscriptionVersion(prev => prev + 1); - + // Notify all subscribers - subscribers.forEach(callback => { + subscribersRef.current.forEach(callback => { try { callback(); } catch (error) { console.warn('Error notifying subscription change subscriber:', error); } }); - }, [subscribers]); + }, []); // Empty dependency array - function is now stable const subscribeToChanges = useCallback((callback: () => void) => { - setSubscribers(prev => { - const newSubscribers = new Set(prev); - newSubscribers.add(callback); - return newSubscribers; - }); + subscribersRef.current.add(callback); // Return unsubscribe function return () => { - setSubscribers(prev => { - const newSubscribers = new Set(prev); - newSubscribers.delete(callback); - return newSubscribers; - }); + subscribersRef.current.delete(callback); }; }, []); diff --git a/frontend/src/features/demo-onboarding/config/tour-steps.ts b/frontend/src/features/demo-onboarding/config/tour-steps.ts index 79d461e4..4b0bb9db 100644 --- a/frontend/src/features/demo-onboarding/config/tour-steps.ts +++ b/frontend/src/features/demo-onboarding/config/tour-steps.ts @@ -4,7 +4,7 @@ export const getDemoTourSteps = (): DriveStep[] => [ { element: '[data-tour="demo-banner"]', popover: { - title: '¡Bienvenido a BakeryIA!', + title: '¡Bienvenido a El Panadero Digital!', description: 'Descubre cómo gestionar tu panadería en 5 minutos al día. Esta demo de 30 minutos usa datos reales de una panadería española. Te mostramos cómo ahorrar 2-3 horas diarias en planificación y reducir desperdicio un 15-25%. Puedes cerrar el tour con ESC.', side: 'bottom', align: 'center', @@ -105,7 +105,7 @@ export const getMobileTourSteps = (): DriveStep[] => [ { element: '[data-tour="demo-banner"]', popover: { - title: '¡Bienvenido a BakeryIA!', + title: '¡Bienvenido a El Panadero Digital !', description: 'Gestiona tu panadería en 5 min/día. Demo de 30 min con datos reales. Ahorra 2-3h diarias y reduce desperdicio 15-25%.', side: 'bottom', align: 'center', diff --git a/frontend/src/locales/es/landing.json b/frontend/src/locales/es/landing.json index df551741..43a692ee 100644 --- a/frontend/src/locales/es/landing.json +++ b/frontend/src/locales/es/landing.json @@ -244,13 +244,13 @@ } }, "central_workshop": { - "title": "Obrador Central + Puntos de Venta", - "subtitle": "Producción centralizada, distribución múltiple", - "description": "Produces centralmente y distribuyes a múltiples puntos de venta. Necesitas coordinar producción, logística y demanda entre ubicaciones para optimizar cada punto.", + "title": "Panadería Franquiciada", + "subtitle": "Punto de venta con obrador central", + "description": "Operas un punto de venta que recibe productos de un obrador central. Necesitas gestionar pedidos, inventario y ventas para optimizar tu operación retail.", "features": { - "prediction": "Predicción agregada y por punto de venta individual", - "distribution": "Gestión de distribución multi-ubicación coordinada", - "visibility": "Visibilidad centralizada con control granular" + "prediction": "Gestión de pedidos al obrador central", + "distribution": "Control de inventario de productos recibidos", + "visibility": "Previsión de ventas para tu punto" } }, "same_ai": "La misma IA potente, adaptada a tu forma de trabajar" diff --git a/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx b/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx index 8a2f2feb..5b78e502 100644 --- a/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx +++ b/frontend/src/pages/app/analytics/ProcurementAnalyticsPage.tsx @@ -12,11 +12,21 @@ import { Truck, Calendar } from 'lucide-react'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Legend +} from 'recharts'; import { PageHeader } from '../../../components/layout'; import { Card, StatsGrid, Button, Tabs } from '../../../components/ui'; import { useSubscription } from '../../../api/hooks/subscription'; import { useCurrentTenant } from '../../../stores/tenant.store'; -import { useProcurementDashboard } from '../../../api/hooks/orders'; +import { useProcurementDashboard, useProcurementTrends } from '../../../api/hooks/procurement'; import { formatters } from '../../../components/ui/Stats/StatsPresets'; const ProcurementAnalyticsPage: React.FC = () => { @@ -27,6 +37,7 @@ const ProcurementAnalyticsPage: React.FC = () => { const [activeTab, setActiveTab] = useState('overview'); const { data: dashboard, isLoading: dashboardLoading } = useProcurementDashboard(tenantId); + const { data: trends, isLoading: trendsLoading } = useProcurementTrends(tenantId, 7); // Check if user has access to advanced analytics (professional/enterprise) const hasAdvancedAccess = canAccessAnalytics('advanced'); @@ -162,32 +173,32 @@ const ProcurementAnalyticsPage: React.FC = () => { <> {/* Overview Tab */}
- {/* Plan Status Distribution */} - -
-

- Distribución de Estados de Planes -

-
- {dashboard?.plan_status_distribution?.map((status: any) => ( -
- {status.status} -
-
-
-
- - {status.count} - + {/* Plan Status Distribution */} + +
+

+ Distribución de Estados de Planes +

+
+ {dashboard?.plan_status_distribution?.map((status: any) => ( +
+ {status.status} +
+
+
+ + {status.count} +
- ))} -
+
+ ))}
- +
+
{/* Critical Requirements */} @@ -302,15 +313,63 @@ const ProcurementAnalyticsPage: React.FC = () => {
- {/* Performance Trend Chart Placeholder */} + {/* Performance Trend Chart */}

- Tendencias de Rendimiento + Tendencias de Rendimiento (Últimos 7 días)

-
- Gráfico de tendencias - Próximamente -
+ {trendsLoading ? ( +
+
+
+ ) : trends && trends.performance_trend && trends.performance_trend.length > 0 ? ( + + + + + `${(value * 100).toFixed(0)}%`} + /> + `${(value * 100).toFixed(1)}%`} + labelStyle={{ color: 'var(--text-primary)' }} + /> + + + + + + ) : ( +
+ No hay datos de tendencias disponibles +
+ )}
@@ -459,11 +518,51 @@ const ProcurementAnalyticsPage: React.FC = () => {

- Tendencia de Calidad + Tendencia de Calidad (Últimos 7 días)

-
- Gráfico de tendencia de calidad - Próximamente -
+ {trendsLoading ? ( +
+
+
+ ) : trends && trends.quality_trend && trends.quality_trend.length > 0 ? ( + + + + + + `${value.toFixed(1)} / 10`} + labelStyle={{ color: 'var(--text-primary)' }} + /> + + + + ) : ( +
+ No hay datos de calidad disponibles +
+ )}
diff --git a/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx b/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx index 74c99132..a42fcc19 100644 --- a/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx +++ b/frontend/src/pages/app/analytics/performance/PerformanceAnalyticsPage.tsx @@ -1,22 +1,80 @@ import React, { useState } from 'react'; -import { Activity, Clock, Users, TrendingUp, Target, AlertCircle, Download, Calendar } from 'lucide-react'; -import { Button, Card, Badge } from '../../../../components/ui'; +import { + Activity, + Clock, + TrendingUp, + Target, + AlertCircle, + Download, + Calendar, + Lock, + BarChart3, + Zap, + DollarSign, + Package, + AlertOctagon, +} from 'lucide-react'; +import { + ResponsiveContainer, + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + LineChart, + Line, + RadarChart, + Radar, + PolarGrid, + PolarAngleAxis, + PolarRadiusAxis, +} from 'recharts'; +import { Button, Card, Badge, StatsGrid, Tabs } from '../../../../components/ui'; import { PageHeader } from '../../../../components/layout'; +import { useSubscription } from '../../../../api/hooks/subscription'; +import { useCurrentTenant } from '../../../../stores/tenant.store'; +import { + useCycleTimeMetrics, + useProcessEfficiencyScore, + useResourceUtilization, + useCostRevenueRatio, + useQualityImpactIndex, + useCriticalBottlenecks, + useDepartmentPerformance, + usePerformanceAlerts, +} from '../../../../api/hooks/performance'; +import { TimePeriod } from '../../../../api/types/performance'; + +// Formatters for StatsGrid +const formatters = { + number: (value: number) => value.toFixed(0), + percentage: (value: number) => `${value.toFixed(1)}%`, + hours: (value: number) => `${value.toFixed(1)}h`, + currency: (value: number) => `€${value.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`, +}; const PerformanceAnalyticsPage: React.FC = () => { - const [selectedTimeframe, setSelectedTimeframe] = useState('month'); - const [selectedMetric, setSelectedMetric] = useState('efficiency'); + const { canAccessAnalytics, subscriptionInfo } = useSubscription(); + const currentTenant = useCurrentTenant(); + const tenantId = currentTenant?.id || ''; - const performanceMetrics = { - overallEfficiency: 87.5, - productionTime: 4.2, - qualityScore: 92.1, - employeeProductivity: 89.3, - customerSatisfaction: 94.7, - resourceUtilization: 78.9, - }; + const [selectedPeriod, setSelectedPeriod] = useState('week'); + const [activeTab, setActiveTab] = useState('overview'); - const timeframes = [ + // Fetch all cross-functional performance data + const { data: cycleTime, isLoading: cycleTimeLoading } = useCycleTimeMetrics(tenantId, selectedPeriod); + const { data: processScore, isLoading: processScoreLoading } = useProcessEfficiencyScore(tenantId, selectedPeriod); + const { data: resourceUtil, isLoading: resourceUtilLoading } = useResourceUtilization(tenantId, selectedPeriod); + const { data: costRevenue, isLoading: costRevenueLoading } = useCostRevenueRatio(tenantId, selectedPeriod); + const { data: qualityIndex, isLoading: qualityIndexLoading } = useQualityImpactIndex(tenantId, selectedPeriod); + const { data: bottlenecks, isLoading: bottlenecksLoading } = useCriticalBottlenecks(tenantId, selectedPeriod); + const { data: departments, isLoading: departmentsLoading } = useDepartmentPerformance(tenantId, selectedPeriod); + const { data: alerts, isLoading: alertsLoading } = usePerformanceAlerts(tenantId); + + // Period options + const timeframes: { value: TimePeriod; label: string }[] = [ { value: 'day', label: 'Hoy' }, { value: 'week', label: 'Esta Semana' }, { value: 'month', label: 'Este Mes' }, @@ -24,380 +82,568 @@ const PerformanceAnalyticsPage: React.FC = () => { { value: 'year', label: 'Año' }, ]; - const departmentPerformance = [ - { - department: 'Producción', - efficiency: 91.2, - trend: 5.3, - issues: 2, - employees: 8, - metrics: { - avgBatchTime: '2.3h', - qualityRate: '94%', - wastePercentage: '3.1%' - } - }, - { - department: 'Ventas', - efficiency: 88.7, - trend: -1.2, - issues: 1, - employees: 4, - metrics: { - avgServiceTime: '3.2min', - customerWaitTime: '2.1min', - salesPerHour: '€127' - } - }, - { - department: 'Inventario', - efficiency: 82.4, - trend: 2.8, - issues: 3, - employees: 2, - metrics: { - stockAccuracy: '96.7%', - turnoverRate: '12.3', - wastageRate: '4.2%' - } - }, - { - department: 'Administración', - efficiency: 94.1, - trend: 8.1, - issues: 0, - employees: 3, - metrics: { - responseTime: '1.2h', - taskCompletion: '98%', - documentAccuracy: '99.1%' - } - } + // Tab configuration + const tabs = [ + { id: 'overview', label: 'Vista General' }, + { id: 'efficiency', label: 'Eficiencia Operativa' }, + { id: 'quality', label: 'Impacto de Calidad' }, + { id: 'optimization', label: 'Optimización' }, ]; - const kpiTrends = [ - { - name: 'Eficiencia General', - current: 87.5, - target: 90.0, - previous: 84.2, - unit: '%', - color: 'blue' - }, - { - name: 'Tiempo de Producción', - current: 4.2, - target: 4.0, - previous: 4.5, - unit: 'h', - color: 'green', - inverse: true - }, - { - name: 'Satisfacción Cliente', - current: 94.7, - target: 95.0, - previous: 93.1, - unit: '%', - color: 'purple' - }, - { - name: 'Utilización de Recursos', - current: 78.9, - target: 85.0, - previous: 76.3, - unit: '%', - color: 'orange' - } - ]; + // Combined loading state + const isLoading = + cycleTimeLoading || + processScoreLoading || + resourceUtilLoading || + costRevenueLoading || + qualityIndexLoading || + bottlenecksLoading || + departmentsLoading || + alertsLoading; - const performanceAlerts = [ - { - id: '1', - type: 'warning', - title: 'Eficiencia de Inventario Baja', - description: 'El departamento de inventario está por debajo del objetivo del 85%', - value: '82.4%', - target: '85%', - department: 'Inventario' - }, - { - id: '2', - type: 'info', - title: 'Tiempo de Producción Mejorado', - description: 'El tiempo promedio de producción ha mejorado este mes', - value: '4.2h', - target: '4.0h', - department: 'Producción' - }, - { - id: '3', - type: 'success', - title: 'Administración Supera Objetivos', - description: 'El departamento administrativo está funcionando por encima del objetivo', - value: '94.1%', - target: '90%', - department: 'Administración' - } - ]; + // Show loading state while subscription data is being fetched + if (subscriptionInfo.loading) { + return ( +
+ + +
+
+

Cargando información de suscripción...

+
+
+
+ ); + } - const productivityData = [ - { hour: '07:00', efficiency: 75, transactions: 12, employees: 3 }, - { hour: '08:00', efficiency: 82, transactions: 18, employees: 5 }, - { hour: '09:00', efficiency: 89, transactions: 28, employees: 6 }, - { hour: '10:00', efficiency: 91, transactions: 32, employees: 7 }, - { hour: '11:00', efficiency: 94, transactions: 38, employees: 8 }, - { hour: '12:00', efficiency: 96, transactions: 45, employees: 8 }, - { hour: '13:00', efficiency: 95, transactions: 42, employees: 8 }, - { hour: '14:00', efficiency: 88, transactions: 35, employees: 7 }, - { hour: '15:00', efficiency: 85, transactions: 28, employees: 6 }, - { hour: '16:00', efficiency: 83, transactions: 25, employees: 5 }, - { hour: '17:00', efficiency: 87, transactions: 31, employees: 6 }, - { hour: '18:00', efficiency: 90, transactions: 38, employees: 7 }, - { hour: '19:00', efficiency: 86, transactions: 29, employees: 5 }, - { hour: '20:00', efficiency: 78, transactions: 18, employees: 3 }, - ]; + // If user doesn't have access to advanced analytics, show upgrade message + if (!canAccessAnalytics('advanced')) { + return ( +
+ + + +

+ Funcionalidad Exclusiva para Profesionales y Empresas +

+

+ El análisis de rendimiento avanzado está disponible solo para planes Professional y Enterprise. + Actualiza tu plan para acceder a métricas transversales de rendimiento, análisis de procesos integrados y optimización operativa. +

+ +
+
+ ); + } - const getTrendIcon = (trend: number) => { - if (trend > 0) { + // Helper functions + const getTrendIcon = (trend: 'up' | 'down' | 'stable') => { + if (trend === 'up') { return ; - } else { + } else if (trend === 'down') { return ; } + return ; }; - const getTrendColor = (trend: number) => { - return trend >= 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'; - }; - - const getPerformanceColor = (value: number, target: number, inverse = false) => { - const comparison = inverse ? value < target : value >= target; - return comparison ? 'text-[var(--color-success)]' : value >= target * 0.9 ? 'text-yellow-600' : 'text-[var(--color-error)]'; - }; - - const getAlertIcon = (type: string) => { + const getAlertIcon = (type: 'warning' | 'critical' | 'info') => { switch (type) { + case 'critical': + return ; case 'warning': return ; - case 'success': - return ; default: return ; } }; - const getAlertColor = (type: string) => { + const getAlertColor = (type: 'warning' | 'critical' | 'info') => { switch (type) { + case 'critical': + return 'bg-red-50 border-red-200'; case 'warning': return 'bg-yellow-50 border-yellow-200'; - case 'success': - return 'bg-green-50 border-green-200'; default: return 'bg-[var(--color-info)]/5 border-[var(--color-info)]/20'; } }; + // Prepare StatsGrid data (6 cross-functional metrics) - NO MEMOIZATION to avoid loops + const statsData = [ + { + title: 'Tiempo de Ciclo', + value: cycleTime?.average_cycle_time || 0, + icon: Clock, + formatter: formatters.hours, + }, + { + title: 'Eficiencia de Procesos', + value: processScore?.overall_score || 0, + icon: Zap, + formatter: formatters.percentage, + }, + { + title: 'Utilización de Recursos', + value: resourceUtil?.overall_utilization || 0, + icon: Package, + formatter: formatters.percentage, + }, + { + title: 'Ratio Costo-Ingreso', + value: costRevenue?.cost_revenue_ratio || 0, + icon: DollarSign, + formatter: formatters.percentage, + }, + { + title: 'Índice de Calidad', + value: qualityIndex?.overall_quality_index || 0, + icon: Target, + formatter: formatters.percentage, + }, + { + title: 'Cuellos de Botella Críticos', + value: bottlenecks?.critical_count || 0, + icon: AlertOctagon, + formatter: formatters.number, + }, + ]; + return (
+ {/* Page Header */} - - -
- } + description="Monitorea métricas transversales que muestran cómo los departamentos trabajan juntos" + actions={[ + { + id: 'configure-alerts', + label: 'Configurar Alertas', + icon: Calendar, + onClick: () => {}, + variant: 'outline', + disabled: true, + }, + { + id: 'export-report', + label: 'Exportar Reporte', + icon: Download, + onClick: () => {}, + variant: 'outline', + disabled: true, + }, + ]} /> {/* Controls */}
- +
-
- - -
- {/* KPI Overview */} -
- {kpiTrends.map((kpi) => ( - -
-

{kpi.name}

-
-
-
-
-

- {kpi.current}{kpi.unit} -

-

- Objetivo: {kpi.target}{kpi.unit} -

-
-
-
- {getTrendIcon(kpi.current - kpi.previous)} - - {Math.abs(kpi.current - kpi.previous).toFixed(1)}{kpi.unit} - -
-
-
-
-
-
-
- ))} -
+ {/* Block 1: StatsGrid with 6 cross-functional metrics */} + - {/* Performance Alerts */} - -

Alertas de Rendimiento

-
- {performanceAlerts.map((alert) => ( -
-
- {getAlertIcon(alert.type)} -
-
-

{alert.title}

- {alert.department} -
-

{alert.description}

-
- - Actual: {alert.value} - - - Objetivo: {alert.target} - -
-
-
-
- ))} -
-
+ {/* Block 2: Tabs */} + -
- {/* Department Performance */} - -

Rendimiento por Departamento

-
- {departmentPerformance.map((dept) => ( -
-
-
-

{dept.department}

- {dept.employees} empleados -
-
- - {dept.efficiency}% - -
- {getTrendIcon(dept.trend)} - - {Math.abs(dept.trend).toFixed(1)}% - -
-
-
- -
- {Object.entries(dept.metrics).map(([key, value]) => ( -
-

- {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} -

-

{value}

+ {/* Block 3: Tab Content */} +
+ {/* Vista General Tab */} + {activeTab === 'overview' && !isLoading && ( + <> + {/* Department Comparison Matrix */} + {departments && departments.length > 0 && ( + +

+ Comparación de Departamentos +

+
+ {departments.map((dept) => ( +
+
+

+ {dept.department_name} +

+
+ + {dept.efficiency.toFixed(1)}% + + {getTrendIcon(dept.trend)} +
+
+
+
+

+ {dept.metrics.primary_metric.label} +

+

+ {dept.metrics.primary_metric.value.toFixed(1)} + {dept.metrics.primary_metric.unit} +

+
+
+

+ {dept.metrics.secondary_metric.label} +

+

+ {dept.metrics.secondary_metric.value.toFixed(1)} + {dept.metrics.secondary_metric.unit} +

+
+
+

+ {dept.metrics.tertiary_metric.label} +

+

+ {dept.metrics.tertiary_metric.value.toFixed(1)} + {dept.metrics.tertiary_metric.unit} +

+
+
))}
+
+ )} - {dept.issues > 0 && ( -
- - {dept.issues} problema{dept.issues > 1 ? 's' : ''} detectado{dept.issues > 1 ? 's' : ''} + {/* Process Efficiency Breakdown */} + {processScore && ( + +

+ Desglose de Eficiencia por Procesos +

+ + + + + + + + + + + +
+ )} + + )} + + {/* Eficiencia Operativa Tab */} + {activeTab === 'efficiency' && !isLoading && ( + <> + {/* Cycle Time Breakdown */} + {cycleTime && ( + +

+ Análisis de Tiempo de Ciclo +

+
+
+

Pedido → Producción

+

+ {cycleTime.order_to_production_time.toFixed(1)}h +

- )} -
- ))} -
- +
+

Tiempo de Producción

+

+ {cycleTime.production_time.toFixed(1)}h +

+
+
+

Producción → Entrega

+

+ {cycleTime.production_to_delivery_time.toFixed(1)}h +

+
+
+
+ Tiempo Total de Ciclo + + {cycleTime.average_cycle_time.toFixed(1)}h + +
+ + )} - {/* Hourly Productivity */} - -

Eficiencia por Hora

-
- {productivityData.map((data, index) => ( -
-
{data.efficiency}%
-
= 90 ? '#10B981' : data.efficiency >= 80 ? '#F59E0B' : '#EF4444' - }} - >
- - {data.hour} - -
- ))} -
-
-
-
- ≥90% Excelente -
-
-
- 80-89% Bueno -
-
-
- <80% Bajo -
-
-
+ {/* Bottlenecks Analysis */} + {bottlenecks && bottlenecks.bottlenecks.length > 0 && ( + +

+ Cuellos de Botella Detectados +

+
+ {bottlenecks.bottlenecks.map((bottleneck, index) => ( +
+
+ +
+
+

+ {bottleneck.description} +

+ + {bottleneck.area} + +
+

+ {bottleneck.metric}: {bottleneck.value.toFixed(1)} +

+
+
+
+ ))} +
+
+ )} + + {/* Resource Utilization */} + {resourceUtil && ( + +

+ Utilización de Recursos +

+
+
+

Equipamiento

+

+ {resourceUtil.equipment_utilization.toFixed(1)}% +

+
+
+

Inventario

+

+ {resourceUtil.inventory_utilization.toFixed(1)}% +

+
+
+

Balance

+

+ {resourceUtil.resource_balance === 'balanced' ? 'Equilibrado' : 'Desbalanceado'} +

+
+
+
+ )} + + )} + + {/* Impacto de Calidad Tab */} + {activeTab === 'quality' && !isLoading && ( + <> + {/* Quality Index Overview */} + {qualityIndex && ( + +

+ Índice de Calidad General +

+
+
+

Calidad de Producción

+

+ {qualityIndex.production_quality.toFixed(1)}% +

+
+
+

Calidad de Inventario

+

+ {qualityIndex.inventory_quality.toFixed(1)}% +

+
+
+
+ Índice de Calidad Combinado + + {qualityIndex.overall_quality_index.toFixed(1)}% + +
+
+ )} + + {/* Quality Issues Breakdown */} + {qualityIndex && ( + +

+ Desglose de Problemas de Calidad +

+
+
+ Defectos de Producción + + {qualityIndex.quality_issues.production_defects.toFixed(1)}% + +
+
+ Desperdicio + + {qualityIndex.quality_issues.waste_percentage.toFixed(1)}% + +
+
+ Items por Vencer + + {qualityIndex.quality_issues.expiring_items} + +
+
+ Stock Bajo Afectando Calidad + + {qualityIndex.quality_issues.low_stock_affecting_quality} + +
+
+
+ )} + + )} + + {/* Optimización Tab */} + {activeTab === 'optimization' && !isLoading && ( + <> + {/* Cost-Revenue Analysis */} + {costRevenue && ( + +

+ Análisis de Rentabilidad +

+
+
+

Ingresos Totales

+

+ €{costRevenue.total_revenue.toLocaleString('es-ES')} +

+
+
+

Costos Estimados

+

+ €{costRevenue.estimated_costs.toLocaleString('es-ES')} +

+
+
+
+
+ Ratio Costo-Ingreso + + {costRevenue.cost_revenue_ratio.toFixed(1)}% + +
+
+ Margen de Beneficio + + {costRevenue.profit_margin.toFixed(1)}% + +
+
+
+ )} + + {/* Improvement Recommendations */} + {bottlenecks && bottlenecks.total_bottlenecks > 0 && ( + +

+ Recomendaciones de Mejora +

+
+
+

+ Área más crítica: {bottlenecks.most_critical_area} +

+

+ Se han detectado {bottlenecks.critical_count} cuellos de botella críticos. + Prioriza la optimización de esta área para mejorar el flujo general. +

+
+ {qualityIndex && qualityIndex.waste_impact > 5 && ( +
+

+ Reducir Desperdicio +

+

+ El desperdicio actual es de {qualityIndex.waste_impact.toFixed(1)}%. + Implementa controles de calidad más estrictos para reducir pérdidas. +

+
+ )} + {resourceUtil && resourceUtil.resource_balance === 'imbalanced' && ( +
+

+ Balance de Recursos +

+

+ Los recursos están desbalanceados entre departamentos. + Considera redistribuir para optimizar la utilización general. +

+
+ )} +
+
+ )} + + {/* No Recommendations */} + {(!bottlenecks || bottlenecks.total_bottlenecks === 0) && ( + +

+ Recomendaciones de Mejora +

+
+ +

+ ¡Excelente! No se han detectado áreas críticas que requieran optimización inmediata. +

+
+
+ )} + + )}
); }; -export default PerformanceAnalyticsPage; \ No newline at end of file +export default PerformanceAnalyticsPage; diff --git a/frontend/src/pages/app/analytics/scenario-simulation/ScenarioSimulationPage.tsx b/frontend/src/pages/app/analytics/scenario-simulation/ScenarioSimulationPage.tsx index 40192066..d2eb438a 100644 --- a/frontend/src/pages/app/analytics/scenario-simulation/ScenarioSimulationPage.tsx +++ b/frontend/src/pages/app/analytics/scenario-simulation/ScenarioSimulationPage.tsx @@ -5,7 +5,7 @@ * Allows users to test different scenarios and see potential impacts on demand */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useTenantStore } from '../../../../stores'; import { forecastingService } from '../../../../api/services/forecasting'; @@ -39,8 +39,11 @@ import { ArrowDownRight, Play, Sparkles, + Package, } from 'lucide-react'; import { PageHeader } from '../../../../components/layout'; +import { useIngredients } from '../../../../api/hooks/inventory'; +import { useModels } from '../../../../api/hooks/training'; export const ScenarioSimulationPage: React.FC = () => { const { t } = useTranslation(); @@ -57,6 +60,43 @@ export const ScenarioSimulationPage: React.FC = () => { const [durationDays, setDurationDays] = useState(7); const [selectedProducts, setSelectedProducts] = useState([]); + // Fetch real inventory data + const { + data: ingredientsData, + isLoading: ingredientsLoading, + } = useIngredients(currentTenant?.id || ''); + + // Fetch trained models to filter products + const { + data: modelsData, + isLoading: modelsLoading, + } = useModels(currentTenant?.id || '', { active_only: true }); + + // Build products list from ingredients that have trained models + const availableProducts = useMemo(() => { + if (!ingredientsData || !modelsData) { + return []; + } + + // Handle both array and paginated response formats + const modelsList = Array.isArray(modelsData) ? modelsData : (modelsData.models || modelsData.items || []); + + // Get inventory product IDs that have trained models + const modelProductIds = new Set(modelsList.map((model: any) => model.inventory_product_id)); + + // Filter ingredients to only those with models + const ingredientsWithModels = ingredientsData.filter(ingredient => + modelProductIds.has(ingredient.id) + ); + + return ingredientsWithModels.map(ingredient => ({ + id: ingredient.id, + name: ingredient.name, + category: ingredient.category || 'Other', + hasModel: true + })); + }, [ingredientsData, modelsData]); + // Scenario-specific parameters const [weatherParams, setWeatherParams] = useState({ temperature_change: 15, @@ -81,6 +121,16 @@ export const ScenarioSimulationPage: React.FC = () => { promotion_type: 'discount', expected_traffic_increase: 0.3, }); + const [holidayParams, setHolidayParams] = useState({ + holiday_name: 'christmas', + expected_impact_multiplier: 1.5, + }); + const [supplyDisruptionParams, setSupplyDisruptionParams] = useState({ + severity: 'moderate', + affected_percentage: 30, + duration_days: 7, + }); + const [customParams, setCustomParams] = useState>({}); const handleSimulate = async () => { if (!currentTenant?.id) return; @@ -119,6 +169,15 @@ export const ScenarioSimulationPage: React.FC = () => { case ScenarioType.PROMOTION: request.promotion_params = promotionParams; break; + case ScenarioType.HOLIDAY: + request.custom_multipliers = { holiday_multiplier: holidayParams.expected_impact_multiplier }; + break; + case ScenarioType.SUPPLY_DISRUPTION: + request.custom_multipliers = { disruption_severity: supplyDisruptionParams.affected_percentage / 100 }; + break; + case ScenarioType.CUSTOM: + request.custom_multipliers = customParams; + break; } const result = await forecastingService.simulateScenario(currentTenant.id, request); @@ -201,7 +260,7 @@ export const ScenarioSimulationPage: React.FC = () => { value={scenarioName} onChange={(e) => setScenarioName(e.target.value)} placeholder={t('analytics.scenario_simulation.scenario_name_placeholder', 'e.g., Summer Heatwave Impact')} - className="w-full px-3 py-2 border rounded-lg" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
@@ -215,7 +274,7 @@ export const ScenarioSimulationPage: React.FC = () => { type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} - className="w-full px-3 py-2 border rounded-lg" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" min={new Date().toISOString().split('T')[0]} />
@@ -229,10 +288,92 @@ export const ScenarioSimulationPage: React.FC = () => { onChange={(e) => setDurationDays(parseInt(e.target.value) || 7)} min={1} max={30} - className="w-full px-3 py-2 border rounded-lg" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
+ + {/* Product Selection */} +
+
+ + + {selectedProducts.length} selected + +
+ + {ingredientsLoading || modelsLoading ? ( +
+ Loading products... +
+ ) : availableProducts.length === 0 ? ( +
+
+ +
+

No products available for simulation

+

You need to train ML models for your products first. Visit the Training section to get started.

+
+
+
+ ) : ( + <> + {/* Quick Select All/None */} +
+ + | + +
+ + {/* Product Grid */} +
+ {availableProducts.map((product) => ( + + ))} +
+ + )} +
@@ -274,7 +415,7 @@ export const ScenarioSimulationPage: React.FC = () => { type="number" value={weatherParams.temperature_change || 0} onChange={(e) => setWeatherParams({ ...weatherParams, temperature_change: parseFloat(e.target.value) })} - className="w-full px-3 py-2 border rounded-lg mt-1" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" min={-30} max={30} /> @@ -284,7 +425,7 @@ export const ScenarioSimulationPage: React.FC = () => { setPromotionParams({ ...promotionParams, promotion_type: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + > + + + + +
@@ -353,10 +508,208 @@ export const ScenarioSimulationPage: React.FC = () => { type="number" value={promotionParams.expected_traffic_increase * 100} onChange={(e) => setPromotionParams({ ...promotionParams, expected_traffic_increase: parseFloat(e.target.value) / 100 })} - className="w-full px-3 py-2 border rounded-lg mt-1" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" min={0} max={200} /> +

Most promotions see 20-50% increase

+
+
+ )} + + {selectedScenarioType === ScenarioType.EVENT && ( +
+
+ + +
+
+ + setEventParams({ ...eventParams, expected_attendance: parseInt(e.target.value) || 0 })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={100} + step={100} + /> +

Number of people expected

+
+
+ + setEventParams({ ...eventParams, distance_km: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={0} + max={50} + /> +

Closer events have bigger impact

+
+
+ + setEventParams({ ...eventParams, duration_days: parseInt(e.target.value) || 1 })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={1} + max={30} + /> +
+
+ )} + + {selectedScenarioType === ScenarioType.PRICING && ( +
+
+ + setPricingParams({ ...pricingParams, price_change_percent: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={-50} + max={100} + /> +

+ Negative for price decrease, positive for increase +

+
+
+
+

💡 Pricing Impact Guide:

+
    +
  • -10% price: Usually +8-12% demand
  • +
  • +10% price: Usually -8-12% demand
  • +
  • Impact varies by product type and competition
  • +
+
+
+
+ )} + + {selectedScenarioType === ScenarioType.HOLIDAY && ( +
+
+ + +
+
+ + setHolidayParams({ ...holidayParams, expected_impact_multiplier: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={0.5} + max={3} + /> +

+ 1.0 = no change, 1.5 = 50% increase, 0.8 = 20% decrease +

+
+
+ )} + + {selectedScenarioType === ScenarioType.SUPPLY_DISRUPTION && ( +
+
+ + +
+
+ + setSupplyDisruptionParams({ ...supplyDisruptionParams, affected_percentage: parseInt(e.target.value) || 0 })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={0} + max={100} + /> +

+ Percentage of normal supply that will be unavailable +

+
+
+ + setSupplyDisruptionParams({ ...supplyDisruptionParams, duration_days: parseInt(e.target.value) || 1 })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={1} + max={30} + /> +
+
+ )} + + {selectedScenarioType === ScenarioType.CUSTOM && ( +
+
+

Custom Scenario

+

+ Define your own demand multiplier for unique situations +

+
+
+ + setCustomParams({ custom_multiplier: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg mt-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + min={0} + max={5} + /> +

+ 1.0 = no change, 1.5 = 50% increase, 0.7 = 30% decrease +

+
+
+ +