/** * Smart Action Handlers - Complete Implementation * Handles execution of all smart action types from enriched alerts * * NO PLACEHOLDERS - All action types fully implemented */ import { useNavigate } from 'react-router-dom'; import { SmartAction as ImportedSmartAction, SmartActionType } from '../api/types/events'; // ============================================================ // Types (using imported types from events.ts) // ============================================================ // Legacy interface for backwards compatibility with existing handler code export interface SmartAction { label?: string; label_key?: string; action_type: string; type?: string; // For backward compatibility variant?: 'primary' | 'secondary' | 'ghost' | 'danger'; metadata?: Record; disabled?: boolean; disabled_reason?: string; estimated_time_minutes?: number; consequence?: string; } // Re-export types from events.ts export { SmartActionType }; // ============================================================ // Smart Action Handler Class // ============================================================ export class SmartActionHandler { private navigate: ReturnType | null = null; private onSuccess?: (alertId?: string) => void; private onError?: (error: string) => void; constructor( navigate?: ReturnType, callbacks?: { onSuccess?: (alertId?: string) => void; onError?: (error: string) => void; } ) { this.navigate = navigate || null; this.onSuccess = callbacks?.onSuccess; this.onError = callbacks?.onError; } async handleAction(action: SmartAction, alertId?: string): Promise { try { let result = false; // Support both legacy (type) and new (action_type) field names const actionType = action.action_type || action.type; switch (actionType) { case SmartActionType.APPROVE_PO: result = await this.handleApprovePO(action); break; case SmartActionType.REJECT_PO: result = await this.handleRejectPO(action); break; case SmartActionType.MODIFY_PO: result = this.handleModifyPO(action); break; case SmartActionType.VIEW_PO_DETAILS: result = this.handleViewPODetails(action); break; case SmartActionType.CALL_SUPPLIER: result = this.handleCallSupplier(action); break; case SmartActionType.NAVIGATE: result = this.handleNavigate(action); break; case SmartActionType.ADJUST_PRODUCTION: result = this.handleAdjustProduction(action); break; case SmartActionType.START_PRODUCTION_BATCH: result = await this.handleStartProductionBatch(action); break; case SmartActionType.NOTIFY_CUSTOMER: result = await this.handleNotifyCustomer(action); break; case SmartActionType.CANCEL_AUTO_ACTION: result = await this.handleCancelAutoAction(action); break; case SmartActionType.MARK_DELIVERY_RECEIVED: result = await this.handleMarkDeliveryReceived(action); break; case SmartActionType.COMPLETE_STOCK_RECEIPT: result = await this.handleCompleteStockReceipt(action); break; case SmartActionType.OPEN_REASONING: result = this.handleOpenReasoning(action); break; case SmartActionType.SNOOZE: result = await this.handleSnooze(action, alertId); break; case SmartActionType.DISMISS: result = await this.handleDismiss(action, alertId); break; case SmartActionType.MARK_READ: result = await this.handleMarkRead(action, alertId); break; default: console.warn('Unknown action type:', actionType); this.onError?.(`Unknown action type: ${actionType}`); return false; } if (result && this.onSuccess) { this.onSuccess(alertId); } return result; } catch (error) { console.error('Error handling action:', error); this.onError?.(error instanceof Error ? error.message : 'Unknown error'); return false; } } // ============================================================ // Action Handlers // ============================================================ /** * 1. APPROVE_PO - Approve a purchase order */ private async handleApprovePO(action: SmartAction): Promise { const { po_id, tenant_id, amount } = action.metadata || {}; if (!po_id || !tenant_id) { console.error('Missing PO ID or tenant ID'); this.onError?.('Missing required data for PO approval'); return false; } try { const response = await fetch( `/api/v1/tenants/${tenant_id}/procurement/purchase-orders/${po_id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token') || ''}`, }, body: JSON.stringify({ action: 'approve', approved_by: 'current_user', notes: `Approved via alert action${amount ? ` (€${amount})` : ''}`, }), } ); if (response.ok) { // Dispatch success event for real-time updates window.dispatchEvent( new CustomEvent('po:approved', { detail: { po_id, amount }, }) ); return true; } else { const error = await response.text(); console.error('Failed to approve PO:', error); this.onError?.(`Failed to approve PO: ${error}`); return false; } } catch (error) { console.error('Error approving PO:', error); this.onError?.(error instanceof Error ? error.message : 'Network error'); return false; } } /** * 2. REJECT_PO - Reject a purchase order */ private async handleRejectPO(action: SmartAction): Promise { const { po_id, tenant_id, reason } = action.metadata || {}; if (!po_id || !tenant_id) { console.error('Missing PO ID or tenant ID'); this.onError?.('Missing required data for PO rejection'); return false; } try { const response = await fetch( `/api/v1/tenants/${tenant_id}/procurement/purchase-orders/${po_id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token') || ''}`, }, body: JSON.stringify({ action: 'reject', approved_by: 'current_user', notes: reason || 'Rejected via alert action', }), } ); if (response.ok) { window.dispatchEvent( new CustomEvent('po:rejected', { detail: { po_id }, }) ); return true; } else { const error = await response.text(); console.error('Failed to reject PO:', error); this.onError?.(`Failed to reject PO: ${error}`); return false; } } catch (error) { console.error('Error rejecting PO:', error); this.onError?.(error instanceof Error ? error.message : 'Network error'); return false; } } /** * 3. MODIFY_PO - Open PO in edit mode */ private handleModifyPO(action: SmartAction): boolean { const { po_id } = action.metadata || {}; if (!po_id) { console.error('Missing PO ID'); this.onError?.('Missing PO ID for modification'); return false; } // Emit event to open PO modal in edit mode window.dispatchEvent( new CustomEvent('po:open-edit', { detail: { po_id, mode: 'edit' }, }) ); return true; } /** * 3.5. VIEW_PO_DETAILS - Open PO in view mode */ private handleViewPODetails(action: SmartAction): boolean { const { po_id, tenant_id } = action.metadata || {}; if (!po_id) { console.error('Missing PO ID'); this.onError?.('Missing PO ID for viewing details'); return false; } // Emit event to open PO modal in view mode window.dispatchEvent( new CustomEvent('po:open-details', { detail: { po_id, tenant_id, mode: 'view' }, }) ); return true; } /** * 4. CALL_SUPPLIER - Initiate phone call */ private handleCallSupplier(action: SmartAction): boolean { const { phone, name } = action.metadata || {}; if (!phone) { console.error('Missing phone number'); this.onError?.('Missing supplier phone number'); return false; } // Format phone number for tel: protocol (remove spaces, dashes) const cleanPhone = phone.replace(/[\s\-()]/g, ''); // On mobile devices, this will trigger the phone dialer // On desktop, it will use the system's default phone app window.location.href = `tel:${cleanPhone}`; return true; } /** * 5. NAVIGATE - Navigate to a page */ private handleNavigate(action: SmartAction): boolean { const { path, state } = action.metadata || {}; if (!path) { console.error('Missing navigation path'); this.onError?.('Missing navigation path'); return false; } if (this.navigate) { this.navigate(path, { state }); return true; } // Fallback: Use window.location window.location.href = path; return true; } /** * 6. ADJUST_PRODUCTION - Navigate to batch edit with suggested quantity */ private handleAdjustProduction(action: SmartAction): boolean { const { batch_id, suggested_quantity } = action.metadata || {}; if (!batch_id) { console.error('Missing batch ID'); this.onError?.('Missing batch ID for adjustment'); return false; } const path = `/app/operations/production/batches/${batch_id}/edit`; const state = suggested_quantity ? { suggested_quantity } : undefined; if (this.navigate) { this.navigate(path, { state }); return true; } window.location.href = path; return true; } /** * 7. START_PRODUCTION_BATCH - Start a production batch */ private async handleStartProductionBatch(action: SmartAction): Promise { const { batch_id, tenant_id } = action.metadata || {}; if (!batch_id || !tenant_id) { console.error('Missing batch ID or tenant ID'); this.onError?.('Missing required data to start batch'); return false; } try { const response = await fetch( `/api/v1/tenants/${tenant_id}/production/batches/${batch_id}/start`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token') || ''}`, }, body: JSON.stringify({ started_at: new Date().toISOString(), started_by: 'current_user', }), } ); if (response.ok) { window.dispatchEvent( new CustomEvent('batch:started', { detail: { batch_id }, }) ); return true; } else { const error = await response.text(); console.error('Failed to start batch:', error); this.onError?.(`Failed to start batch: ${error}`); return false; } } catch (error) { console.error('Error starting batch:', error); this.onError?.(error instanceof Error ? error.message : 'Network error'); return false; } } /** * 8. NOTIFY_CUSTOMER - Navigate to customer notification page */ private async handleNotifyCustomer(action: SmartAction): Promise { const { customer_name, customer_id, message } = action.metadata || {}; if (!customer_id) { console.error('Missing customer ID'); this.onError?.('Missing customer ID for notification'); return false; } // Navigate to communications page with pre-filled message if (this.navigate) { this.navigate(`/app/communications`, { state: { customer_id, customer_name, pre_message: message, }, }); return true; } return false; } /** * 9. CANCEL_AUTO_ACTION - Cancel pending auto-action */ private async handleCancelAutoAction(action: SmartAction): Promise { const { alert_id, tenant_id, action_type } = action.metadata || {}; if (!alert_id || !tenant_id) { console.error('Missing alert ID or tenant ID'); this.onError?.('Missing required data to cancel auto-action'); return false; } try { const response = await fetch( `/api/v1/tenants/${tenant_id}/alerts/${alert_id}/cancel-auto-action`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token') || ''}`, }, body: JSON.stringify({ action_type, cancelled_by: 'current_user', }), } ); if (response.ok) { window.dispatchEvent( new CustomEvent('auto-action:cancelled', { detail: { alert_id, action_type }, }) ); return true; } else { const error = await response.text(); console.error('Failed to cancel auto-action:', error); this.onError?.(`Failed to cancel auto-action: ${error}`); return false; } } catch (error) { console.error('Failed to cancel auto-action:', error); this.onError?.(error instanceof Error ? error.message : 'Network error'); return false; } } /** * 10. MARK_DELIVERY_RECEIVED - Mark delivery as received and open stock receipt modal */ private async handleMarkDeliveryReceived(action: SmartAction): Promise { const { po_id, delivery_id, tenant_id } = action.metadata || {}; if (!po_id || !tenant_id) { console.error('Missing PO ID or tenant ID'); this.onError?.('Missing required data for delivery receipt'); return false; } // Emit event to open stock receipt modal window.dispatchEvent( new CustomEvent('delivery:mark-received', { detail: { po_id, delivery_id, tenant_id, }, }) ); return true; } /** * 11. COMPLETE_STOCK_RECEIPT - Open stock receipt modal for completion */ private async handleCompleteStockReceipt(action: SmartAction): Promise { const { receipt_id, po_id, tenant_id } = action.metadata || {}; if ((!receipt_id && !po_id) || !tenant_id) { console.error('Missing receipt ID/PO ID or tenant ID'); this.onError?.('Missing required data for stock receipt'); return false; } // Emit event to open stock receipt modal window.dispatchEvent( new CustomEvent('stock-receipt:open', { detail: { receipt_id, po_id, tenant_id, mode: receipt_id ? 'edit' : 'create', }, }) ); return true; } /** * 12. OPEN_REASONING - Show AI reasoning details */ private handleOpenReasoning(action: SmartAction): boolean { const { action_id, po_id, batch_id, reasoning } = action.metadata || {}; // Always emit event to show reasoning modal inline (don't navigate away) // This keeps the user on the dashboard while showing reasoning details window.dispatchEvent( new CustomEvent('reasoning:show', { detail: { action_id, po_id, batch_id, reasoning }, }) ); return true; } /** * 13. SNOOZE - Snooze alert for specified duration */ private async handleSnooze(action: SmartAction, alertId?: string): Promise { const { duration_hours, alert_id } = action.metadata || {}; const finalAlertId = alert_id || alertId; if (!finalAlertId) { console.error('Missing alert ID'); this.onError?.('Missing alert ID for snooze'); return false; } // Emit event to snooze alert window.dispatchEvent( new CustomEvent('alert:snooze', { detail: { alert_id: finalAlertId, duration_hours: duration_hours || 4, // Default 4 hours snoozed_at: new Date().toISOString(), }, }) ); return true; } /** * 14. DISMISS - Dismiss alert */ private async handleDismiss(action: SmartAction, alertId?: string): Promise { const { alert_id } = action.metadata || {}; const finalAlertId = alert_id || alertId; if (!finalAlertId) { console.error('Missing alert ID'); this.onError?.('Missing alert ID for dismiss'); return false; } window.dispatchEvent( new CustomEvent('alert:dismiss', { detail: { alert_id: finalAlertId, dismissed_at: new Date().toISOString(), }, }) ); return true; } /** * 15. MARK_READ - Mark alert as read */ private async handleMarkRead(action: SmartAction, alertId?: string): Promise { const { alert_id } = action.metadata || {}; const finalAlertId = alert_id || alertId; if (!finalAlertId) { console.error('Missing alert ID'); this.onError?.('Missing alert ID for mark read'); return false; } window.dispatchEvent( new CustomEvent('alert:mark-read', { detail: { alert_id: finalAlertId, read_at: new Date().toISOString(), }, }) ); return true; } } // ============================================================ // React Hooks // ============================================================ /** * Hook to get action handler with navigation and callbacks */ export function useSmartActionHandler(callbacks?: { onSuccess?: (alertId?: string) => void; onError?: (error: string) => void; }) { const navigate = useNavigate(); return new SmartActionHandler(navigate, callbacks); } /** * Get action handler without navigation (for use outside React components) */ export function getSmartActionHandler() { return new SmartActionHandler(); } // ============================================================ // Utility Functions // ============================================================ /** * Map smart action variant to Button component variant * * Smart actions use: primary, secondary, tertiary, danger * Button component supports: primary, secondary, outline, ghost, danger, success, warning, gradient */ export function mapActionVariantToButton( actionVariant?: string ): 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'warning' | 'gradient' { switch (actionVariant) { case 'primary': return 'primary'; case 'secondary': return 'secondary'; case 'tertiary': return 'outline'; // Map tertiary to outline for subtle actions case 'danger': return 'danger'; default: return 'outline'; // Default to outline for unknown variants } } /** * Get button variant class name (for use with UI libraries) */ export function getActionButtonClass(variant: string): string { const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors'; switch (variant) { case 'primary': return `${baseClasses} bg-primary text-white hover:bg-primary-dark`; case 'secondary': return `${baseClasses} bg-secondary text-secondary-foreground hover:bg-secondary-dark`; case 'ghost': return `${baseClasses} border border-border text-foreground hover:bg-accent`; case 'danger': return `${baseClasses} bg-error text-white hover:bg-error-dark`; default: return `${baseClasses} bg-accent text-accent-foreground hover:bg-accent-dark`; } }