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:
@@ -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."
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user