fix(dashboard): Fix production plans and reasoning modal issues
1. **Fix "Sin Plan" (No Production Planned)**: - Updated BASE_REFERENCE_DATE from Nov 25 to Nov 27 to match current date - Production batches were being filtered out because they were dated for Nov 25 - The get_todays_batches() method filters for batches scheduled TODAY 2. **Fix "Ver razonamiento" Button Not Opening Modal**: - Changed handleOpenReasoning() to always emit 'reasoning:show' event - Previous logic tried to navigate to PO/batch detail pages instead - Now keeps user on dashboard and shows reasoning modal inline - Passes all metadata (action_id, po_id, batch_id, reasoning) in event This ensures production progress shows correctly and users can view AI reasoning without leaving the dashboard. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
678
frontend/src/utils/smartActionHandlers.ts
Normal file
678
frontend/src/utils/smartActionHandlers.ts
Normal file
@@ -0,0 +1,678 @@
|
||||
/**
|
||||
* Smart Action Handlers - Complete Implementation
|
||||
* Handles execution of all 14 smart action types from enriched alerts
|
||||
*
|
||||
* NO PLACEHOLDERS - All action types fully implemented
|
||||
*/
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// ============================================================
|
||||
// Types (matching backend SmartActionType enum)
|
||||
// ============================================================
|
||||
|
||||
export enum SmartActionType {
|
||||
APPROVE_PO = 'approve_po',
|
||||
REJECT_PO = 'reject_po',
|
||||
MODIFY_PO = 'modify_po',
|
||||
CALL_SUPPLIER = 'call_supplier',
|
||||
NAVIGATE = 'navigate',
|
||||
ADJUST_PRODUCTION = 'adjust_production',
|
||||
START_PRODUCTION_BATCH = 'start_production_batch',
|
||||
NOTIFY_CUSTOMER = 'notify_customer',
|
||||
CANCEL_AUTO_ACTION = 'cancel_auto_action',
|
||||
MARK_DELIVERY_RECEIVED = 'mark_delivery_received',
|
||||
COMPLETE_STOCK_RECEIPT = 'complete_stock_receipt',
|
||||
OPEN_REASONING = 'open_reasoning',
|
||||
SNOOZE = 'snooze',
|
||||
DISMISS = 'dismiss',
|
||||
MARK_READ = 'mark_read',
|
||||
}
|
||||
|
||||
export interface SmartAction {
|
||||
label: string;
|
||||
type: SmartActionType;
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
metadata?: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
disabled_reason?: string;
|
||||
estimated_time_minutes?: number;
|
||||
consequence?: string;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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;
|
||||
|
||||
switch (action.type) {
|
||||
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.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:', action.type);
|
||||
this.onError?.(`Unknown action type: ${action.type}`);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user