Add Ticket Assignment feature (Feature 2)

- Add assigned_to column support in TicketModel with assignTicket() and unassignTicket() methods
- Create assign_ticket.php API endpoint for assignment operations
- Update TicketController to load user list from UserModel
- Add assignment dropdown UI in TicketView
- Add JavaScript handler for assignment changes
- Integrate with audit log for assignment tracking

Users can now assign tickets to team members via dropdown selector.

🤖 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:36:34 -05:00
parent f9629f60b6
commit 99e60795c9
5 changed files with 135 additions and 4 deletions

42
api/assign_ticket.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
session_start();
require_once '../config/db.php';
require_once '../models/TicketModel.php';
require_once '../models/AuditLogModel.php';
header('Content-Type: application/json');
// Check authentication
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Not authenticated']);
exit;
}
// Get request data
$data = json_decode(file_get_contents('php://input'), true);
$ticketId = $data['ticket_id'] ?? null;
$assignedTo = $data['assigned_to'] ?? null;
if (!$ticketId) {
echo json_encode(['success' => false, 'error' => 'Ticket ID required']);
exit;
}
$ticketModel = new TicketModel($conn);
$auditLogModel = new AuditLogModel($conn);
if ($assignedTo === null || $assignedTo === '') {
// Unassign ticket
$success = $ticketModel->unassignTicket($ticketId, $_SESSION['user_id']);
if ($success) {
$auditLogModel->log($_SESSION['user_id'], 'unassign', 'ticket', $ticketId);
}
} else {
// Assign ticket
$success = $ticketModel->assignTicket($ticketId, $assignedTo, $_SESSION['user_id']);
if ($success) {
$auditLogModel->log($_SESSION['user_id'], 'assign', 'ticket', $ticketId, ['assigned_to' => $assignedTo]);
}
}
echo json_encode(['success' => $success]);

View File

@@ -213,7 +213,7 @@ function toggleMarkdownMode() {
document.addEventListener('DOMContentLoaded', function() {
// Show description tab by default
showTab('description');
// Auto-resize the description textarea to fit content
const descriptionTextarea = document.querySelector('textarea[data-field="description"]');
if (descriptionTextarea) {
@@ -223,15 +223,50 @@ document.addEventListener('DOMContentLoaded', function() {
// Set the height to match the scrollHeight
descriptionTextarea.style.height = descriptionTextarea.scrollHeight + 'px';
}
// Initial resize
autoResizeTextarea();
// Resize on input when in edit mode
descriptionTextarea.addEventListener('input', autoResizeTextarea);
}
// Initialize assignment handling
handleAssignmentChange();
});
/**
* Handle ticket assignment dropdown changes
*/
function handleAssignmentChange() {
const assignedToSelect = document.getElementById('assignedToSelect');
if (!assignedToSelect) return;
assignedToSelect.addEventListener('change', function() {
const ticketId = window.ticketData.id;
const assignedTo = this.value || null;
fetch('/api/assign_ticket.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ticket_id: ticketId, assigned_to: assignedTo })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
alert('Error updating assignment');
console.error(data.error);
} else {
console.log('Assignment updated successfully');
}
})
.catch(error => {
console.error('Error updating assignment:', error);
alert('Error updating assignment: ' + error.message);
});
});
}
function showTab(tabName) {
// Hide all tab contents
const descriptionTab = document.getElementById('description-tab');

View File

@@ -3,17 +3,20 @@
require_once dirname(__DIR__) . '/models/TicketModel.php';
require_once dirname(__DIR__) . '/models/CommentModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
require_once dirname(__DIR__) . '/models/UserModel.php';
class TicketController {
private $ticketModel;
private $commentModel;
private $auditLogModel;
private $userModel;
private $envVars;
public function __construct($conn) {
$this->ticketModel = new TicketModel($conn);
$this->commentModel = new CommentModel($conn);
$this->auditLogModel = new AuditLogModel($conn);
$this->userModel = new UserModel($conn);
// Load environment variables for Discord webhook
$envPath = dirname(__DIR__) . '/.env';
@@ -61,6 +64,9 @@ class TicketController {
// Get timeline for this ticket
$timeline = $this->auditLogModel->getTicketTimeline($id);
// Get all users for assignment dropdown
$allUsers = $this->userModel->getAllUsers();
// Load the view
include dirname(__DIR__) . '/views/TicketView.php';
}

View File

@@ -11,10 +11,13 @@ class TicketModel {
u_created.username as creator_username,
u_created.display_name as creator_display_name,
u_updated.username as updater_username,
u_updated.display_name as updater_display_name
u_updated.display_name as updater_display_name,
u_assigned.username as assigned_username,
u_assigned.display_name as assigned_display_name
FROM tickets t
LEFT JOIN users u_created ON t.created_by = u_created.user_id
LEFT JOIN users u_updated ON t.updated_by = u_updated.user_id
LEFT JOIN users u_assigned ON t.assigned_to = u_assigned.user_id
WHERE t.ticket_id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $id);
@@ -283,4 +286,37 @@ class TicketModel {
];
}
}
/**
* Assign ticket to a user
*
* @param int $ticketId Ticket ID
* @param int $userId User ID to assign to
* @param int $assignedBy User ID performing the assignment
* @return bool Success status
*/
public function assignTicket($ticketId, $userId, $assignedBy) {
$sql = "UPDATE tickets SET assigned_to = ?, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("iii", $userId, $assignedBy, $ticketId);
$result = $stmt->execute();
$stmt->close();
return $result;
}
/**
* Unassign ticket (set assigned_to to NULL)
*
* @param int $ticketId Ticket ID
* @param int $updatedBy User ID performing the unassignment
* @return bool Success status
*/
public function unassignTicket($ticketId, $updatedBy) {
$sql = "UPDATE tickets SET assigned_to = NULL, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("ii", $updatedBy, $ticketId);
$result = $stmt->execute();
$stmt->close();
return $result;
}
}

View File

@@ -167,6 +167,18 @@ function formatDetails($details, $actionType) {
}
?>
</div>
<div class="ticket-assignment" style="margin-top: 0.5rem;">
<label style="font-weight: 500; margin-right: 0.5rem;">Assigned to:</label>
<select id="assignedToSelect" class="assignment-select" style="padding: 0.25rem 0.5rem; border-radius: 4px; border: 1px solid var(--border-color, #ddd);">
<option value="">Unassigned</option>
<?php foreach ($allUsers as $user): ?>
<option value="<?php echo $user['user_id']; ?>"
<?php echo ($ticket['assigned_to'] == $user['user_id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="header-controls">
<div class="status-priority-group">