Feature 3: Implement Status Transitions with Workflow Validation

Add comprehensive workflow management system for ticket status transitions:

- Created WorkflowModel.php for managing status transition rules
- Updated TicketController.php to load allowed transitions for each ticket
- Modified TicketView.php to display dynamic status dropdown with only allowed transitions
- Enhanced api/update_ticket.php with server-side workflow validation
- Added updateTicketStatus() JavaScript function for client-side status changes
- Included CSS styling for status select dropdown with color-coded states
- Transitions can require comments or admin privileges
- Status changes are validated against status_transitions table

This feature enforces proper ticket workflows and prevents invalid status changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 18:57:23 -05:00
parent 99e60795c9
commit 683420cdb9
6 changed files with 310 additions and 13 deletions

View File

@@ -515,4 +515,59 @@ body.dark-mode .timeline-content {
--border-color: #444;
--text-muted: #a0aec0;
--text-secondary: #cbd5e0;
}
}
/* Status select dropdown */
.status-select {
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
text-transform: uppercase;
font-size: 0.9em;
letter-spacing: 0.5px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
}
.status-select:hover {
opacity: 0.9;
border-color: rgba(255, 255, 255, 0.3);
}
.status-select:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.5);
}
/* Status colors for dropdown */
.status-select.status-open {
background-color: var(--status-open) !important;
color: white !important;
}
.status-select.status-in-progress {
background-color: var(--status-in-progress) !important;
color: #212529 !important;
}
.status-select.status-closed {
background-color: var(--status-closed) !important;
color: white !important;
}
.status-select.status-resolved {
background-color: #28a745 !important;
color: white !important;
}
/* Dropdown options inherit colors */
.status-select option {
background-color: var(--bg-primary);
color: var(--text-primary);
padding: 8px;
}
body.dark-mode .status-select option {
background-color: #2d3748;
color: #f7fafc;
}

View File

@@ -267,6 +267,98 @@ function handleAssignmentChange() {
});
}
function updateTicketStatus() {
const statusSelect = document.getElementById('statusSelect');
const selectedOption = statusSelect.options[statusSelect.selectedIndex];
const newStatus = selectedOption.value;
const requiresComment = selectedOption.dataset.requiresComment === '1';
const requiresAdmin = selectedOption.dataset.requiresAdmin === '1';
// Check if transitioning to the same status (current)
if (selectedOption.text.includes('(current)')) {
return; // No change needed
}
// Warn if comment is required
if (requiresComment) {
const proceed = confirm(`This status change requires a comment. Please add a comment explaining the reason for this transition.\n\nProceed with status change to "${newStatus}"?`);
if (!proceed) {
// Reset to current status
statusSelect.selectedIndex = 0;
return;
}
}
// Extract ticket ID
let ticketId;
if (window.location.href.includes('?id=')) {
ticketId = window.location.href.split('id=')[1];
} else {
const matches = window.location.pathname.match(/\/ticket\/(\d+)/);
ticketId = matches ? matches[1] : null;
}
if (!ticketId) {
console.error('Could not determine ticket ID');
statusSelect.selectedIndex = 0;
return;
}
// Update status via API
fetch('/api/update_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ticket_id: ticketId,
status: newStatus
})
})
.then(response => {
if (!response.ok) {
return response.text().then(text => {
console.error('Server response:', text);
throw new Error('Network response was not ok');
});
}
return response.json();
})
.then(data => {
if (data.success) {
// Update the dropdown to show new status as current
const newClass = 'status-' + newStatus.toLowerCase().replace(/ /g, '-');
statusSelect.className = 'editable status-select ' + newClass;
// Update the selected option text to show as current
selectedOption.text = newStatus + ' (current)';
// Move the selected option to the top
statusSelect.remove(statusSelect.selectedIndex);
statusSelect.insertBefore(selectedOption, statusSelect.firstChild);
statusSelect.selectedIndex = 0;
console.log('Status updated successfully to:', newStatus);
// Reload page to refresh activity timeline
setTimeout(() => {
window.location.reload();
}, 500);
} else {
console.error('Error updating status:', data.error || 'Unknown error');
alert('Error updating status: ' + (data.error || 'Unknown error'));
// Reset to current status
statusSelect.selectedIndex = 0;
}
})
.catch(error => {
console.error('Error updating status:', error);
alert('Error updating status: ' + error.message);
// Reset to current status
statusSelect.selectedIndex = 0;
});
}
function showTab(tabName) {
// Hide all tab contents
const descriptionTab = document.getElementById('description-tab');