Enhance audience management features with new components and API integrations

- Added `AssignCoursesModal`, `AudienceMemberRow`, `AudienceTable`, and related components for improved audience management UI.
- Implemented audience invite actions including resend and revoke functionality.
- Updated audience API to support importing members and assigning courses.
- Refactored audience data handling to include status and invite management.
- Improved overall user experience with pagination and selection features in the audience table.
This commit is contained in:
Rotimi Best
2026-03-22 18:42:23 +01:00
parent e9c4082f89
commit 3919ff6c2f
157 changed files with 13183 additions and 872 deletions
@@ -1,4 +1,6 @@
import {
ZAssignAudienceCourses,
ZAudienceInviteByEmail,
ZCancelOrgPlan,
ZCreateOrgPlan,
ZCreateOrganization,
@@ -7,12 +9,20 @@ import {
ZGetOrganizationCoursesQuery,
ZGetOrganizations,
ZGetUserAnalytics,
ZImportAudienceMembers,
ZInviteTeamMembers,
ZLMSExercisesParam,
ZRemoveTeamMember,
ZUpdateOrgPlan,
ZUpdateOrganization
} from '@cio/utils/validation/organization';
import { assertMcpAutomationUsageAllowed, recordMcpAutomationUsage } from '@api/services/organization/automation-usage';
import {
assignAudienceToCourses,
importAudienceMembers,
resendAudienceInvite,
revokeAudiencePendingInvite
} from '@api/services/organization/audience';
import {
cancelOrgPlan,
createOrg,
@@ -33,13 +43,13 @@ import {
} from '@api/services/organization';
import { Hono } from '@api/utils/hono';
import { ROLE } from '@cio/utils/constants';
import { TOrganization } from '@db/types';
import { automationRouter } from '@api/routes/organization/automation';
import { assetsRouter } from '@api/routes/organization/assets';
import { authMiddleware } from '@api/middlewares/auth';
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key';
import { assertMcpAutomationUsageAllowed, recordMcpAutomationUsage } from '@api/services/organization/automation-usage';
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
import { automationRouter } from '@api/routes/organization/automation';
import { courseImportRouter } from '@api/routes/organization/course-import';
import { getLMSExercisesService } from '@api/services/exercise';
import { handleError } from '@api/utils/errors';
@@ -47,9 +57,9 @@ import { inviteTeamMembers } from '@api/services/organization/invite';
import { orgAdminMiddleware } from '@api/middlewares/org-admin';
import { orgMemberMiddleware } from '@api/middlewares/org-member';
import { orgMemberOrAutomationKeyMiddleware } from '@api/middlewares/org-member-or-automation-key';
import { orgTeamMemberMiddleware } from '@api/middlewares/org-team-member';
import { quizRouter } from '@api/routes/organization/quiz';
import { tagsRouter } from '@api/routes/organization/tags';
import { ROLE } from '@cio/utils/constants';
import { zValidator } from '@hono/zod-validator';
export const organizationRouter = new Hono()
@@ -100,7 +110,7 @@ export const organizationRouter = new Hono()
* Gets organization team members (non-students)
* Requires authentication
*/
.get('/team', authMiddleware, orgMemberMiddleware, async (c) => {
.get('/team', authMiddleware, orgTeamMemberMiddleware, async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const team = await getOrgTeam(orgId);
@@ -168,7 +178,7 @@ export const organizationRouter = new Hono()
* Gets organization audience (students)
* Requires authentication
*/
.get('/audience', authMiddleware, orgMemberMiddleware, async (c) => {
.get('/audience', authMiddleware, orgTeamMemberMiddleware, async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const audience = await getOrgAudience(orgId);
@@ -184,6 +194,48 @@ export const organizationRouter = new Hono()
return handleError(c, error, 'Failed to fetch organization audience');
}
})
/**
* POST /organization/audience/resend-invite
* Resends a student org invite email (new token; optional course metadata from last invite)
*/
.post(
'/audience/resend-invite',
authMiddleware,
orgTeamMemberMiddleware,
zValidator('json', ZAudienceInviteByEmail),
async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const user = c.get('user')!;
const data = c.req.valid('json');
const result = await resendAudienceInvite(orgId, data, user.id);
return c.json({ success: true, data: result }, 200);
} catch (error) {
return handleError(c, error, 'Failed to resend audience invite');
}
}
)
/**
* POST /organization/audience/revoke-invite
* Revokes an active (non-expired) pending student org invite
*/
.post(
'/audience/revoke-invite',
authMiddleware,
orgTeamMemberMiddleware,
zValidator('json', ZAudienceInviteByEmail),
async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const user = c.get('user')!;
const data = c.req.valid('json');
const result = await revokeAudiencePendingInvite(orgId, data, user.id);
return c.json({ success: true, data: result }, 200);
} catch (error) {
return handleError(c, error, 'Failed to revoke audience invite');
}
}
)
/**
* GET /organization/audience/:userId/analytics
* Gets user analytics for a specific user in an organization
@@ -192,7 +244,7 @@ export const organizationRouter = new Hono()
.get(
'/audience/:userId/analytics',
authMiddleware,
orgMemberMiddleware,
orgTeamMemberMiddleware,
zValidator('param', ZGetUserAnalytics),
async (c) => {
try {
@@ -458,7 +510,6 @@ export const organizationRouter = new Hono()
*/
.put('/', authMiddleware, orgAdminMiddleware, zValidator('json', ZUpdateOrganization), async (c) => {
try {
console.log('updateOrg');
console.log(c.req.valid('json'));
const orgId = c.req.header('cio-org-id')!;
@@ -515,6 +566,65 @@ export const organizationRouter = new Hono()
return handleError(c, error, 'Failed to fetch LMS exercises');
}
})
/**
* POST /organization/audience/import
* Imports users into the organization as students with optional course assignment
* Requires authentication and admin role
*/
.post(
'/audience/import',
authMiddleware,
orgTeamMemberMiddleware,
zValidator('json', ZImportAudienceMembers),
async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const user = c.get('user')!;
const data = c.req.valid('json');
const result = await importAudienceMembers(orgId, data, user.id);
return c.json(
{
success: true,
data: result
},
201
);
} catch (error) {
return handleError(c, error, 'Failed to import audience members');
}
}
)
/**
* POST /organization/audience/assign-courses
* Assigns existing audience members to one or more courses
* Requires authentication and admin role
*/
.post(
'/audience/assign-courses',
authMiddleware,
orgAdminMiddleware,
zValidator('json', ZAssignAudienceCourses),
async (c) => {
try {
const orgId = c.req.header('cio-org-id')!;
const data = c.req.valid('json');
const result = await assignAudienceToCourses(orgId, data);
return c.json(
{
success: true,
data: result
},
200
);
} catch (error) {
return handleError(c, error, 'Failed to assign audience to courses');
}
}
)
.route('/automation', automationRouter)
.route('/course-import', courseImportRouter)
.route('/tags', tagsRouter)