feat(cohorts): rename Programs to Cohorts with data migration (#694)
* feat(cohorts): rename Programs to Cohorts with data migration - Add cohort database tables and Drizzle migration alongside legacy program tables - Introduce migrate-programs-to-cohorts script to copy existing program data safely - Rename API routes (/cohort), services, queries, validation, and error codes - Rename dashboard feature module, routes (/cohorts), navigation, and translations - Update docs, emails, and internal compliance cron endpoints for cohort terminology - Keep legacy program tables in place for rollback until explicitly removed Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(db): restore migrate:answer-data script alongside cohort migration Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(db): cast program enums when migrating data to cohort tables Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(db): truncate cohort tables before program-to-cohort copy Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(db): report accurate row counts in programs-to-cohorts migration Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
fe9f8c441f
commit
ec61bfe7d9
@@ -0,0 +1,730 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import {
|
||||
ZAddCourseToCohort,
|
||||
ZAddCohortMembers,
|
||||
ZAssignExistingStudentsToCohort,
|
||||
ZCreateCohort,
|
||||
ZCreateCohortGoal,
|
||||
ZCreateCohortNewsfeed,
|
||||
ZCreateCohortNewsfeedComment,
|
||||
ZInviteStudentsToCohort,
|
||||
ZUpdateCohort,
|
||||
ZUpdateCohortGoal,
|
||||
ZUpdateCohortMember,
|
||||
ZUpdateCohortNewsfeed,
|
||||
ZUpdateCohortReaction
|
||||
} from '@cio/utils/validation/cohort';
|
||||
import {
|
||||
addCourseToCohortService,
|
||||
addCohortMembers,
|
||||
createCohort,
|
||||
createCohortNewsfeedCommentService,
|
||||
createCohortNewsfeedService,
|
||||
deleteCohort,
|
||||
removeCohortMemberService,
|
||||
deleteCohortNewsfeedCommentService,
|
||||
deleteCohortNewsfeedService,
|
||||
getEnrolledCohorts,
|
||||
getCohort,
|
||||
listOrgCohorts,
|
||||
listCohortCourses,
|
||||
listCohortMembers,
|
||||
listCohortNewsfeed,
|
||||
listCohortNewsfeedComments,
|
||||
removeCourseFromCohortService,
|
||||
updateCohort,
|
||||
updateCohortMemberService,
|
||||
updateCohortNewsfeedReactionService,
|
||||
updateCohortNewsfeedService
|
||||
} from '@api/services/cohort/cohort';
|
||||
import { assignExistingStudentsToCohort, inviteStudentsToCohort } from '@api/services/cohort/invite';
|
||||
import {
|
||||
archiveGoal,
|
||||
createGoal,
|
||||
evaluateGoal,
|
||||
evaluateCohortGoals,
|
||||
getGoal,
|
||||
getMyGoals,
|
||||
getOrgGoalsOverview,
|
||||
listGoals,
|
||||
removeGoal,
|
||||
updateGoal
|
||||
} from '@api/services/cohort/goal';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { orgTeamMemberMiddleware } from '@api/middlewares/org-team-member';
|
||||
import { cohortMemberMiddleware } from '@api/middlewares/cohort-member';
|
||||
import { cohortTeamMemberMiddleware } from '@api/middlewares/cohort-team-member';
|
||||
import { cohortNewsfeedCommentAuthorOrTeamMiddleware } from '@api/middlewares/cohort-newsfeed-comment-author-or-team';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
const ZCohortParam = z.object({ cohortId: z.string().uuid() });
|
||||
const ZMemberParam = z.object({ cohortId: z.string().uuid(), memberId: z.string().uuid() });
|
||||
const ZCourseParam = z.object({ cohortId: z.string().uuid(), courseId: z.string().uuid() });
|
||||
const ZFeedParam = z.object({ cohortId: z.string().uuid(), feedId: z.string().uuid() });
|
||||
const ZCommentParam = z.object({
|
||||
cohortId: z.string().uuid(),
|
||||
feedId: z.string().uuid(),
|
||||
commentId: z.coerce.number().int()
|
||||
});
|
||||
const ZGoalParam = z.object({ cohortId: z.string().uuid(), goalId: z.string().uuid() });
|
||||
const ZListQuery = z.object({
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(50).default(10)
|
||||
});
|
||||
const ZOrgQuery = z.object({ organizationId: z.string().uuid() });
|
||||
|
||||
export const cohortRouter = new Hono()
|
||||
// ── Enrolled Cohorts (LMS) ─── must be before /:cohortId ───────────────
|
||||
|
||||
/**
|
||||
* GET /cohort/enrolled
|
||||
* Get programs the current user is enrolled in
|
||||
*/
|
||||
.get('/enrolled', authMiddleware, async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const programs = await getEnrolledCohorts(user.id);
|
||||
return c.json({ success: true, data: programs }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to get enrolled cohorts');
|
||||
}
|
||||
})
|
||||
|
||||
// ── Programs ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /cohort?organizationId=...
|
||||
* List all programs for an org
|
||||
*/
|
||||
.get('/', authMiddleware, zValidator('query', ZOrgQuery), async (c) => {
|
||||
try {
|
||||
const { organizationId } = c.req.valid('query');
|
||||
const programs = await listOrgCohorts(organizationId);
|
||||
return c.json({ success: true, data: programs }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list cohorts');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /cohort
|
||||
* Create a new program
|
||||
*/
|
||||
.post(
|
||||
'/',
|
||||
authMiddleware,
|
||||
zValidator('json', ZCreateCohort.extend({ organizationId: z.string().uuid() })),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { organizationId, ...data } = c.req.valid('json');
|
||||
const cohort = await createCohort(organizationId, user.id, data);
|
||||
return c.json({ success: true, data: cohort }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create cohort');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* GET /cohort/:cohortId
|
||||
* Get a cohort by ID
|
||||
*/
|
||||
.get('/:cohortId', authMiddleware, cohortMemberMiddleware, zValidator('param', ZCohortParam), async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const cohort = await getCohort(cohortId);
|
||||
return c.json({ success: true, data: cohort }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to get cohort');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* PUT /cohort/:cohortId
|
||||
* Update a cohort
|
||||
*/
|
||||
.put(
|
||||
'/:cohortId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('json', ZUpdateCohort),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const cohort = await updateCohort(cohortId, data);
|
||||
return c.json({ success: true, data: cohort }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update cohort');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /cohort/:cohortId
|
||||
* Delete a cohort
|
||||
*/
|
||||
.delete('/:cohortId', authMiddleware, cohortTeamMemberMiddleware, zValidator('param', ZCohortParam), async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const cohort = await deleteCohort(cohortId);
|
||||
return c.json({ success: true, data: cohort }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete cohort');
|
||||
}
|
||||
})
|
||||
|
||||
// ── Members ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /cohort/:cohortId/members
|
||||
* List cohort members
|
||||
*/
|
||||
.get('/:cohortId/members', authMiddleware, cohortMemberMiddleware, zValidator('param', ZCohortParam), async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const members = await listCohortMembers(cohortId);
|
||||
return c.json({ success: true, data: members }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list cohort members');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/members
|
||||
* Add members to a cohort
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/members',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('json', ZAddCohortMembers),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await addCohortMembers(cohortId, data);
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to add cohort members');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /cohort/:cohortId/members/:memberId
|
||||
* Update a cohort member's role
|
||||
*/
|
||||
.put(
|
||||
'/:cohortId/members/:memberId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZMemberParam),
|
||||
zValidator('json', ZUpdateCohortMember),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId, memberId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const member = await updateCohortMemberService(cohortId, memberId, data);
|
||||
return c.json({ success: true, data: member }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update cohort member');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /cohort/:cohortId/members/:memberId
|
||||
* Remove a member from a cohort
|
||||
*/
|
||||
.delete(
|
||||
'/:cohortId/members/:memberId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZMemberParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId, memberId } = c.req.valid('param');
|
||||
const member = await removeCohortMemberService(cohortId, memberId);
|
||||
return c.json({ success: true, data: member }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to remove program member');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Invite ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/invite
|
||||
* Invite new students to a cohort by CSV emails. Issues org invites
|
||||
* pre-tagged with this cohort so accept auto-enrolls them.
|
||||
* Gated by cohort-team-member middleware — Cohort ADMINs/TUTORs can
|
||||
* invite into their own program without org-admin rights.
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/invite',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('json', ZInviteStudentsToCohort),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await inviteStudentsToCohort(cohortId, data, user.id);
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to invite students to program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/invite/assign
|
||||
* Assign existing org-audience student profiles to a cohort.
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/invite/assign',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('json', ZAssignExistingStudentsToCohort),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await assignExistingStudentsToCohort(cohortId, data);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to assign students to program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Courses ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /cohort/:cohortId/courses
|
||||
* List courses in a cohort
|
||||
*/
|
||||
.get('/:cohortId/courses', authMiddleware, cohortMemberMiddleware, zValidator('param', ZCohortParam), async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const courses = await listCohortCourses(cohortId, user.id);
|
||||
return c.json({ success: true, data: courses }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list cohort courses');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/courses
|
||||
* Add a course to a cohort
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/courses',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('json', ZAddCourseToCohort),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await addCourseToCohortService(cohortId, data);
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to add course to program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /cohort/:cohortId/courses/:courseId
|
||||
* Remove a course from a cohort
|
||||
*/
|
||||
.delete(
|
||||
'/:cohortId/courses/:courseId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCourseParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId, courseId } = c.req.valid('param');
|
||||
const result = await removeCourseFromCohortService(cohortId, courseId);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to remove course from program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Newsfeed ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /cohort/:cohortId/newsfeed
|
||||
* List newsfeed for a cohort (paginated)
|
||||
*/
|
||||
.get(
|
||||
'/:cohortId/newsfeed',
|
||||
authMiddleware,
|
||||
cohortMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('query', ZListQuery),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const { cursor, limit } = c.req.valid('query');
|
||||
const result = await listCohortNewsfeed(cohortId, { cursor, limit });
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list cohort newsfeed');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/newsfeed
|
||||
* Create a newsfeed post
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/newsfeed',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('json', ZCreateCohortNewsfeed),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const feed = await createCohortNewsfeedService(cohortId, user.id, data);
|
||||
return c.json({ success: true, data: feed }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create cohort newsfeed post');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /cohort/:cohortId/newsfeed/:feedId
|
||||
* Update a newsfeed post
|
||||
*/
|
||||
.put(
|
||||
'/:cohortId/newsfeed/:feedId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
zValidator('json', ZUpdateCohortNewsfeed),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const feed = await updateCohortNewsfeedService(feedId, data);
|
||||
return c.json({ success: true, data: feed }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update cohort newsfeed post');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /cohort/:cohortId/newsfeed/:feedId/react
|
||||
* Update reaction on a newsfeed post
|
||||
*/
|
||||
.put(
|
||||
'/:cohortId/newsfeed/:feedId/react',
|
||||
authMiddleware,
|
||||
cohortMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
zValidator('json', ZUpdateCohortReaction),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const feed = await updateCohortNewsfeedReactionService(feedId, data);
|
||||
return c.json({ success: true, data: feed }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update cohort newsfeed reaction');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /cohort/:cohortId/newsfeed/:feedId
|
||||
* Delete a newsfeed post
|
||||
*/
|
||||
.delete(
|
||||
'/:cohortId/newsfeed/:feedId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const feed = await deleteCohortNewsfeedService(feedId);
|
||||
return c.json({ success: true, data: feed }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete cohort newsfeed post');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* GET /cohort/:cohortId/newsfeed/:feedId/comments
|
||||
* List comments on a newsfeed post
|
||||
*/
|
||||
.get(
|
||||
'/:cohortId/newsfeed/:feedId/comments',
|
||||
authMiddleware,
|
||||
cohortMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const comments = await listCohortNewsfeedComments(feedId);
|
||||
return c.json({ success: true, data: comments }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list cohort newsfeed comments');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/newsfeed/:feedId/comment
|
||||
* Create a comment on a newsfeed post
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/newsfeed/:feedId/comment',
|
||||
authMiddleware,
|
||||
cohortMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
zValidator('json', ZCreateCohortNewsfeedComment),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { feedId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const comment = await createCohortNewsfeedCommentService(feedId, user.id, data);
|
||||
return c.json({ success: true, data: comment }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create cohort newsfeed comment');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /cohort/:cohortId/newsfeed/:feedId/comment/:commentId
|
||||
* Delete a comment on a newsfeed post
|
||||
*/
|
||||
.delete(
|
||||
'/:cohortId/newsfeed/:feedId/comment/:commentId',
|
||||
authMiddleware,
|
||||
cohortNewsfeedCommentAuthorOrTeamMiddleware,
|
||||
zValidator('param', ZCommentParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { commentId } = c.req.valid('param');
|
||||
const comment = await deleteCohortNewsfeedCommentService(commentId);
|
||||
return c.json({ success: true, data: comment }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete cohort newsfeed comment');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Goals ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /cohort/my/goals
|
||||
* Goal assignments for the current authenticated user across all their programs.
|
||||
* Used by the LMS "Your Goals" widget.
|
||||
*/
|
||||
.get('/my/goals', authMiddleware, async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const goals = await getMyGoals(user.id);
|
||||
return c.json({ success: true, data: goals }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to load goals');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /cohort/goals/overview?organizationId=...
|
||||
* Cross-cohort goal roll-up for the org owner. One entry per active goal
|
||||
* with per-status learner counts, scoped to the org via header + query.
|
||||
*/
|
||||
.get('/goals/overview', authMiddleware, orgTeamMemberMiddleware, zValidator('query', ZOrgQuery), async (c) => {
|
||||
try {
|
||||
const { organizationId } = c.req.valid('query');
|
||||
const overview = await getOrgGoalsOverview(organizationId);
|
||||
return c.json({ success: true, data: overview }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to load goals overview');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /cohort/:cohortId/goals
|
||||
*/
|
||||
.get('/:cohortId/goals', authMiddleware, cohortMemberMiddleware, zValidator('param', ZCohortParam), async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const goals = await listGoals(cohortId);
|
||||
return c.json({ success: true, data: goals }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list cohort goals');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/goals
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/goals',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
zValidator('json', ZCreateCohortGoal),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const goal = await createGoal(cohortId, user.id, data);
|
||||
return c.json({ success: true, data: goal }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create cohort goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* GET /cohort/:cohortId/goals/:goalId
|
||||
*/
|
||||
.get(
|
||||
'/:cohortId/goals/:goalId',
|
||||
authMiddleware,
|
||||
cohortMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const goal = await getGoal(goalId);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to get cohort goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /cohort/:cohortId/goals/:goalId
|
||||
*/
|
||||
.put(
|
||||
'/:cohortId/goals/:goalId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
zValidator('json', ZUpdateCohortGoal),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const goal = await updateGoal(goalId, data);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update cohort goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /cohort/:cohortId/goals/:goalId
|
||||
* Hard delete (vs. archive). Use PUT with status=archived to preserve history.
|
||||
*/
|
||||
.delete(
|
||||
'/:cohortId/goals/:goalId',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const goal = await removeGoal(goalId);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete cohort goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/goals/:goalId/archive
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/goals/:goalId/archive',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const goal = await archiveGoal(goalId);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to archive cohort goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/goals/:goalId/evaluate
|
||||
* Force a re-evaluation of one goal's per-learner statuses.
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/goals/:goalId/evaluate',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const result = await evaluateGoal(goalId);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to evaluate cohort goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /cohort/:cohortId/goals/evaluate-all
|
||||
* Force a re-evaluation of every active goal in the cohort.
|
||||
*/
|
||||
.post(
|
||||
'/:cohortId/goals/evaluate-all',
|
||||
authMiddleware,
|
||||
cohortTeamMemberMiddleware,
|
||||
zValidator('param', ZCohortParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { cohortId } = c.req.valid('param');
|
||||
const result = await evaluateCohortGoals(cohortId);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to evaluate cohort goals');
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { cohortRouter } from './cohort';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { runComplianceExpiryCheck, runComplianceReminderScan } from '@api/services/course/compliance';
|
||||
import { runProgramGoalEvaluationSweep, runProgramGoalReminderScan } from '@api/services/program/goal';
|
||||
import { runCohortGoalEvaluationSweep, runCohortGoalReminderScan } from '@api/services/cohort/goal';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { apiKeyMiddleware } from '@api/middlewares/api-key';
|
||||
@@ -47,30 +47,30 @@ export const internalComplianceRouter = new Hono()
|
||||
}
|
||||
})
|
||||
/**
|
||||
* POST /internal/compliance/evaluate-program-goals
|
||||
* Daily sweep: re-evaluate every active program goal and refresh per-learner
|
||||
* `program_goal_assignment` rows.
|
||||
* POST /internal/compliance/evaluate-cohort-goals
|
||||
* Daily sweep: re-evaluate every active cohort goal and refresh per-learner
|
||||
* `cohort_goal_assignment` rows.
|
||||
*/
|
||||
.post('/evaluate-program-goals', apiKeyMiddleware, async (c) => {
|
||||
.post('/evaluate-cohort-goals', apiKeyMiddleware, async (c) => {
|
||||
try {
|
||||
const data = await runProgramGoalEvaluationSweep();
|
||||
const data = await runCohortGoalEvaluationSweep();
|
||||
|
||||
return c.json({ success: true, data }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to run program goal evaluation sweep');
|
||||
return handleError(c, error, 'Failed to run cohort goal evaluation sweep');
|
||||
}
|
||||
})
|
||||
/**
|
||||
* POST /internal/compliance/send-program-goal-reminders
|
||||
* Sends learner reminder emails for upcoming/overdue program goals using
|
||||
* POST /internal/compliance/send-cohort-goal-reminders
|
||||
* Sends learner reminder emails for upcoming/overdue cohort goals using
|
||||
* each goal's `reminderDaysBefore` cadence.
|
||||
*/
|
||||
.post('/send-program-goal-reminders', apiKeyMiddleware, async (c) => {
|
||||
.post('/send-cohort-goal-reminders', apiKeyMiddleware, async (c) => {
|
||||
try {
|
||||
const data = await runProgramGoalReminderScan();
|
||||
const data = await runCohortGoalReminderScan();
|
||||
|
||||
return c.json({ success: true, data }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to run program goal reminder scan');
|
||||
return handleError(c, error, 'Failed to run cohort goal reminder scan');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { programRouter } from './program';
|
||||
@@ -1,742 +0,0 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import {
|
||||
ZAddCourseToProgram,
|
||||
ZAddProgramMembers,
|
||||
ZAssignExistingStudentsToProgram,
|
||||
ZCreateProgram,
|
||||
ZCreateProgramGoal,
|
||||
ZCreateProgramNewsfeed,
|
||||
ZCreateProgramNewsfeedComment,
|
||||
ZInviteStudentsToProgram,
|
||||
ZUpdateProgram,
|
||||
ZUpdateProgramGoal,
|
||||
ZUpdateProgramMember,
|
||||
ZUpdateProgramNewsfeed,
|
||||
ZUpdateProgramReaction
|
||||
} from '@cio/utils/validation/program';
|
||||
import {
|
||||
addCourseToProgramService,
|
||||
addProgramMembers,
|
||||
createProgram,
|
||||
createProgramNewsfeedCommentService,
|
||||
createProgramNewsfeedService,
|
||||
deleteProgram,
|
||||
removeProgramMemberService,
|
||||
deleteProgramNewsfeedCommentService,
|
||||
deleteProgramNewsfeedService,
|
||||
getEnrolledPrograms,
|
||||
getProgram,
|
||||
listOrgPrograms,
|
||||
listProgramCourses,
|
||||
listProgramMembers,
|
||||
listProgramNewsfeed,
|
||||
listProgramNewsfeedComments,
|
||||
removeCourseFromProgramService,
|
||||
updateProgram,
|
||||
updateProgramMemberService,
|
||||
updateProgramNewsfeedReactionService,
|
||||
updateProgramNewsfeedService
|
||||
} from '@api/services/program/program';
|
||||
import { assignExistingStudentsToProgram, inviteStudentsToProgram } from '@api/services/program/invite';
|
||||
import {
|
||||
archiveGoal,
|
||||
createGoal,
|
||||
evaluateGoal,
|
||||
evaluateProgramGoals,
|
||||
getGoal,
|
||||
getMyGoals,
|
||||
getOrgGoalsOverview,
|
||||
listGoals,
|
||||
removeGoal,
|
||||
updateGoal
|
||||
} from '@api/services/program/goal';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { orgTeamMemberMiddleware } from '@api/middlewares/org-team-member';
|
||||
import { programMemberMiddleware } from '@api/middlewares/program-member';
|
||||
import { programTeamMemberMiddleware } from '@api/middlewares/program-team-member';
|
||||
import { programNewsfeedCommentAuthorOrTeamMiddleware } from '@api/middlewares/program-newsfeed-comment-author-or-team';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
const ZProgramParam = z.object({ programId: z.string().uuid() });
|
||||
const ZMemberParam = z.object({ programId: z.string().uuid(), memberId: z.string().uuid() });
|
||||
const ZCourseParam = z.object({ programId: z.string().uuid(), courseId: z.string().uuid() });
|
||||
const ZFeedParam = z.object({ programId: z.string().uuid(), feedId: z.string().uuid() });
|
||||
const ZCommentParam = z.object({
|
||||
programId: z.string().uuid(),
|
||||
feedId: z.string().uuid(),
|
||||
commentId: z.coerce.number().int()
|
||||
});
|
||||
const ZGoalParam = z.object({ programId: z.string().uuid(), goalId: z.string().uuid() });
|
||||
const ZListQuery = z.object({
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(50).default(10)
|
||||
});
|
||||
const ZOrgQuery = z.object({ organizationId: z.string().uuid() });
|
||||
|
||||
export const programRouter = new Hono()
|
||||
// ── Enrolled Programs (LMS) ─── must be before /:programId ───────────────
|
||||
|
||||
/**
|
||||
* GET /program/enrolled
|
||||
* Get programs the current user is enrolled in
|
||||
*/
|
||||
.get('/enrolled', authMiddleware, async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const programs = await getEnrolledPrograms(user.id);
|
||||
return c.json({ success: true, data: programs }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to get enrolled programs');
|
||||
}
|
||||
})
|
||||
|
||||
// ── Programs ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /program?organizationId=...
|
||||
* List all programs for an org
|
||||
*/
|
||||
.get('/', authMiddleware, zValidator('query', ZOrgQuery), async (c) => {
|
||||
try {
|
||||
const { organizationId } = c.req.valid('query');
|
||||
const programs = await listOrgPrograms(organizationId);
|
||||
return c.json({ success: true, data: programs }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list programs');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /program
|
||||
* Create a new program
|
||||
*/
|
||||
.post(
|
||||
'/',
|
||||
authMiddleware,
|
||||
zValidator('json', ZCreateProgram.extend({ organizationId: z.string().uuid() })),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { organizationId, ...data } = c.req.valid('json');
|
||||
const program = await createProgram(organizationId, user.id, data);
|
||||
return c.json({ success: true, data: program }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* GET /program/:programId
|
||||
* Get a program by ID
|
||||
*/
|
||||
.get('/:programId', authMiddleware, programMemberMiddleware, zValidator('param', ZProgramParam), async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const program = await getProgram(programId);
|
||||
return c.json({ success: true, data: program }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to get program');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* PUT /program/:programId
|
||||
* Update a program
|
||||
*/
|
||||
.put(
|
||||
'/:programId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('json', ZUpdateProgram),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const program = await updateProgram(programId, data);
|
||||
return c.json({ success: true, data: program }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /program/:programId
|
||||
* Delete a program
|
||||
*/
|
||||
.delete('/:programId', authMiddleware, programTeamMemberMiddleware, zValidator('param', ZProgramParam), async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const program = await deleteProgram(programId);
|
||||
return c.json({ success: true, data: program }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete program');
|
||||
}
|
||||
})
|
||||
|
||||
// ── Members ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /program/:programId/members
|
||||
* List program members
|
||||
*/
|
||||
.get(
|
||||
'/:programId/members',
|
||||
authMiddleware,
|
||||
programMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const members = await listProgramMembers(programId);
|
||||
return c.json({ success: true, data: members }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list program members');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/members
|
||||
* Add members to a program
|
||||
*/
|
||||
.post(
|
||||
'/:programId/members',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('json', ZAddProgramMembers),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await addProgramMembers(programId, data);
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to add program members');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /program/:programId/members/:memberId
|
||||
* Update a program member's role
|
||||
*/
|
||||
.put(
|
||||
'/:programId/members/:memberId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZMemberParam),
|
||||
zValidator('json', ZUpdateProgramMember),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId, memberId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const member = await updateProgramMemberService(programId, memberId, data);
|
||||
return c.json({ success: true, data: member }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update program member');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /program/:programId/members/:memberId
|
||||
* Remove a member from a program
|
||||
*/
|
||||
.delete(
|
||||
'/:programId/members/:memberId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZMemberParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId, memberId } = c.req.valid('param');
|
||||
const member = await removeProgramMemberService(programId, memberId);
|
||||
return c.json({ success: true, data: member }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to remove program member');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Invite ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /program/:programId/invite
|
||||
* Invite new students to a program by CSV emails. Issues org invites
|
||||
* pre-tagged with this program so accept auto-enrolls them.
|
||||
* Gated by program-team-member middleware — Program ADMINs/TUTORs can
|
||||
* invite into their own program without org-admin rights.
|
||||
*/
|
||||
.post(
|
||||
'/:programId/invite',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('json', ZInviteStudentsToProgram),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { programId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await inviteStudentsToProgram(programId, data, user.id);
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to invite students to program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/invite/assign
|
||||
* Assign existing org-audience student profiles to a program.
|
||||
*/
|
||||
.post(
|
||||
'/:programId/invite/assign',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('json', ZAssignExistingStudentsToProgram),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await assignExistingStudentsToProgram(programId, data);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to assign students to program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Courses ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /program/:programId/courses
|
||||
* List courses in a program
|
||||
*/
|
||||
.get(
|
||||
'/:programId/courses',
|
||||
authMiddleware,
|
||||
programMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { programId } = c.req.valid('param');
|
||||
const courses = await listProgramCourses(programId, user.id);
|
||||
return c.json({ success: true, data: courses }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list program courses');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/courses
|
||||
* Add a course to a program
|
||||
*/
|
||||
.post(
|
||||
'/:programId/courses',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('json', ZAddCourseToProgram),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const result = await addCourseToProgramService(programId, data);
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to add course to program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /program/:programId/courses/:courseId
|
||||
* Remove a course from a program
|
||||
*/
|
||||
.delete(
|
||||
'/:programId/courses/:courseId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZCourseParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId, courseId } = c.req.valid('param');
|
||||
const result = await removeCourseFromProgramService(programId, courseId);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to remove course from program');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Newsfeed ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /program/:programId/newsfeed
|
||||
* List newsfeed for a program (paginated)
|
||||
*/
|
||||
.get(
|
||||
'/:programId/newsfeed',
|
||||
authMiddleware,
|
||||
programMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('query', ZListQuery),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const { cursor, limit } = c.req.valid('query');
|
||||
const result = await listProgramNewsfeed(programId, { cursor, limit });
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list program newsfeed');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/newsfeed
|
||||
* Create a newsfeed post
|
||||
*/
|
||||
.post(
|
||||
'/:programId/newsfeed',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('json', ZCreateProgramNewsfeed),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { programId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const feed = await createProgramNewsfeedService(programId, user.id, data);
|
||||
return c.json({ success: true, data: feed }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create program newsfeed post');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /program/:programId/newsfeed/:feedId
|
||||
* Update a newsfeed post
|
||||
*/
|
||||
.put(
|
||||
'/:programId/newsfeed/:feedId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
zValidator('json', ZUpdateProgramNewsfeed),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const feed = await updateProgramNewsfeedService(feedId, data);
|
||||
return c.json({ success: true, data: feed }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update program newsfeed post');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /program/:programId/newsfeed/:feedId/react
|
||||
* Update reaction on a newsfeed post
|
||||
*/
|
||||
.put(
|
||||
'/:programId/newsfeed/:feedId/react',
|
||||
authMiddleware,
|
||||
programMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
zValidator('json', ZUpdateProgramReaction),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const feed = await updateProgramNewsfeedReactionService(feedId, data);
|
||||
return c.json({ success: true, data: feed }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update program newsfeed reaction');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /program/:programId/newsfeed/:feedId
|
||||
* Delete a newsfeed post
|
||||
*/
|
||||
.delete(
|
||||
'/:programId/newsfeed/:feedId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const feed = await deleteProgramNewsfeedService(feedId);
|
||||
return c.json({ success: true, data: feed }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete program newsfeed post');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* GET /program/:programId/newsfeed/:feedId/comments
|
||||
* List comments on a newsfeed post
|
||||
*/
|
||||
.get(
|
||||
'/:programId/newsfeed/:feedId/comments',
|
||||
authMiddleware,
|
||||
programMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const comments = await listProgramNewsfeedComments(feedId);
|
||||
return c.json({ success: true, data: comments }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list program newsfeed comments');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/newsfeed/:feedId/comment
|
||||
* Create a comment on a newsfeed post
|
||||
*/
|
||||
.post(
|
||||
'/:programId/newsfeed/:feedId/comment',
|
||||
authMiddleware,
|
||||
programMemberMiddleware,
|
||||
zValidator('param', ZFeedParam),
|
||||
zValidator('json', ZCreateProgramNewsfeedComment),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { feedId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const comment = await createProgramNewsfeedCommentService(feedId, user.id, data);
|
||||
return c.json({ success: true, data: comment }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create program newsfeed comment');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /program/:programId/newsfeed/:feedId/comment/:commentId
|
||||
* Delete a comment on a newsfeed post
|
||||
*/
|
||||
.delete(
|
||||
'/:programId/newsfeed/:feedId/comment/:commentId',
|
||||
authMiddleware,
|
||||
programNewsfeedCommentAuthorOrTeamMiddleware,
|
||||
zValidator('param', ZCommentParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { commentId } = c.req.valid('param');
|
||||
const comment = await deleteProgramNewsfeedCommentService(commentId);
|
||||
return c.json({ success: true, data: comment }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete program newsfeed comment');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ── Goals ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /program/my/goals
|
||||
* Goal assignments for the current authenticated user across all their programs.
|
||||
* Used by the LMS "Your Goals" widget.
|
||||
*/
|
||||
.get('/my/goals', authMiddleware, async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const goals = await getMyGoals(user.id);
|
||||
return c.json({ success: true, data: goals }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to load goals');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /program/goals/overview?organizationId=...
|
||||
* Cross-program goal roll-up for the org owner. One entry per active goal
|
||||
* with per-status learner counts, scoped to the org via header + query.
|
||||
*/
|
||||
.get('/goals/overview', authMiddleware, orgTeamMemberMiddleware, zValidator('query', ZOrgQuery), async (c) => {
|
||||
try {
|
||||
const { organizationId } = c.req.valid('query');
|
||||
const overview = await getOrgGoalsOverview(organizationId);
|
||||
return c.json({ success: true, data: overview }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to load goals overview');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /program/:programId/goals
|
||||
*/
|
||||
.get('/:programId/goals', authMiddleware, programMemberMiddleware, zValidator('param', ZProgramParam), async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const goals = await listGoals(programId);
|
||||
return c.json({ success: true, data: goals }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to list program goals');
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /program/:programId/goals
|
||||
*/
|
||||
.post(
|
||||
'/:programId/goals',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
zValidator('json', ZCreateProgramGoal),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user')!;
|
||||
const { programId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const goal = await createGoal(programId, user.id, data);
|
||||
return c.json({ success: true, data: goal }, 201);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create program goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* GET /program/:programId/goals/:goalId
|
||||
*/
|
||||
.get(
|
||||
'/:programId/goals/:goalId',
|
||||
authMiddleware,
|
||||
programMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const goal = await getGoal(goalId);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to get program goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PUT /program/:programId/goals/:goalId
|
||||
*/
|
||||
.put(
|
||||
'/:programId/goals/:goalId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
zValidator('json', ZUpdateProgramGoal),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const goal = await updateGoal(goalId, data);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to update program goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE /program/:programId/goals/:goalId
|
||||
* Hard delete (vs. archive). Use PUT with status=archived to preserve history.
|
||||
*/
|
||||
.delete(
|
||||
'/:programId/goals/:goalId',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const goal = await removeGoal(goalId);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to delete program goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/goals/:goalId/archive
|
||||
*/
|
||||
.post(
|
||||
'/:programId/goals/:goalId/archive',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const goal = await archiveGoal(goalId);
|
||||
return c.json({ success: true, data: goal }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to archive program goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/goals/:goalId/evaluate
|
||||
* Force a re-evaluation of one goal's per-learner statuses.
|
||||
*/
|
||||
.post(
|
||||
'/:programId/goals/:goalId/evaluate',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZGoalParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { goalId } = c.req.valid('param');
|
||||
const result = await evaluateGoal(goalId);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to evaluate program goal');
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST /program/:programId/goals/evaluate-all
|
||||
* Force a re-evaluation of every active goal in the program.
|
||||
*/
|
||||
.post(
|
||||
'/:programId/goals/evaluate-all',
|
||||
authMiddleware,
|
||||
programTeamMemberMiddleware,
|
||||
zValidator('param', ZProgramParam),
|
||||
async (c) => {
|
||||
try {
|
||||
const { programId } = c.req.valid('param');
|
||||
const result = await evaluateProgramGoals(programId);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to evaluate program goals');
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user