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:
poduck
2025-11-28 05:44:39 -05:00
parent c400e8a722
commit 9dabb0cb83
9 changed files with 253 additions and 28 deletions

View File

@@ -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(),
}

View File

@@ -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):
"""

View File

@@ -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}")

View File

@@ -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