function getWorkflowStatusBadge(status) {
const statusConfig = {
'draft': { icon: 'file-earmark', label: 'Draft' },
'submitted': { icon: 'send', label: 'Submitted' },
'pending_approval': { icon: 'clock', label: 'Pending Approval' },
'approved': { icon: 'check-circle', label: 'Approved' },
'rejected': { icon: 'x-circle', label: 'Rejected' },
'completed': { icon: 'check-all', label: 'Completed' },
'cancelled': { icon: 'slash-circle', label: 'Cancelled' }
};
const config = statusConfig[status] || statusConfig['draft'];
return `
${config.label}
`;
}
Submitted by John Doe
Jan 15, 2025 10:30 AM
Approved by Jane Smith
"Budget approved for Q1"
Jan 15, 2025 2:45 PM
Assigned to Bob Johnson (Finance Director)
Waiting for 2 hours
/**
* WorkflowWidget - Reusable workflow component
*
* Usage:
* const widget = new WorkflowWidget('myContainer', {
* instanceId: 'workflow-123',
* apiBaseUrl: '/api/v1/workflows'
* });
*/
class WorkflowWidget {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.options = {
apiBaseUrl: options.apiBaseUrl || '/api/v1/workflows',
instanceId: options.instanceId,
showTimeline: options.showTimeline !== false,
showProgress: options.showProgress !== false,
showActions: options.showActions !== false,
onAction: options.onAction || this.defaultActionHandler.bind(this)
};
this.instance = null;
this.availableActions = [];
if (this.options.instanceId) {
this.load();
}
}
async load() {
try {
// Load workflow instance
const response = await fetch(
`${this.options.apiBaseUrl}/instances/${this.options.instanceId}`
);
this.instance = await response.json();
// Load available actions
const actionsResponse = await fetch(
`${this.options.apiBaseUrl}/instances/${this.options.instanceId}/available-actions`
);
this.availableActions = await actionsResponse.json();
// Load history
const historyResponse = await fetch(
`${this.options.apiBaseUrl}/instances/${this.options.instanceId}/history`
);
this.history = await historyResponse.json();
this.render();
} catch (error) {
console.error('Error loading workflow:', error);
this.renderError(error);
}
}
render() {
if (!this.instance) return;
let html = '';
this.container.innerHTML = html;
this.attachEventListeners();
}
renderHeader() {
return `
${this.instance.entity_type} #${this.instance.entity_id}
${this.getStatusBadge(this.instance.status)}
`;
}
renderActions() {
let html = '';
this.availableActions.forEach(action => {
html += `
`;
});
html += '';
return html;
}
renderTimeline() {
if (!this.history || this.history.length === 0) return '';
let html = '';
this.history.forEach((action, index) => {
const statusClass = this.getTimelineStatusClass(action.action);
html += `
${action.to_state.name}
${action.action} by ${action.performed_by_user.username}
${action.comment ? `"${action.comment}"
` : ''}
${new Date(action.action_date).toLocaleString()}
${action.action}
`;
});
html += '';
return html;
}
getStatusBadge(status) {
return `
${status.replace('_', ' ')}
`;
}
getStatusIcon(status) {
const icons = {
'draft': 'file-earmark',
'submitted': 'send',
'pending_approval': 'clock',
'approved': 'check-circle',
'rejected': 'x-circle',
'completed': 'check-all',
'cancelled': 'slash-circle'
};
return icons[status] || 'circle';
}
getActionIcon(action) {
const icons = {
'submit': 'send',
'approve': 'check-circle',
'reject': 'x-circle',
'cancel': 'slash-circle',
'reassign': 'person-check',
'hold': 'pause-circle',
'resume': 'play-circle',
'revise': 'pencil-square'
};
return icons[action] || 'circle';
}
attachEventListeners() {
// Attach click handlers to action buttons
this.container.querySelectorAll('[data-action]').forEach(btn => {
btn.addEventListener('click', (e) => {
const action = e.currentTarget.dataset.action;
const transitionId = e.currentTarget.dataset.transitionId;
this.handleAction(action, transitionId);
});
});
}
async handleAction(action, transitionId) {
const actionData = this.availableActions.find(a => a.transition_id === transitionId);
// Show comment modal if required
if (actionData && actionData.requires_comment) {
this.showCommentModal(action, transitionId, actionData);
} else {
await this.executeAction(action, transitionId);
}
}
async executeAction(action, transitionId, comment = null) {
try {
const response = await fetch(
`${this.options.apiBaseUrl}/instances/${this.options.instanceId}/${action}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment })
}
);
if (response.ok) {
await this.load(); // Reload
this.options.onAction(action, true);
} else {
throw new Error('Action failed');
}
} catch (error) {
console.error('Error executing action:', error);
this.options.onAction(action, false, error);
}
}
defaultActionHandler(action, success, error) {
if (success) {
alert(`Action "${action}" completed successfully!`);
} else {
alert(`Action "${action}" failed: ${error}`);
}
}
}