feat: Add 9 new features for enhanced UX and security
Quick Wins: - Feature 1: Ticket linking in comments (#123456789 auto-links) - Feature 6: Checkbox click area fix (click anywhere in cell) - Feature 7: User groups display in settings modal UI Enhancements: - Feature 4: Collapsible sidebar with localStorage persistence - Feature 5: Inline ticket preview popup on hover (300ms delay) - Feature 2: Mobile responsive improvements (44px touch targets, iOS zoom fix) Major Features: - Feature 3: Kanban card view with status columns (toggle with localStorage) - Feature 9: API key generation admin panel (/admin/api-keys) - Feature 8: Ticket visibility levels (public/internal/confidential) New files: - views/admin/ApiKeysView.php - api/generate_api_key.php - api/revoke_api_key.php - migrations/008_ticket_visibility.sql Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -261,8 +261,8 @@ class TicketModel {
|
||||
// Generate ticket ID (9-digit format with leading zeros)
|
||||
$ticket_id = sprintf('%09d', mt_rand(1, 999999999));
|
||||
|
||||
$sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$sql = "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, created_by, visibility, visibility_groups)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
|
||||
@@ -271,9 +271,22 @@ class TicketModel {
|
||||
$priority = $ticketData['priority'] ?? '4';
|
||||
$category = $ticketData['category'] ?? 'General';
|
||||
$type = $ticketData['type'] ?? 'Issue';
|
||||
$visibility = $ticketData['visibility'] ?? 'public';
|
||||
$visibilityGroups = $ticketData['visibility_groups'] ?? null;
|
||||
|
||||
// Validate visibility
|
||||
$allowedVisibilities = ['public', 'internal', 'confidential'];
|
||||
if (!in_array($visibility, $allowedVisibilities)) {
|
||||
$visibility = 'public';
|
||||
}
|
||||
|
||||
// Clear visibility_groups if not internal
|
||||
if ($visibility !== 'internal') {
|
||||
$visibilityGroups = null;
|
||||
}
|
||||
|
||||
$stmt->bind_param(
|
||||
"sssssssi",
|
||||
"sssssssiss",
|
||||
$ticket_id,
|
||||
$ticketData['title'],
|
||||
$ticketData['description'],
|
||||
@@ -281,7 +294,9 @@ class TicketModel {
|
||||
$priority,
|
||||
$category,
|
||||
$type,
|
||||
$createdBy
|
||||
$createdBy,
|
||||
$visibility,
|
||||
$visibilityGroups
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
@@ -407,4 +422,123 @@ class TicketModel {
|
||||
$stmt->close();
|
||||
return $tickets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user can access a ticket based on visibility settings
|
||||
*
|
||||
* @param array $ticket The ticket data
|
||||
* @param array $user The user data (must include user_id, is_admin, groups)
|
||||
* @return bool True if user can access the ticket
|
||||
*/
|
||||
public function canUserAccessTicket($ticket, $user) {
|
||||
// Admins can access all tickets
|
||||
if (!empty($user['is_admin'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$visibility = $ticket['visibility'] ?? 'public';
|
||||
|
||||
// Public tickets are accessible to all authenticated users
|
||||
if ($visibility === 'public') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Confidential tickets: only creator, assignee, and admins
|
||||
if ($visibility === 'confidential') {
|
||||
$userId = $user['user_id'] ?? null;
|
||||
return ($ticket['created_by'] == $userId || $ticket['assigned_to'] == $userId);
|
||||
}
|
||||
|
||||
// Internal tickets: check if user is in any of the allowed groups
|
||||
if ($visibility === 'internal') {
|
||||
$allowedGroups = array_filter(array_map('trim', explode(',', $ticket['visibility_groups'] ?? '')));
|
||||
if (empty($allowedGroups)) {
|
||||
return false; // No groups specified means no access
|
||||
}
|
||||
|
||||
$userGroups = array_filter(array_map('trim', explode(',', $user['groups'] ?? '')));
|
||||
// Check if any user group matches any allowed group
|
||||
return !empty(array_intersect($userGroups, $allowedGroups));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build visibility filter SQL for queries
|
||||
*
|
||||
* @param array $user The current user
|
||||
* @return array ['sql' => string, 'params' => array, 'types' => string]
|
||||
*/
|
||||
public function getVisibilityFilter($user) {
|
||||
// Admins see all tickets
|
||||
if (!empty($user['is_admin'])) {
|
||||
return ['sql' => '1=1', 'params' => [], 'types' => ''];
|
||||
}
|
||||
|
||||
$userId = $user['user_id'] ?? 0;
|
||||
$userGroups = array_filter(array_map('trim', explode(',', $user['groups'] ?? '')));
|
||||
|
||||
// Build the visibility filter
|
||||
// 1. Public tickets
|
||||
// 2. Confidential tickets where user is creator or assignee
|
||||
// 3. Internal tickets where user's groups overlap with visibility_groups
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
$types = '';
|
||||
|
||||
// Public visibility
|
||||
$conditions[] = "(t.visibility = 'public' OR t.visibility IS NULL)";
|
||||
|
||||
// Confidential - user is creator or assignee
|
||||
$conditions[] = "(t.visibility = 'confidential' AND (t.created_by = ? OR t.assigned_to = ?))";
|
||||
$params[] = $userId;
|
||||
$params[] = $userId;
|
||||
$types .= 'ii';
|
||||
|
||||
// Internal - check group membership
|
||||
if (!empty($userGroups)) {
|
||||
$groupConditions = [];
|
||||
foreach ($userGroups as $group) {
|
||||
$groupConditions[] = "FIND_IN_SET(?, REPLACE(t.visibility_groups, ' ', ''))";
|
||||
$params[] = $group;
|
||||
$types .= 's';
|
||||
}
|
||||
$conditions[] = "(t.visibility = 'internal' AND (" . implode(' OR ', $groupConditions) . "))";
|
||||
}
|
||||
|
||||
return [
|
||||
'sql' => '(' . implode(' OR ', $conditions) . ')',
|
||||
'params' => $params,
|
||||
'types' => $types
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ticket visibility settings
|
||||
*
|
||||
* @param int $ticketId
|
||||
* @param string $visibility ('public', 'internal', 'confidential')
|
||||
* @param string|null $visibilityGroups Comma-separated group names for 'internal' visibility
|
||||
* @param int $updatedBy User ID
|
||||
* @return bool
|
||||
*/
|
||||
public function updateVisibility($ticketId, $visibility, $visibilityGroups, $updatedBy) {
|
||||
$allowedVisibilities = ['public', 'internal', 'confidential'];
|
||||
if (!in_array($visibility, $allowedVisibilities)) {
|
||||
$visibility = 'public';
|
||||
}
|
||||
|
||||
// Clear visibility_groups if not internal
|
||||
if ($visibility !== 'internal') {
|
||||
$visibilityGroups = null;
|
||||
}
|
||||
|
||||
$sql = "UPDATE tickets SET visibility = ?, visibility_groups = ?, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?";
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
$stmt->bind_param("ssii", $visibility, $visibilityGroups, $updatedBy, $ticketId);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user