diff --git a/apps/onboarding/viewsets.py b/apps/onboarding/viewsets.py index 2f38a06..6fd2dea 100644 --- a/apps/onboarding/viewsets.py +++ b/apps/onboarding/viewsets.py @@ -10,7 +10,7 @@ from rest_framework.response import Response from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet -from apps.accounts.models import Organization, Role +from apps.accounts.models import Organization, Role, User from apps.accounts.permissions import CanManageOrganization, can_manage_organization from apps.onboarding.models import AgentConfig, AgentInteractionLog, OnboardingFlow, OnboardingSession from apps.onboarding.serializers import AgentConfigSerializer, AgentInteractionLogSerializer, OnboardingFlowSerializer, OnboardingSessionSerializer @@ -309,12 +309,115 @@ class OnboardingSessionViewSet(ModelViewSet): if role_uuid: queryset = queryset.filter(role__uuid=role_uuid) + user_uuid = self.request.query_params.get('user_uuid') + if user_uuid in (None, ''): + user_uuid = self.request.data.get('user_uuid') + if user_uuid: + if not user.is_manager and str(user.uuid) != str(user_uuid): + raise PermissionDenied('You can only view your own progress sessions.') + queryset = queryset.filter(user__uuid=user_uuid) + + flow_uuid = self.request.query_params.get('flow_uuid') + if flow_uuid in (None, ''): + flow_uuid = self.request.data.get('flow_uuid') + if flow_uuid: + queryset = queryset.filter(state__flow_uuid=str(flow_uuid)) + status_value = self.request.query_params.get('status') if status_value: queryset = queryset.filter(status=status_value) return queryset.order_by('-created_at') + @action(detail=False, methods=['get'], url_path='progress-overview') + def progress_overview(self, request): + user = request.user + + role_uuid = request.query_params.get('role_uuid') + + if user.is_manager: + roles_qs = Role.objects.filter( + Q(organization__owner=user) | Q(organization__members=user) + ).distinct() + else: + roles_qs = Role.objects.filter(members=user) + + if role_uuid: + roles_qs = roles_qs.filter(uuid=role_uuid) + + rows = [] + + for role in roles_qs.order_by('name'): + flows = list(OnboardingFlow.objects.filter(role=role).order_by('-updated_at')) + if not flows: + continue + + if user.is_manager: + learners = list(role.members.all().order_by('first_name', 'last_name', 'email_address')) + else: + learners = [user] if role.members.filter(id=user.id).exists() else [] + + if not learners: + continue + + role_sessions = list( + OnboardingSession.objects.filter(role=role, user__in=learners) + .select_related('user') + .order_by('-updated_at') + ) + + latest_by_user_flow = {} + for session in role_sessions: + state = session.state if isinstance(session.state, dict) else {} + session_flow_uuid = str(state.get('flow_uuid') or '') + if not session_flow_uuid: + continue + + key = (session.user_id, session_flow_uuid) + if key not in latest_by_user_flow: + latest_by_user_flow[key] = session + + for flow in flows: + flow_uuid_str = str(flow.uuid) + for learner in learners: + latest_session = latest_by_user_flow.get((learner.id, flow_uuid_str)) + + latest_status = latest_session.status if latest_session else 'not_started' + state = latest_session.state if latest_session and isinstance(latest_session.state, dict) else {} + progress = state.get('progress_percentage', state.get('progress', 0)) + + rows.append({ + 'role': { + 'uuid': str(role.uuid), + 'name': role.name, + }, + 'user': { + 'uuid': str(learner.uuid), + 'name': str(getattr(learner, 'full_name', '')).strip() or learner.email_address, + 'email': learner.email_address, + }, + 'flow': { + 'uuid': flow_uuid_str, + 'title': flow.title, + 'is_active': flow.is_active, + }, + 'latest_status': latest_status, + 'progress': int(progress) if isinstance(progress, (int, float)) else 0, + 'is_completed': latest_status == 'completed', + 'latest_session_uuid': str(latest_session.uuid) if latest_session else None, + 'updated_at': latest_session.updated_at.isoformat() if latest_session and latest_session.updated_at else None, + }) + + rows.sort( + key=lambda row: ( + str(row.get('role', {}).get('name', '')), + str(row.get('user', {}).get('name', '')), + str(row.get('flow', {}).get('title', '')), + ) + ) + + return Response(rows, status=HTTP_200_OK) + def create(self, request, *args, **kwargs): return Response( {'error': 'Use onboarding-flow//start-session/ to create a session.'}, diff --git a/site/src/views/OrganizationManage.vue b/site/src/views/OrganizationManage.vue index 050420d..31670f6 100644 --- a/site/src/views/OrganizationManage.vue +++ b/site/src/views/OrganizationManage.vue @@ -38,6 +38,9 @@ const loading = ref(false) const creatingRole = ref(false) const deletingRoleUuid = ref(null) const roleModalVisible = ref(false) +const roleMembersModalVisible = ref(false) +const selectedRoleForMembers = ref(null) +const selectedRoleMembers = ref([]) const createRoleForm = ref({ name: '', description: '', @@ -186,6 +189,12 @@ const deleteRole = async (role: Role) => { }) } +const openRoleMembersModal = (role: Role) => { + selectedRoleForMembers.value = role + selectedRoleMembers.value = Array.isArray(role.members) ? role.members : [] + roleMembersModalVisible.value = true +} + const createInvite = async () => { try { const response = await apiClient.post( @@ -482,6 +491,9 @@ onMounted(async () => { /> {{ item.member_count }} members + + + + + + + + No members assigned to this role yet. + +