feat: Add real-time ticket updates via WebSocket and staff permission control
WebSocket Updates: - Create useTicketWebSocket hook for real-time ticket list updates - Hook invalidates React Query cache when tickets are created/updated - Shows toast notifications for new tickets and comments - Auto-reconnect with exponential backoff Staff Permissions: - Add can_access_tickets() method to User model - Owners and managers always have ticket access - Staff members need explicit can_access_tickets permission - Update Sidebar to conditionally show Tickets menu based on permission - Add can_access_tickets to API user response Backend Updates: - Update ticket signals to broadcast updates to all relevant users - Check ticket access permission in views 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,7 @@ def current_user_view(request):
|
||||
'business_subdomain': business_subdomain,
|
||||
'permissions': user.permissions,
|
||||
'can_invite_staff': user.can_invite_staff(),
|
||||
'can_access_tickets': user.can_access_tickets(),
|
||||
}
|
||||
|
||||
return Response(user_data, status=status.HTTP_200_OK)
|
||||
@@ -212,6 +213,7 @@ def _get_user_data(user):
|
||||
'business_subdomain': business_subdomain,
|
||||
'permissions': user.permissions,
|
||||
'can_invite_staff': user.can_invite_staff(),
|
||||
'can_access_tickets': user.can_access_tickets(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -143,6 +143,22 @@ class User(AbstractUser):
|
||||
if self.role == self.Role.TENANT_MANAGER:
|
||||
return self.permissions.get('can_invite_staff', False)
|
||||
return False
|
||||
|
||||
def can_access_tickets(self):
|
||||
"""Check if user can access the ticket system"""
|
||||
# Platform users can always access
|
||||
if self.is_platform_user():
|
||||
return True
|
||||
# Owners and managers can always access
|
||||
if self.role in [self.Role.TENANT_OWNER, self.Role.TENANT_MANAGER]:
|
||||
return True
|
||||
# Staff can access if granted permission (default: False)
|
||||
if self.role == self.Role.TENANT_STAFF:
|
||||
return self.permissions.get('can_access_tickets', False)
|
||||
# Customers can create tickets
|
||||
if self.role == self.Role.CUSTOMER:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_accessible_tenants(self):
|
||||
"""
|
||||
|
||||
@@ -190,32 +190,46 @@ def _handle_ticket_creation(ticket):
|
||||
def _handle_ticket_update(ticket):
|
||||
"""Send notifications when a ticket is updated."""
|
||||
try:
|
||||
update_message = {
|
||||
"type": "ticket_update",
|
||||
"ticket_id": ticket.id,
|
||||
"subject": ticket.subject,
|
||||
"status": ticket.status,
|
||||
"priority": ticket.priority,
|
||||
"ticket_type": ticket.ticket_type,
|
||||
"message": f"Ticket #{ticket.id} '{ticket.subject}' updated. Status: {ticket.status}"
|
||||
}
|
||||
|
||||
# For PLATFORM tickets, notify platform support team
|
||||
if ticket.ticket_type == Ticket.TicketType.PLATFORM:
|
||||
platform_team = get_platform_support_team()
|
||||
for member in platform_team:
|
||||
send_websocket_notification(f"user_{member.id}", update_message)
|
||||
|
||||
# For tenant tickets, notify tenant managers and staff with ticket access
|
||||
if ticket.tenant:
|
||||
tenant_managers = get_tenant_managers(ticket.tenant)
|
||||
for manager in tenant_managers:
|
||||
send_websocket_notification(f"user_{manager.id}", update_message)
|
||||
|
||||
# Notify creator (if different from managers already notified)
|
||||
if ticket.creator:
|
||||
send_websocket_notification(f"user_{ticket.creator.id}", update_message)
|
||||
|
||||
# Notify assignee if one exists
|
||||
if not ticket.assignee:
|
||||
return
|
||||
if ticket.assignee:
|
||||
# Create Notification object for the assignee
|
||||
create_notification(
|
||||
recipient=ticket.assignee,
|
||||
actor=ticket.creator,
|
||||
verb=f"Ticket #{ticket.id} '{ticket.subject}' was updated.",
|
||||
action_object=ticket,
|
||||
target=ticket,
|
||||
data={'ticket_id': ticket.id, 'subject': ticket.subject, 'status': ticket.status}
|
||||
)
|
||||
# Send WebSocket to assignee
|
||||
send_websocket_notification(f"user_{ticket.assignee.id}", update_message)
|
||||
|
||||
# Create Notification object for the assignee
|
||||
create_notification(
|
||||
recipient=ticket.assignee,
|
||||
actor=ticket.creator,
|
||||
verb=f"Ticket #{ticket.id} '{ticket.subject}' was updated.",
|
||||
action_object=ticket,
|
||||
target=ticket,
|
||||
data={'ticket_id': ticket.id, 'subject': ticket.subject, 'status': ticket.status}
|
||||
)
|
||||
|
||||
# Send WebSocket message to assignee's personal channel
|
||||
send_websocket_notification(
|
||||
f"user_{ticket.assignee.id}",
|
||||
{
|
||||
"type": "ticket_update",
|
||||
"ticket_id": ticket.id,
|
||||
"subject": ticket.subject,
|
||||
"status": ticket.status,
|
||||
"assignee_id": str(ticket.assignee.id),
|
||||
"message": f"Ticket #{ticket.id} '{ticket.subject}' updated. Status: {ticket.status}"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling ticket update for ticket {ticket.id}: {e}")
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ class IsTenantUser(IsAuthenticated):
|
||||
# Platform admins can do anything
|
||||
if is_platform_admin(request.user):
|
||||
return True
|
||||
# Check if user has ticket access permission
|
||||
if hasattr(request.user, 'can_access_tickets') and not request.user.can_access_tickets():
|
||||
return False
|
||||
# Tenant users can only access their own tenant's data
|
||||
return hasattr(request.user, 'tenant') and request.user.tenant is not None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user