feat: Implement staff invitation system with role-based permissions

- Add StaffInvitation model with token-based 7-day expiration
- Create invitation API endpoints (create, cancel, resend, accept, decline)
- Add permissions JSONField to User model for granular access control
- Implement frontend invite modal with role-specific permissions:
  - Manager: can_invite_staff, can_manage_resources, can_manage_services,
    can_view_reports, can_access_settings, can_refund_payments
  - Staff: can_view_all_schedules, can_manage_own_appointments
- Add edit staff modal with permissions management and deactivate option
- Create AcceptInvitePage for invitation acceptance flow
- Add active/inactive staff separation with collapsible section
- Auto-create bookable resource when configured at invite time
- Remove Quick Add Appointment from dashboard

🤖 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 02:03:48 -05:00
parent b10426fbdb
commit 83815fcb34
15 changed files with 2477 additions and 181 deletions

View File

@@ -282,12 +282,17 @@ class ServiceViewSet(viewsets.ModelViewSet):
return Response({'status': 'ok', 'updated': len(order)})
class StaffViewSet(viewsets.ReadOnlyModelViewSet):
class StaffViewSet(viewsets.ModelViewSet):
"""
API endpoint for listing staff members (Users who can be assigned to resources).
API endpoint for managing staff members (Users who can be assigned to resources).
Staff members are Users with roles: TENANT_OWNER, TENANT_MANAGER, TENANT_STAFF.
This endpoint is read-only for assigning staff to resources.
Supports:
- GET /api/staff/ - List staff members
- GET /api/staff/{id}/ - Get staff member details
- PATCH /api/staff/{id}/ - Update staff member (is_active, permissions)
- POST /api/staff/{id}/toggle_active/ - Toggle active status
"""
serializer_class = StaffSerializer
# TODO: Re-enable authentication for production
@@ -297,6 +302,10 @@ class StaffViewSet(viewsets.ReadOnlyModelViewSet):
ordering_fields = ['email', 'first_name', 'last_name']
ordering = ['first_name', 'last_name']
# Disable create and delete - staff are managed via invitations
# Note: 'post' is needed for custom actions like toggle_active
http_method_names = ['get', 'patch', 'post', 'head', 'options']
def get_queryset(self):
"""
Return staff members for the current tenant.
@@ -305,11 +314,17 @@ class StaffViewSet(viewsets.ReadOnlyModelViewSet):
"""
from django.db.models import Q
# Include inactive staff for listing (so admins can reactivate them)
show_inactive = self.request.query_params.get('show_inactive', 'true')
queryset = User.objects.filter(
Q(role=User.Role.TENANT_OWNER) |
Q(role=User.Role.TENANT_MANAGER) |
Q(role=User.Role.TENANT_STAFF)
).filter(is_active=True)
)
if show_inactive.lower() != 'true':
queryset = queryset.filter(is_active=True)
# Filter by tenant if user is authenticated and has a tenant
# TODO: Re-enable this when authentication is enabled
@@ -326,3 +341,55 @@ class StaffViewSet(viewsets.ReadOnlyModelViewSet):
)
return queryset
def partial_update(self, request, *args, **kwargs):
"""
Update staff member.
Allowed fields: is_active, permissions
Owners can edit any staff member.
Managers can only edit staff (not other managers or owners).
"""
instance = self.get_object()
# TODO: Add permission checks when authentication is enabled
# current_user = request.user
# if current_user.role == User.Role.TENANT_MANAGER:
# if instance.role in [User.Role.TENANT_OWNER, User.Role.TENANT_MANAGER]:
# return Response(
# {'error': 'Managers cannot edit owners or other managers.'},
# status=status.HTTP_403_FORBIDDEN
# )
# Only allow updating specific fields
allowed_fields = {'is_active', 'permissions'}
update_data = {k: v for k, v in request.data.items() if k in allowed_fields}
serializer = self.get_serializer(instance, data=update_data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
@action(detail=True, methods=['post'])
def toggle_active(self, request, pk=None):
"""Toggle the active status of a staff member."""
staff = self.get_object()
# Prevent deactivating yourself
# TODO: Enable this check when authentication is enabled
# if request.user.id == staff.id:
# return Response(
# {'error': 'You cannot deactivate your own account.'},
# status=status.HTTP_400_BAD_REQUEST
# )
staff.is_active = not staff.is_active
staff.save(update_fields=['is_active'])
return Response({
'id': staff.id,
'is_active': staff.is_active,
'message': f"Staff member {'activated' if staff.is_active else 'deactivated'} successfully."
})