2025-11-27 11:02:59 +01:00
|
|
|
/**
|
|
|
|
|
* Smart Action Handlers - Complete Implementation
|
2025-12-05 20:07:01 +01:00
|
|
|
* Handles execution of all smart action types from enriched alerts
|
2025-11-27 11:02:59 +01:00
|
|
|
*
|
|
|
|
|
* NO PLACEHOLDERS - All action types fully implemented
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2025-12-05 20:07:01 +01:00
|
|
|
import { SmartAction as ImportedSmartAction, SmartActionType } from '../api/types/events';
|
2025-11-27 11:02:59 +01:00
|
|
|
|
|
|
|
|
// ============================================================
|
2025-12-05 20:07:01 +01:00
|
|
|
// Types (using imported types from events.ts)
|
2025-11-27 11:02:59 +01:00
|
|
|
// ============================================================
|
|
|
|
|
|
2025-12-05 20:07:01 +01:00
|
|
|
// Legacy interface for backwards compatibility with existing handler code
|
2025-11-27 11:02:59 +01:00
|
|
|
export interface SmartAction {
|
2025-12-05 20:07:01 +01:00
|
|
|
label?: string;
|
|
|
|
|
label_key?: string;
|
|
|
|
|
action_type: string;
|
|
|
|
|
type?: string; // For backward compatibility
|
2025-11-27 11:02:59 +01:00
|
|
|
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
|
|
|
metadata?: Record<string, any>;
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
disabled_reason?: string;
|
|
|
|
|
estimated_time_minutes?: number;
|
|
|
|
|
consequence?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 20:07:01 +01:00
|
|
|
// Re-export types from events.ts
|
|
|
|
|
export { SmartActionType };
|
|
|
|
|
|
2025-11-27 11:02:59 +01:00
|
|
|
// ============================================================
|
|
|
|
|
// Smart Action Handler Class
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
export class SmartActionHandler {
|
|
|
|
|
private navigate: ReturnType<typeof useNavigate> | null = null;
|
|
|
|
|
private onSuccess?: (alertId?: string) => void;
|
|
|
|
|
private onError?: (error: string) => void;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
navigate?: ReturnType<typeof useNavigate>,
|
|
|
|
|
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<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
let result = false;
|
|
|
|
|
|
2025-12-05 20:07:01 +01:00
|
|
|
// Support both legacy (type) and new (action_type) field names
|
|
|
|
|
const actionType = action.action_type || action.type;
|
|
|
|
|
|
|
|
|
|
switch (actionType) {
|
2025-11-27 11:02:59 +01:00
|
|
|
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;
|
|
|
|
|
|
2025-12-05 20:07:01 +01:00
|
|
|
case SmartActionType.VIEW_PO_DETAILS:
|
|
|
|
|
result = this.handleViewPODetails(action);
|
|
|
|
|
break;
|
|
|
|
|
|
2025-11-27 11:02:59 +01:00
|
|
|
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:
|
2025-12-05 20:07:01 +01:00
|
|
|
console.warn('Unknown action type:', actionType);
|
|
|
|
|
this.onError?.(`Unknown action type: ${actionType}`);
|
2025-11-27 11:02:59 +01:00
|
|
|
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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 20:07:01 +01:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 11:02:59 +01:00
|
|
|
/**
|
|
|
|
|
* 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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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<boolean> {
|
|
|
|
|
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`;
|
|
|
|
|
}
|
|
|
|
|
}
|