@headless-lms/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +24 -18
- package/dist/assets/file-urls.d.ts +11 -7
- package/dist/assets/file-urls.js +14 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +5 -4
- package/dist/assets/index.js +4 -3
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/ports.d.ts +37 -17
- package/dist/assets/service.d.ts +30 -15
- package/dist/assets/service.js +229 -59
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js +2 -63
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +1 -2
- package/dist/automations/catalog.js +137 -65
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +20 -20
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +36 -20
- package/dist/automations/community-mail.js +32 -24
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/events.d.ts +0 -10
- package/dist/automations/index.d.ts +12 -4
- package/dist/automations/index.js +9 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +346 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +96 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +276 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +35 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +98 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +29 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +560 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +134 -21
- package/dist/automations/service.d.ts +30 -30
- package/dist/automations/service.js +111 -144
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +437 -46
- package/dist/content/events.js +45 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +8 -4
- package/dist/content/index.js +5 -1
- package/dist/content/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +37 -0
- package/dist/content/notification-categories.js +73 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +132 -68
- package/dist/content/service.d.ts +100 -68
- package/dist/content/service.js +703 -146
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +25 -0
- package/dist/content/titles.js +82 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +63 -39
- package/dist/discussion/chats.js +455 -190
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +227 -23
- package/dist/discussion/events.js +114 -19
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +12 -4
- package/dist/discussion/index.js +9 -2
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +279 -114
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +60 -44
- package/dist/discussion/posts.js +453 -287
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +50 -35
- package/dist/discussion/service.js +333 -205
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +115 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +28 -0
- package/dist/discussion/titles.js +98 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +3 -2
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +68 -38
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +43 -28
- package/dist/entitlements/service.js +387 -160
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +109 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/index.d.ts +6 -5
- package/dist/identity/index.js +4 -3
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +114 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +80 -56
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +17 -13
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +33 -16
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +45 -0
- package/dist/organizations/effective-permissions.js +113 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +320 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +15 -10
- package/dist/organizations/index.js +8 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +34 -0
- package/dist/organizations/notification-categories.js +66 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +187 -65
- package/dist/organizations/service.d.ts +104 -35
- package/dist/organizations/service.js +1247 -323
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +23 -8
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +80 -26
- package/dist/reporting/member/ports.d.ts +50 -28
- package/dist/reporting/member/service.d.ts +81 -35
- package/dist/reporting/member/service.js +438 -145
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +22 -3
- package/dist/shared/errors.d.ts +15 -2
- package/dist/shared/errors.js +21 -2
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +7 -0
- package/dist/shared/id.js +8 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/mailer.d.ts +11 -1
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +68 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +119 -6
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +14 -17
- package/dist/types/index.d.ts +13 -12
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +1 -10
- package/dist/types/schemas/automations.js +1 -3
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +596 -11
- package/dist/types/schemas/content.js +225 -9
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +362 -66
- package/dist/types/schemas/discussion.js +123 -15
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +14 -1
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +1 -85
- package/dist/types/schemas/identity.js +1 -42
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -7
- package/dist/types/schemas/index.js +8 -7
- package/dist/types/schemas/notifications.d.ts +96 -0
- package/dist/types/schemas/notifications.js +57 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/organizations.d.ts +96 -60
- package/dist/types/schemas/organizations.js +78 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +0 -1
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/automations/community-notifications.d.ts +0 -16
- package/dist/automations/community-notifications.js +0 -84
- package/dist/automations/community-notifications.js.map +0 -1
- package/dist/automations/seeds.d.ts +0 -17
- package/dist/automations/seeds.js +0 -235
- package/dist/automations/seeds.js.map +0 -1
- package/dist/identity/notifications.d.ts +0 -27
- package/dist/identity/notifications.js +0 -101
- package/dist/identity/notifications.js.map +0 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/progress/service.ts"],"sourcesContent":["// progress context — service implementation (inbound port).\n// The frontend only reports usage; this service makes every completion\n// decision. One report batch runs in one UoW transaction: ensure the record,\n// merge item payloads into the per-subject state map, evaluate the activity's\n// completion rule, then module/course against current published structure —\n// appending events with the writes. The container is resolved from the\n// hierarchy (activity → module → course), never supplied by the caller.\n// Percentage/resume are derived by readers, never stored here.\nimport { genId } from '../shared/id.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { ProgressRecord } from './model.js';\nimport type {\n ContentAccessReader,\n ProgressRepository,\n ProgressService,\n ProgressUnitOfWork,\n ProgressWriteScope,\n} from './ports.js';\nimport type { ProgressReportItem, ProgressTarget, ReportProgressInput } from './types.js';\nimport { progressEvents, type NewProgressEvent } from './events.js';\nimport type { Activity, CourseManagementService, Module } from '../content/index.js';\nimport type { JsonValue } from '../types/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\n\n/** Mirrors reporting/member: `settings.published === false` is the only draft signal. */\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\n/** Fold a report batch into the record's per-subject state map. Reserved keys\n * (`asset` = subject, `completed` = claim) are stripped; the rest is the asset\n * type's own vocabulary, stored opaquely — latest report per subject wins. */\nfunction mergeReports(\n position: unknown,\n items: ProgressReportItem[],\n): { map: Record<string, JsonValue>; changed: boolean; claimed: boolean } {\n const map = { ...((position as Record<string, JsonValue> | null) ?? {}) };\n let changed = false;\n let claimed = false;\n for (const item of items) {\n const { asset, completed, ...payload } = item;\n if (completed === true) {\n claimed = true;\n }\n if (Object.keys(payload).length > 0) {\n map[asset ?? 'self'] = payload;\n changed = true;\n }\n }\n return { map, changed, claimed };\n}\n\n/** The completion-rule seam. Only `manual` ships: no authored rule → the\n * learner's claim decides. Authored rules are never satisfied until their\n * evaluators exist. Future rules evaluate against the accumulated state map. */\nfunction completionSatisfied(settings: unknown, claimed: boolean): boolean {\n const rule = (settings as { completion?: { rule?: string } } | null)?.completion?.rule;\n if (!rule || rule === 'manual') {\n return claimed;\n }\n return false;\n}\n\nexport type ProgressServiceParams = {\n repo: ProgressRepository;\n content: CourseManagementService;\n access: ContentAccessReader;\n uow: ProgressUnitOfWork;\n logger?: Logger;\n};\n\nexport class ProgressServiceImpl implements ProgressService {\n private readonly repo: ProgressRepository;\n private readonly content: CourseManagementService;\n private readonly access: ContentAccessReader;\n private readonly uow: ProgressUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: ProgressServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.access = params.access;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async report(orgId: string, input: ReportProgressInput): Promise<ProgressRecord> {\n const activity = await this.content.getActivity(orgId, input.activityId);\n if (!activity || !isActivityPublished(activity.settings)) {\n throw new NotFoundError('Activity', input.activityId);\n }\n const courseId = activity.courseId;\n const course = await this.content.getCourse(orgId, courseId);\n if (\n !course ||\n course.status !== 'published' ||\n !(await this.access.hasAccess(orgId, input.orgUserId, courseId))\n ) {\n throw new NotFoundError('Activity', input.activityId);\n }\n const modules = await this.content.listCourseModules(orgId, courseId);\n const courseActivities = await this.content.listCourseActivities(orgId, courseId);\n return this.uow.run(async (scope) => {\n // Serializes concurrent reports for this student+course (locks are tx-scoped;\n // both racers lock in the same order, the loser waits and then sees committed state).\n const lockIds = [\n ...courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id),\n ...modules.map((m) => m.id),\n courseId,\n ];\n await scope.progress.findByTargets(orgId, input.orgUserId, lockIds, { forUpdate: true });\n const events: NewProgressEvent[] = [];\n let record = await this.ensureActivityRecord(orgId, input, scope, events);\n const state = mergeReports(record.position, input.reports);\n if (state.changed) {\n record = (await scope.progress.update(orgId, record.id, { position: state.map })) ?? record;\n }\n if (!record.completedAt && completionSatisfied(activity.settings, state.claimed)) {\n record =\n (await scope.progress.update(orgId, record.id, {\n completedAt: new Date(),\n })) ?? record;\n events.push(progressEvents.progressCompleted.make({ orgId, data: record }));\n await this.completeContainers(\n orgId,\n input,\n courseId,\n modules,\n courseActivities,\n scope,\n events,\n );\n this.logger.info('progress completed', { orgId, recordId: record.id });\n }\n if (events.length > 0) {\n await scope.outbox.append(events);\n }\n return record;\n });\n }\n\n private async ensureActivityRecord(\n orgId: string,\n input: ReportProgressInput,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<ProgressRecord> {\n const target: ProgressTarget = {\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n };\n const existing = await scope.progress.findByTarget(orgId, target);\n if (existing) {\n return existing;\n }\n const now = new Date();\n const record = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n startedAt: now,\n position: null,\n completedAt: null,\n createdAt: now,\n updatedAt: now,\n });\n if (!record) {\n // Lost a concurrent first-touch insert: the winner owns the row and emitted\n // progress.started. Return its row without re-emitting.\n const winner = await scope.progress.findByTarget(orgId, target);\n if (!winner) {\n throw new Error('progress record vanished after insert conflict');\n }\n return winner;\n }\n events.push(progressEvents.progressStarted.make({ orgId, data: record }));\n this.logger.info('progress started', { orgId, recordId: record.id });\n return record;\n }\n\n /** After an activity completes: newly-complete containers get their records\n * (created complete) and a progress.completed event — same transaction. */\n private async completeContainers(\n orgId: string,\n input: ReportProgressInput,\n courseId: string,\n modules: Module[],\n courseActivities: Activity[],\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const published = courseActivities.filter((a) => isActivityPublished(a.settings));\n const byModule = modules.map((m) => ({\n id: m.id,\n activityIds: published.filter((a) => a.moduleId === m.id).map((a) => a.id),\n }));\n const allIds = byModule.flatMap((m) => m.activityIds);\n const records = await scope.progress.findByTargets(orgId, input.orgUserId, allIds);\n const done = new Set(\n records.filter((r) => r.targetType === 'activity' && r.completedAt).map((r) => r.targetId),\n );\n const containing = byModule.find((m) => m.activityIds.includes(input.activityId));\n if (containing && containing.activityIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'module', containing.id, scope, events);\n }\n if (allIds.length > 0 && allIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'course', courseId, scope, events);\n }\n }\n\n private async ensureContainerComplete(\n orgId: string,\n input: ReportProgressInput,\n targetType: 'module' | 'course',\n targetId: string,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const target: ProgressTarget = { orgUserId: input.orgUserId, targetType, targetId };\n let existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n const now = new Date();\n const inserted = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType,\n targetId,\n startedAt: now,\n position: null,\n completedAt: now,\n createdAt: now,\n updatedAt: now,\n });\n if (inserted) {\n events.push(progressEvents.progressCompleted.make({ orgId, data: inserted }));\n return;\n }\n // Lost a concurrent insert: re-read the winner's row.\n existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n throw new Error('progress container record vanished after insert conflict');\n }\n }\n const record =\n (await scope.progress.update(orgId, existing.id, {\n completedAt: new Date(),\n })) ?? existing;\n events.push(progressEvents.progressCompleted.make({ orgId, data: record }));\n }\n\n get(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n return this.repo.findByTarget(orgId, target);\n }\n\n listByTargets(orgId: string, orgUserId: string, targetIds: string[]): Promise<ProgressRecord[]> {\n return this.repo.findByTargets(orgId, orgUserId, targetIds);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/progress/service.ts"],"sourcesContent":["// progress context — service implementation (inbound port).\n// The frontend only reports usage; this service makes every completion\n// decision. One report batch runs in one UoW transaction: ensure the record,\n// merge item payloads into the per-subject state map, evaluate the activity's\n// completion rule, then module/course against current published structure —\n// appending events with the writes. The container is resolved from the\n// hierarchy (activity → module → course), never supplied by the caller.\n// Percentage/resume are derived by readers, never stored here.\nimport { genId } from '../shared/id.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { ProgressRecord } from './model.js';\nimport type {\n ContentAccessReader,\n CourseReader,\n ProgressRepository,\n ProgressService,\n ProgressUnitOfWork,\n ProgressWriteScope,\n SystemActors,\n} from './ports.js';\nimport type { ProgressReportItem, ProgressTarget, ReportProgressInput } from './types.js';\nimport { progressEvents, type NewProgressEvent } from './events.js';\nimport type { Activity, Module } from '../content/index.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { JsonValue } from '../types/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\n\n/** Mirrors reporting/member: `settings.published === false` is the only draft signal. */\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\n/** Fold a report batch into the record's per-subject state map. Reserved keys\n * (`asset` = subject, `completed` = claim) are stripped; the rest is the asset\n * type's own vocabulary, stored opaquely — latest report per subject wins. */\nfunction mergeReports(\n position: unknown,\n items: ProgressReportItem[],\n): { map: Record<string, JsonValue>; changed: boolean; claimed: boolean } {\n const map = { ...((position as Record<string, JsonValue> | null) ?? {}) };\n let changed = false;\n let claimed = false;\n for (const item of items) {\n const { asset, completed, ...payload } = item;\n if (completed === true) {\n claimed = true;\n }\n if (Object.keys(payload).length > 0) {\n map[asset ?? 'self'] = payload;\n changed = true;\n }\n }\n return { map, changed, claimed };\n}\n\n/** The completion-rule seam. Only `manual` ships: no authored rule → the\n * learner's claim decides. Authored rules are never satisfied until their\n * evaluators exist. Future rules evaluate against the accumulated state map. */\nfunction completionSatisfied(settings: unknown, claimed: boolean): boolean {\n const rule = (settings as { completion?: { rule?: string } } | null)?.completion?.rule;\n if (!rule || rule === 'manual') {\n return claimed;\n }\n return false;\n}\n\ntype Reporter = ReportProgressInput & { orgUserId: string };\n\nexport type ProgressServiceParams = {\n repo: ProgressRepository;\n content: CourseReader;\n access: ContentAccessReader;\n system: SystemActors;\n uow: ProgressUnitOfWork;\n logger?: Logger;\n};\n\nexport class ProgressServiceImpl implements ProgressService {\n private readonly repo: ProgressRepository;\n private readonly content: CourseReader;\n private readonly access: ContentAccessReader;\n private readonly system: SystemActors;\n private readonly uow: ProgressUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: ProgressServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.access = params.access;\n this.system = params.system;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async report(report: ReportProgressInput, actor: Actor): Promise<ProgressRecord> {\n const orgId = actor.orgId;\n const input: Reporter = { ...report, orgUserId: actor.id };\n const system = this.system.systemActor(orgId);\n const activity = await this.content.getActivity(input.activityId, system);\n if (!activity || !isActivityPublished(activity.settings)) {\n throw new NotFoundError('Activity', input.activityId);\n }\n const courseId = activity.courseId;\n const course = await this.content.findCourse(courseId, system);\n const opens =\n course !== null &&\n (actor.permissions.has('courses:read', courseId) ||\n (course.status === 'published' &&\n (await this.access.hasAccess(orgId, actor.id, courseId))));\n if (!opens) {\n this.logger.warn('progress report refused — course not open', {\n orgId,\n activityId: input.activityId,\n actorId: actor.id,\n });\n throw new NotFoundError('Activity', input.activityId);\n }\n const modules = await this.content.listCourseModules(courseId, system);\n const courseActivities = await this.content.listCourseActivities(courseId, system);\n return this.uow.run(async (scope) => {\n // Serializes concurrent reports for this student+course (locks are tx-scoped;\n // both racers lock in the same order, the loser waits and then sees committed state).\n const lockIds = [\n ...courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id),\n ...modules.map((m) => m.id),\n courseId,\n ];\n await scope.progress.findByTargets(orgId, input.orgUserId, lockIds, { forUpdate: true });\n const events: NewProgressEvent[] = [];\n let record = await this.ensureActivityRecord(orgId, input, scope, events);\n const state = mergeReports(record.position, input.reports);\n if (state.changed) {\n record = (await scope.progress.update(orgId, record.id, { position: state.map })) ?? record;\n }\n if (!record.completedAt && completionSatisfied(activity.settings, state.claimed)) {\n record =\n (await scope.progress.update(orgId, record.id, {\n completedAt: new Date(),\n })) ?? record;\n events.push(progressEvents.progressCompleted.make({ orgId, data: record }));\n await this.completeContainers(\n orgId,\n input,\n courseId,\n modules,\n courseActivities,\n scope,\n events,\n );\n this.logger.info('progress completed', { orgId, recordId: record.id });\n }\n if (events.length > 0) {\n await scope.outbox.append(events);\n }\n return record;\n });\n }\n\n private async ensureActivityRecord(\n orgId: string,\n input: Reporter,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<ProgressRecord> {\n const target: ProgressTarget = {\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n };\n const existing = await scope.progress.findByTarget(orgId, target);\n if (existing) {\n return existing;\n }\n const now = new Date();\n const record = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n startedAt: now,\n position: null,\n completedAt: null,\n createdAt: now,\n updatedAt: now,\n });\n if (!record) {\n // Lost a concurrent first-touch insert: the winner owns the row and emitted\n // progress.started. Return its row without re-emitting.\n const winner = await scope.progress.findByTarget(orgId, target);\n if (!winner) {\n throw new Error('progress record vanished after insert conflict');\n }\n return winner;\n }\n events.push(progressEvents.progressStarted.make({ orgId, data: record }));\n this.logger.info('progress started', { orgId, recordId: record.id });\n return record;\n }\n\n /** After an activity completes: newly-complete containers get their records\n * (created complete) and a progress.completed event — same transaction. */\n private async completeContainers(\n orgId: string,\n input: Reporter,\n courseId: string,\n modules: Module[],\n courseActivities: Activity[],\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const published = courseActivities.filter((a) => isActivityPublished(a.settings));\n const byModule = modules.map((m) => ({\n id: m.id,\n activityIds: published.filter((a) => a.moduleId === m.id).map((a) => a.id),\n }));\n const allIds = byModule.flatMap((m) => m.activityIds);\n const records = await scope.progress.findByTargets(orgId, input.orgUserId, allIds);\n const done = new Set(\n records.filter((r) => r.targetType === 'activity' && r.completedAt).map((r) => r.targetId),\n );\n const containing = byModule.find((m) => m.activityIds.includes(input.activityId));\n if (containing && containing.activityIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'module', containing.id, scope, events);\n }\n if (allIds.length > 0 && allIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'course', courseId, scope, events);\n }\n }\n\n private async ensureContainerComplete(\n orgId: string,\n input: Reporter,\n targetType: 'module' | 'course',\n targetId: string,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const target: ProgressTarget = { orgUserId: input.orgUserId, targetType, targetId };\n let existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n const now = new Date();\n const inserted = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType,\n targetId,\n startedAt: now,\n position: null,\n completedAt: now,\n createdAt: now,\n updatedAt: now,\n });\n if (inserted) {\n events.push(progressEvents.progressCompleted.make({ orgId, data: inserted }));\n return;\n }\n // Lost a concurrent insert: re-read the winner's row.\n existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n throw new Error('progress container record vanished after insert conflict');\n }\n }\n const record =\n (await scope.progress.update(orgId, existing.id, {\n completedAt: new Date(),\n })) ?? existing;\n events.push(progressEvents.progressCompleted.make({ orgId, data: record }));\n }\n\n get(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n return this.repo.findByTarget(orgId, target);\n }\n\n listByTargets(orgId: string, orgUserId: string, targetIds: string[]): Promise<ProgressRecord[]> {\n return this.repo.findByTargets(orgId, orgUserId, targetIds);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { EventAnalytics, EventAttendee } from "../../types/schemas/content.js";
|
|
2
|
+
import { EventAttendeeRow } from "../events/index.js";
|
|
3
|
+
import { CommunityAnalytics, CommunityMember, CommunityMembersQuery, CourseActivityEngagement, CourseAnalytics, CourseEnrollmentPoint, CourseLearner, CourseLearnersQuery, EnrollmentPoint, LearnerGrantStatus, OverviewStats, Page, Student, StudentAnalytics, StudentCourseProgress, StudentItem, StudentRow, StudentsQuery } from "./model.js";
|
|
4
|
+
import { AdminReportRepository, AdminReportService, PermissionsReader, SpaceOpenersReader } from "./ports.js";
|
|
5
|
+
import { AdminReportServiceImpl, AdminReportServiceParams } from "./service.js";
|
|
6
|
+
export { type AdminReportRepository, type AdminReportService, AdminReportServiceImpl, type AdminReportServiceParams, type CommunityAnalytics, type CommunityMember, type CommunityMembersQuery, type CourseActivityEngagement, type CourseAnalytics, type CourseEnrollmentPoint, type CourseLearner, type CourseLearnersQuery, type EnrollmentPoint, type EventAnalytics, type EventAttendee, type EventAttendeeRow, type LearnerGrantStatus, type OverviewStats, type Page, type PermissionsReader, type SpaceOpenersReader, type Student, type StudentAnalytics, type StudentCourseProgress, type StudentItem, type StudentRow, type StudentsQuery };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ContentType, EventAnalytics, EventAttendee } from "../../types/schemas/content.js";
|
|
2
|
+
import { OrgUserProfile, OrgUserStatus } from "../../types/schemas/organizations.js";
|
|
2
3
|
import "../../types/index.js";
|
|
4
|
+
import { ItemRole } from "../../organizations/members.js";
|
|
5
|
+
import "../../organizations/index.js";
|
|
6
|
+
import { EventAttendeeRow } from "../events/index.js";
|
|
3
7
|
//#region src/reporting/admin/model.d.ts
|
|
4
8
|
interface SpacePostCount {
|
|
5
9
|
spaceId: string;
|
|
@@ -25,6 +29,7 @@ interface OverviewStats {
|
|
|
25
29
|
draftCourses: number;
|
|
26
30
|
activeStudents: number;
|
|
27
31
|
expiringSoon: number;
|
|
32
|
+
upcomingEvents: number;
|
|
28
33
|
}
|
|
29
34
|
interface EnrollmentPoint {
|
|
30
35
|
date: string;
|
|
@@ -62,6 +67,15 @@ interface Student extends OrgUserProfile {
|
|
|
62
67
|
status: OrgUserStatus;
|
|
63
68
|
joinedAt: string;
|
|
64
69
|
lastActiveAt: string | null;
|
|
70
|
+
/** The roles they hold on content items. */
|
|
71
|
+
items: ItemRole[];
|
|
72
|
+
}
|
|
73
|
+
interface StudentItem {
|
|
74
|
+
contentId: string;
|
|
75
|
+
contentType: ContentType;
|
|
76
|
+
}
|
|
77
|
+
interface StudentRow extends Omit<Student, 'items'> {
|
|
78
|
+
items: StudentItem[];
|
|
65
79
|
}
|
|
66
80
|
interface StudentCourseProgress {
|
|
67
81
|
courseId: string;
|
|
@@ -84,7 +98,26 @@ interface StudentAnalytics {
|
|
|
84
98
|
courses: StudentCourseProgress[];
|
|
85
99
|
}
|
|
86
100
|
interface CommunityMember extends OrgUserProfile {
|
|
87
|
-
|
|
101
|
+
team: boolean;
|
|
102
|
+
roleName: string | null;
|
|
103
|
+
opensEverySpace: boolean;
|
|
104
|
+
}
|
|
105
|
+
type LearnerGrantStatus = 'active' | 'expired' | 'revoked';
|
|
106
|
+
interface CourseLearner extends OrgUserProfile {
|
|
107
|
+
status: LearnerGrantStatus;
|
|
108
|
+
source: string;
|
|
109
|
+
grantedAt: string;
|
|
110
|
+
expiresAt: string | null;
|
|
111
|
+
totalActivities: number;
|
|
112
|
+
completedActivities: number;
|
|
113
|
+
progress: number;
|
|
114
|
+
lastActivityAt: string | null;
|
|
115
|
+
completedAt: string | null;
|
|
116
|
+
}
|
|
117
|
+
interface CourseLearnersQuery {
|
|
118
|
+
page: number;
|
|
119
|
+
pageSize: number;
|
|
120
|
+
search?: string | undefined;
|
|
88
121
|
}
|
|
89
122
|
interface CommunityMembersQuery {
|
|
90
123
|
page: number;
|
|
@@ -104,5 +137,5 @@ interface Page<T> {
|
|
|
104
137
|
pageSize: number;
|
|
105
138
|
}
|
|
106
139
|
//#endregion
|
|
107
|
-
export { CommunityAnalytics, CommunityMember, CommunityMembersQuery, CourseActivityEngagement, CourseAnalytics, CourseEnrollmentPoint, EnrollmentPoint, OverviewStats, Page, PostCommentCount, SpacePostCount, Student, StudentAnalytics, StudentCourseProgress, StudentsQuery };
|
|
140
|
+
export { CommunityAnalytics, CommunityMember, CommunityMembersQuery, CourseActivityEngagement, CourseAnalytics, CourseEnrollmentPoint, CourseLearner, CourseLearnersQuery, EnrollmentPoint, type EventAnalytics, type EventAttendee, type EventAttendeeRow, LearnerGrantStatus, OverviewStats, Page, PostCommentCount, SpacePostCount, Student, StudentAnalytics, StudentCourseProgress, StudentItem, StudentRow, StudentsQuery };
|
|
108
141
|
//# sourceMappingURL=model.d.ts.map
|
|
@@ -1,27 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EventAnalytics, EventAttendee } from "../../types/schemas/content.js";
|
|
2
|
+
import { OrgUserProfile } from "../../types/schemas/organizations.js";
|
|
3
|
+
import "../../types/index.js";
|
|
4
|
+
import { Actor, EffectivePermissions } from "../../shared/actor.js";
|
|
5
|
+
import { EventAttendeeRow } from "../events/index.js";
|
|
6
|
+
import { CommunityAnalytics, CommunityMember, CommunityMembersQuery, CourseAnalytics, CourseEnrollmentPoint, CourseLearner, CourseLearnersQuery, EnrollmentPoint, OverviewStats, Page, Student, StudentAnalytics, StudentRow, StudentsQuery } from "./model.js";
|
|
2
7
|
//#region src/reporting/admin/ports.d.ts
|
|
3
8
|
interface AdminReportService {
|
|
4
|
-
overview(
|
|
5
|
-
enrollments(
|
|
6
|
-
courseAnalytics(
|
|
7
|
-
courseEnrollments(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
overview(actor: Actor): Promise<OverviewStats>;
|
|
10
|
+
enrollments(days: number, actor: Actor): Promise<EnrollmentPoint[]>;
|
|
11
|
+
courseAnalytics(courseId: string, actor: Actor): Promise<CourseAnalytics>;
|
|
12
|
+
courseEnrollments(courseId: string, days: number, actor: Actor): Promise<CourseEnrollmentPoint[]>;
|
|
13
|
+
courseLearners(courseId: string, query: CourseLearnersQuery, actor: Actor): Promise<Page<CourseLearner>>;
|
|
14
|
+
listStudents(query: StudentsQuery, actor: Actor): Promise<Page<Student>>;
|
|
15
|
+
getStudent(id: string, actor: Actor): Promise<Student>;
|
|
16
|
+
studentAnalytics(id: string, actor: Actor): Promise<StudentAnalytics>;
|
|
17
|
+
communityAnalytics(communityId: string, actor: Actor): Promise<CommunityAnalytics>;
|
|
18
|
+
communityMembers(communityId: string, query: CommunityMembersQuery, actor: Actor): Promise<Page<CommunityMember>>;
|
|
19
|
+
eventAttendees(eventId: string, query: {
|
|
20
|
+
page: number;
|
|
21
|
+
pageSize: number;
|
|
22
|
+
}, actor: Actor): Promise<Page<EventAttendee>>;
|
|
23
|
+
eventAnalytics(eventId: string, actor: Actor): Promise<EventAnalytics>;
|
|
24
|
+
}
|
|
25
|
+
interface PermissionsReader {
|
|
26
|
+
permissionsOfMany(orgId: string, orgUserIds: string[]): Promise<Map<string, EffectivePermissions>>;
|
|
27
|
+
}
|
|
28
|
+
interface SpaceOpenersReader {
|
|
29
|
+
everySpaceOpeners(orgId: string, communityId: string): Promise<string[]>;
|
|
13
30
|
}
|
|
14
31
|
interface AdminReportRepository {
|
|
15
32
|
overview(orgId: string): Promise<OverviewStats>;
|
|
16
33
|
enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]>;
|
|
17
34
|
courseAnalytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
|
|
18
35
|
courseEnrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
courseLearners(orgId: string, courseId: string, query: CourseLearnersQuery): Promise<Page<CourseLearner> | null>;
|
|
37
|
+
/** Active and invited org users whose org role is `memberRoleKey`: no org assignment,
|
|
38
|
+
* and for an invited one, a pending invite carrying that key. */
|
|
39
|
+
listStudents(orgId: string, query: StudentsQuery, memberRoleKey: string): Promise<Page<StudentRow>>;
|
|
40
|
+
getStudent(orgId: string, id: string): Promise<StudentRow | null>;
|
|
21
41
|
studentAnalytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
|
|
22
42
|
communityAnalytics(orgId: string, communityId: string): Promise<CommunityAnalytics | null>;
|
|
23
|
-
communityMembers(orgId: string, communityId: string, query: CommunityMembersQuery): Promise<Page<
|
|
43
|
+
communityMembers(orgId: string, communityId: string, query: CommunityMembersQuery): Promise<Page<OrgUserProfile> | null>;
|
|
44
|
+
eventAttendees(orgId: string, eventId: string, page: {
|
|
45
|
+
page: number;
|
|
46
|
+
pageSize: number;
|
|
47
|
+
}): Promise<Page<EventAttendeeRow>>;
|
|
48
|
+
eventAnalytics(orgId: string, eventId: string, startsAt: Date): Promise<Omit<EventAnalytics, 'capacity'>>;
|
|
24
49
|
}
|
|
25
50
|
//#endregion
|
|
26
|
-
export { AdminReportRepository, AdminReportService };
|
|
51
|
+
export { AdminReportRepository, AdminReportService, PermissionsReader, SpaceOpenersReader };
|
|
27
52
|
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -1,21 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { EventAnalytics, EventAttendee } from "../../types/schemas/content.js";
|
|
2
|
+
import { Actor } from "../../shared/actor.js";
|
|
3
|
+
import { FileUrls } from "../../assets/file-urls.js";
|
|
4
|
+
import "../../assets/index.js";
|
|
5
|
+
import { EventsService } from "../../content/ports.js";
|
|
6
|
+
import "../../content/index.js";
|
|
7
|
+
import { CommunityAnalytics, CommunityMember, CommunityMembersQuery, CourseAnalytics, CourseEnrollmentPoint, CourseLearner, CourseLearnersQuery, EnrollmentPoint, OverviewStats, Page, Student, StudentAnalytics, StudentsQuery } from "./model.js";
|
|
8
|
+
import { AdminReportRepository, AdminReportService, PermissionsReader, SpaceOpenersReader } from "./ports.js";
|
|
3
9
|
//#region src/reporting/admin/service.d.ts
|
|
4
10
|
type AdminReportServiceParams = {
|
|
5
11
|
repo: AdminReportRepository;
|
|
12
|
+
permissions: PermissionsReader;
|
|
13
|
+
spaces: SpaceOpenersReader;
|
|
14
|
+
events: Pick<EventsService, 'findEvent'>;
|
|
15
|
+
fileUrls: Pick<FileUrls, 'publicImage'>;
|
|
6
16
|
};
|
|
7
17
|
declare class AdminReportServiceImpl implements AdminReportService {
|
|
8
18
|
private readonly repo;
|
|
19
|
+
private readonly permissions;
|
|
20
|
+
private readonly spaces;
|
|
21
|
+
private readonly events;
|
|
22
|
+
private readonly fileUrls;
|
|
9
23
|
constructor(params: AdminReportServiceParams);
|
|
10
|
-
overview(
|
|
11
|
-
enrollments(
|
|
12
|
-
courseAnalytics(
|
|
13
|
-
courseEnrollments(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
overview(actor: Actor): Promise<OverviewStats>;
|
|
25
|
+
enrollments(days: number, actor: Actor): Promise<EnrollmentPoint[]>;
|
|
26
|
+
courseAnalytics(courseId: string, actor: Actor): Promise<CourseAnalytics>;
|
|
27
|
+
courseEnrollments(courseId: string, days: number, actor: Actor): Promise<CourseEnrollmentPoint[]>;
|
|
28
|
+
courseLearners(courseId: string, query: CourseLearnersQuery, actor: Actor): Promise<Page<CourseLearner>>;
|
|
29
|
+
listStudents(query: StudentsQuery, actor: Actor): Promise<Page<Student>>;
|
|
30
|
+
getStudent(id: string, actor: Actor): Promise<Student>;
|
|
31
|
+
studentAnalytics(id: string, actor: Actor): Promise<StudentAnalytics>;
|
|
32
|
+
communityAnalytics(communityId: string, actor: Actor): Promise<CommunityAnalytics>;
|
|
33
|
+
communityMembers(communityId: string, query: CommunityMembersQuery, actor: Actor): Promise<Page<CommunityMember>>;
|
|
34
|
+
eventAttendees(eventId: string, query: {
|
|
35
|
+
page: number;
|
|
36
|
+
pageSize: number;
|
|
37
|
+
}, actor: Actor): Promise<Page<EventAttendee>>;
|
|
38
|
+
eventAnalytics(eventId: string, actor: Actor): Promise<EventAnalytics>;
|
|
39
|
+
private withItemRoles;
|
|
19
40
|
}
|
|
20
41
|
//#endregion
|
|
21
42
|
export { AdminReportServiceImpl, AdminReportServiceParams };
|
|
@@ -1,37 +1,121 @@
|
|
|
1
|
+
import { NotFoundError } from "../../shared/errors.js";
|
|
2
|
+
import { MEMBER_ROLE_KEY } from "../../organizations/default-roles.js";
|
|
3
|
+
import { attendeeView } from "../events/index.js";
|
|
1
4
|
//#region src/reporting/admin/service.ts
|
|
2
5
|
var AdminReportServiceImpl = class {
|
|
3
6
|
repo;
|
|
7
|
+
permissions;
|
|
8
|
+
spaces;
|
|
9
|
+
events;
|
|
10
|
+
fileUrls;
|
|
4
11
|
constructor(params) {
|
|
5
12
|
this.repo = params.repo;
|
|
13
|
+
this.permissions = params.permissions;
|
|
14
|
+
this.spaces = params.spaces;
|
|
15
|
+
this.events = params.events;
|
|
16
|
+
this.fileUrls = params.fileUrls;
|
|
6
17
|
}
|
|
7
|
-
overview(
|
|
8
|
-
|
|
18
|
+
async overview(actor) {
|
|
19
|
+
actor.permissions.require("analytics:read");
|
|
20
|
+
return this.repo.overview(actor.orgId);
|
|
9
21
|
}
|
|
10
|
-
enrollments(
|
|
11
|
-
|
|
22
|
+
async enrollments(days, actor) {
|
|
23
|
+
actor.permissions.require("analytics:read");
|
|
24
|
+
return this.repo.enrollments(actor.orgId, days);
|
|
12
25
|
}
|
|
13
|
-
courseAnalytics(
|
|
14
|
-
|
|
26
|
+
async courseAnalytics(courseId, actor) {
|
|
27
|
+
actor.permissions.require("progress:read", courseId);
|
|
28
|
+
return found(await this.repo.courseAnalytics(actor.orgId, courseId), "Course", courseId);
|
|
15
29
|
}
|
|
16
|
-
courseEnrollments(
|
|
17
|
-
|
|
30
|
+
async courseEnrollments(courseId, days, actor) {
|
|
31
|
+
actor.permissions.require("progress:read", courseId);
|
|
32
|
+
return found(await this.repo.courseEnrollments(actor.orgId, courseId, days), "Course", courseId);
|
|
18
33
|
}
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
async courseLearners(courseId, query, actor) {
|
|
35
|
+
actor.permissions.require("progress:read", courseId);
|
|
36
|
+
return found(await this.repo.courseLearners(actor.orgId, courseId, query), "Course", courseId);
|
|
21
37
|
}
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
async listStudents(query, actor) {
|
|
39
|
+
actor.permissions.require("people:read");
|
|
40
|
+
const page = await this.repo.listStudents(actor.orgId, query, MEMBER_ROLE_KEY);
|
|
41
|
+
return {
|
|
42
|
+
...page,
|
|
43
|
+
rows: await this.withItemRoles(actor.orgId, page.rows)
|
|
44
|
+
};
|
|
24
45
|
}
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
async getStudent(id, actor) {
|
|
47
|
+
actor.permissions.require("people:read");
|
|
48
|
+
const row = found(await this.repo.getStudent(actor.orgId, id), "Student", id);
|
|
49
|
+
const [student] = await this.withItemRoles(actor.orgId, [row]);
|
|
50
|
+
return found(student, "Student", id);
|
|
27
51
|
}
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
async studentAnalytics(id, actor) {
|
|
53
|
+
actor.permissions.require("people:read");
|
|
54
|
+
return found(await this.repo.studentAnalytics(actor.orgId, id), "Student", id);
|
|
30
55
|
}
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
async communityAnalytics(communityId, actor) {
|
|
57
|
+
actor.permissions.require("communities:read", communityId);
|
|
58
|
+
return found(await this.repo.communityAnalytics(actor.orgId, communityId), "Community", communityId);
|
|
59
|
+
}
|
|
60
|
+
async communityMembers(communityId, query, actor) {
|
|
61
|
+
actor.permissions.require("access:read");
|
|
62
|
+
const page = found(await this.repo.communityMembers(actor.orgId, communityId, query), "Community", communityId);
|
|
63
|
+
const [held, openers] = await Promise.all([this.permissions.permissionsOfMany(actor.orgId, page.rows.map((row) => row.id)), this.spaces.everySpaceOpeners(actor.orgId, communityId)]);
|
|
64
|
+
const opensEverySpace = new Set(openers);
|
|
65
|
+
return {
|
|
66
|
+
...page,
|
|
67
|
+
rows: page.rows.map((row) => {
|
|
68
|
+
const permissions = held.get(row.id);
|
|
69
|
+
const item = permissions?.roleOn(communityId);
|
|
70
|
+
const org = permissions?.roleOn();
|
|
71
|
+
const team = (item?.team ?? false) || (org?.team ?? false);
|
|
72
|
+
const roleName = item?.team ? item.name : org?.team ? org.name : null;
|
|
73
|
+
return {
|
|
74
|
+
...row,
|
|
75
|
+
team,
|
|
76
|
+
roleName,
|
|
77
|
+
opensEverySpace: opensEverySpace.has(row.id)
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async eventAttendees(eventId, query, actor) {
|
|
83
|
+
actor.permissions.require("events:read", eventId);
|
|
84
|
+
found(await this.events.findEvent(eventId, actor), "Event", eventId);
|
|
85
|
+
const page = await this.repo.eventAttendees(actor.orgId, eventId, query);
|
|
86
|
+
return {
|
|
87
|
+
...page,
|
|
88
|
+
rows: page.rows.map((row) => attendeeView(actor.orgId, row, this.fileUrls))
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async eventAnalytics(eventId, actor) {
|
|
92
|
+
actor.permissions.require("events:read", eventId);
|
|
93
|
+
const event = found(await this.events.findEvent(eventId, actor), "Event", eventId);
|
|
94
|
+
return {
|
|
95
|
+
...await this.repo.eventAnalytics(actor.orgId, eventId, event.startsAt),
|
|
96
|
+
capacity: event.settings.capacity
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async withItemRoles(orgId, rows) {
|
|
100
|
+
const ids = rows.filter((row) => row.items.length > 0).map((row) => row.id);
|
|
101
|
+
const held = ids.length > 0 ? await this.permissions.permissionsOfMany(orgId, ids) : /* @__PURE__ */ new Map();
|
|
102
|
+
return rows.map((row) => ({
|
|
103
|
+
...row,
|
|
104
|
+
items: row.items.flatMap((item) => {
|
|
105
|
+
const role = held.get(row.id)?.roleOn(item.contentId);
|
|
106
|
+
return role ? [{
|
|
107
|
+
...item,
|
|
108
|
+
roleKey: role.key,
|
|
109
|
+
roleName: role.name
|
|
110
|
+
}] : [];
|
|
111
|
+
})
|
|
112
|
+
}));
|
|
33
113
|
}
|
|
34
114
|
};
|
|
115
|
+
function found(value, what, id) {
|
|
116
|
+
if (value === null || value === void 0) throw new NotFoundError(what, id);
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
35
119
|
//#endregion
|
|
36
120
|
export { AdminReportServiceImpl };
|
|
37
121
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/admin/service.ts"],"sourcesContent":["import type {\n CommunityAnalytics,\n CommunityMember,\n CommunityMembersQuery,\n CourseAnalytics,\n CourseEnrollmentPoint,\n EnrollmentPoint,\n OverviewStats,\n Page,\n Student,\n StudentAnalytics,\n StudentsQuery,\n} from './model.js';\nimport type { AdminReportRepository, AdminReportService } from './ports.js';\n\nexport type AdminReportServiceParams = {\n repo: AdminReportRepository;\n};\n\nexport class AdminReportServiceImpl implements AdminReportService {\n private readonly repo: AdminReportRepository;\n\n constructor(params: AdminReportServiceParams) {\n this.repo = params.repo;\n }\n\n overview(orgId: string): Promise<OverviewStats> {\n return this.repo.overview(orgId);\n }\n\n enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]> {\n return this.repo.enrollments(orgId, days);\n }\n\n courseAnalytics(orgId: string, courseId: string): Promise<CourseAnalytics | null> {\n return this.repo.courseAnalytics(orgId, courseId);\n }\n\n courseEnrollments(\n orgId: string,\n courseId: string,\n days: number,\n ): Promise<CourseEnrollmentPoint[] | null> {\n return this.repo.courseEnrollments(orgId, courseId, days);\n }\n\n listStudents(orgId: string, query: StudentsQuery): Promise<Page<Student>> {\n return this.repo.listStudents(orgId, query);\n }\n\n getStudent(orgId: string, id: string): Promise<Student | null> {\n return this.repo.getStudent(orgId, id);\n }\n\n studentAnalytics(orgId: string, id: string): Promise<StudentAnalytics | null> {\n return this.repo.studentAnalytics(orgId, id);\n }\n\n communityAnalytics(orgId: string, communityId: string): Promise<CommunityAnalytics | null> {\n return this.repo.communityAnalytics(orgId, communityId);\n }\n\n communityMembers(\n orgId: string,\n communityId: string,\n query: CommunityMembersQuery,\n ): Promise<Page<CommunityMember> | null> {\n return this.repo.communityMembers(orgId, communityId, query);\n }\n}\n"],"mappings":";AAmBA,IAAa,yBAAb,MAAkE;CAChE;CAEA,YAAY,QAAkC;EAC5C,KAAK,OAAO,OAAO;CACrB;CAEA,SAAS,OAAuC;EAC9C,OAAO,KAAK,KAAK,SAAS,KAAK;CACjC;CAEA,YAAY,OAAe,MAA0C;EACnE,OAAO,KAAK,KAAK,YAAY,OAAO,IAAI;CAC1C;CAEA,gBAAgB,OAAe,UAAmD;EAChF,OAAO,KAAK,KAAK,gBAAgB,OAAO,QAAQ;CAClD;CAEA,kBACE,OACA,UACA,MACyC;EACzC,OAAO,KAAK,KAAK,kBAAkB,OAAO,UAAU,IAAI;CAC1D;CAEA,aAAa,OAAe,OAA8C;EACxE,OAAO,KAAK,KAAK,aAAa,OAAO,KAAK;CAC5C;CAEA,WAAW,OAAe,IAAqC;EAC7D,OAAO,KAAK,KAAK,WAAW,OAAO,EAAE;CACvC;CAEA,iBAAiB,OAAe,IAA8C;EAC5E,OAAO,KAAK,KAAK,iBAAiB,OAAO,EAAE;CAC7C;CAEA,mBAAmB,OAAe,aAAyD;EACzF,OAAO,KAAK,KAAK,mBAAmB,OAAO,WAAW;CACxD;CAEA,iBACE,OACA,aACA,OACuC;EACvC,OAAO,KAAK,KAAK,iBAAiB,OAAO,aAAa,KAAK;CAC7D;AACF"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/admin/service.ts"],"sourcesContent":["import type { Actor } from '../../shared/actor.js';\nimport { NotFoundError } from '../../shared/errors.js';\nimport { MEMBER_ROLE_KEY } from '../../organizations/index.js';\nimport type { EventsService } from '../../content/index.js';\nimport type { FileUrls } from '../../assets/index.js';\nimport type {\n CommunityAnalytics,\n CommunityMember,\n CommunityMembersQuery,\n CourseAnalytics,\n CourseEnrollmentPoint,\n CourseLearner,\n CourseLearnersQuery,\n EnrollmentPoint,\n EventAnalytics,\n EventAttendee,\n OverviewStats,\n Page,\n Student,\n StudentAnalytics,\n StudentRow,\n StudentsQuery,\n} from './model.js';\nimport type {\n AdminReportRepository,\n AdminReportService,\n PermissionsReader,\n SpaceOpenersReader,\n} from './ports.js';\nimport { attendeeView } from '../events/index.js';\n\nexport type AdminReportServiceParams = {\n repo: AdminReportRepository;\n permissions: PermissionsReader;\n spaces: SpaceOpenersReader;\n events: Pick<EventsService, 'findEvent'>;\n fileUrls: Pick<FileUrls, 'publicImage'>;\n};\n\nexport class AdminReportServiceImpl implements AdminReportService {\n private readonly repo: AdminReportRepository;\n private readonly permissions: PermissionsReader;\n private readonly spaces: SpaceOpenersReader;\n private readonly events: Pick<EventsService, 'findEvent'>;\n private readonly fileUrls: Pick<FileUrls, 'publicImage'>;\n\n constructor(params: AdminReportServiceParams) {\n this.repo = params.repo;\n this.permissions = params.permissions;\n this.spaces = params.spaces;\n this.events = params.events;\n this.fileUrls = params.fileUrls;\n }\n\n async overview(actor: Actor): Promise<OverviewStats> {\n actor.permissions.require('analytics:read');\n return this.repo.overview(actor.orgId);\n }\n\n async enrollments(days: number, actor: Actor): Promise<EnrollmentPoint[]> {\n actor.permissions.require('analytics:read');\n return this.repo.enrollments(actor.orgId, days);\n }\n\n async courseAnalytics(courseId: string, actor: Actor): Promise<CourseAnalytics> {\n actor.permissions.require('progress:read', courseId);\n return found(await this.repo.courseAnalytics(actor.orgId, courseId), 'Course', courseId);\n }\n\n async courseEnrollments(\n courseId: string,\n days: number,\n actor: Actor,\n ): Promise<CourseEnrollmentPoint[]> {\n actor.permissions.require('progress:read', courseId);\n return found(\n await this.repo.courseEnrollments(actor.orgId, courseId, days),\n 'Course',\n courseId,\n );\n }\n\n async courseLearners(\n courseId: string,\n query: CourseLearnersQuery,\n actor: Actor,\n ): Promise<Page<CourseLearner>> {\n actor.permissions.require('progress:read', courseId);\n return found(await this.repo.courseLearners(actor.orgId, courseId, query), 'Course', courseId);\n }\n\n async listStudents(query: StudentsQuery, actor: Actor): Promise<Page<Student>> {\n actor.permissions.require('people:read');\n const page = await this.repo.listStudents(actor.orgId, query, MEMBER_ROLE_KEY);\n return { ...page, rows: await this.withItemRoles(actor.orgId, page.rows) };\n }\n\n async getStudent(id: string, actor: Actor): Promise<Student> {\n actor.permissions.require('people:read');\n const row = found(await this.repo.getStudent(actor.orgId, id), 'Student', id);\n const [student] = await this.withItemRoles(actor.orgId, [row]);\n return found(student, 'Student', id);\n }\n\n async studentAnalytics(id: string, actor: Actor): Promise<StudentAnalytics> {\n actor.permissions.require('people:read');\n return found(await this.repo.studentAnalytics(actor.orgId, id), 'Student', id);\n }\n\n async communityAnalytics(communityId: string, actor: Actor): Promise<CommunityAnalytics> {\n actor.permissions.require('communities:read', communityId);\n return found(\n await this.repo.communityAnalytics(actor.orgId, communityId),\n 'Community',\n communityId,\n );\n }\n\n async communityMembers(\n communityId: string,\n query: CommunityMembersQuery,\n actor: Actor,\n ): Promise<Page<CommunityMember>> {\n actor.permissions.require('access:read');\n const page = found(\n await this.repo.communityMembers(actor.orgId, communityId, query),\n 'Community',\n communityId,\n );\n const [held, openers] = await Promise.all([\n this.permissions.permissionsOfMany(\n actor.orgId,\n page.rows.map((row) => row.id),\n ),\n this.spaces.everySpaceOpeners(actor.orgId, communityId),\n ]);\n const opensEverySpace = new Set(openers);\n return {\n ...page,\n rows: page.rows.map((row) => {\n const permissions = held.get(row.id);\n const item = permissions?.roleOn(communityId);\n const org = permissions?.roleOn();\n const team = (item?.team ?? false) || (org?.team ?? false);\n const roleName = item?.team ? item.name : org?.team ? org.name : null;\n return { ...row, team, roleName, opensEverySpace: opensEverySpace.has(row.id) };\n }),\n };\n }\n\n async eventAttendees(\n eventId: string,\n query: { page: number; pageSize: number },\n actor: Actor,\n ): Promise<Page<EventAttendee>> {\n actor.permissions.require('events:read', eventId);\n found(await this.events.findEvent(eventId, actor), 'Event', eventId);\n const page = await this.repo.eventAttendees(actor.orgId, eventId, query);\n return {\n ...page,\n rows: page.rows.map((row) => attendeeView(actor.orgId, row, this.fileUrls)),\n };\n }\n\n async eventAnalytics(eventId: string, actor: Actor): Promise<EventAnalytics> {\n actor.permissions.require('events:read', eventId);\n const event = found(await this.events.findEvent(eventId, actor), 'Event', eventId);\n const figures = await this.repo.eventAnalytics(actor.orgId, eventId, event.startsAt);\n return { ...figures, capacity: event.settings.capacity };\n }\n\n private async withItemRoles(orgId: string, rows: StudentRow[]): Promise<Student[]> {\n const ids = rows.filter((row) => row.items.length > 0).map((row) => row.id);\n const held = ids.length > 0 ? await this.permissions.permissionsOfMany(orgId, ids) : new Map();\n return rows.map((row) => ({\n ...row,\n items: row.items.flatMap((item) => {\n const role = held.get(row.id)?.roleOn(item.contentId);\n return role ? [{ ...item, roleKey: role.key, roleName: role.name }] : [];\n }),\n }));\n }\n}\n\nfunction found<T>(value: T | null | undefined, what: string, id: string): T {\n if (value === null || value === undefined) {\n throw new NotFoundError(what, id);\n }\n return value;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EventAttendee, EventAttendeeFace, EventAttendeeSource } from "../../types/schemas/content.js";
|
|
2
|
+
import "../../types/index.js";
|
|
3
|
+
import { FileUrls } from "../../assets/file-urls.js";
|
|
4
|
+
import "../../assets/index.js";
|
|
5
|
+
//#region src/reporting/events/index.d.ts
|
|
6
|
+
interface EventAttendeeRow {
|
|
7
|
+
orgUserId: string;
|
|
8
|
+
displayName: string | null;
|
|
9
|
+
imageAssetId: string | null;
|
|
10
|
+
source: EventAttendeeSource;
|
|
11
|
+
grantedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
type EventFaceRow = Pick<EventAttendeeRow, 'orgUserId' | 'displayName' | 'imageAssetId'>;
|
|
14
|
+
interface EventFaces {
|
|
15
|
+
total: number;
|
|
16
|
+
rows: EventFaceRow[];
|
|
17
|
+
}
|
|
18
|
+
interface EventReader {
|
|
19
|
+
orgUserId: string;
|
|
20
|
+
readAll: boolean;
|
|
21
|
+
readIds: string[];
|
|
22
|
+
}
|
|
23
|
+
declare function attendeeFace(orgId: string, row: EventFaceRow, fileUrls: Pick<FileUrls, 'publicImage'>): EventAttendeeFace;
|
|
24
|
+
declare function attendeeView(orgId: string, row: EventAttendeeRow, fileUrls: Pick<FileUrls, 'publicImage'>): EventAttendee;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { EventAttendeeRow, EventFaceRow, EventFaces, EventReader, attendeeFace, attendeeView };
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "../../identity/service.js";
|
|
2
|
+
//#region src/reporting/events/index.ts
|
|
3
|
+
function attendeeFace(orgId, row, fileUrls) {
|
|
4
|
+
return {
|
|
5
|
+
orgUserId: row.orgUserId,
|
|
6
|
+
name: row.displayName ?? "A member",
|
|
7
|
+
avatarUrl: row.imageAssetId ? fileUrls.publicImage(orgId, row.orgUserId, row.imageAssetId) : null
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function attendeeView(orgId, row, fileUrls) {
|
|
11
|
+
return {
|
|
12
|
+
...attendeeFace(orgId, row, fileUrls),
|
|
13
|
+
source: row.source,
|
|
14
|
+
grantedAt: row.grantedAt
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { attendeeFace, attendeeView };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/reporting/events/index.ts"],"sourcesContent":["import type { FileUrls } from '../../assets/index.js';\nimport { UNNAMED_MEMBER } from '../../identity/index.js';\nimport type { EventAttendee, EventAttendeeFace, EventAttendeeSource } from '../../types/index.js';\n\nexport interface EventAttendeeRow {\n orgUserId: string;\n displayName: string | null;\n imageAssetId: string | null;\n source: EventAttendeeSource;\n grantedAt: Date;\n}\n\nexport type EventFaceRow = Pick<EventAttendeeRow, 'orgUserId' | 'displayName' | 'imageAssetId'>;\n\nexport interface EventFaces {\n total: number;\n rows: EventFaceRow[];\n}\n\nexport interface EventReader {\n orgUserId: string;\n readAll: boolean;\n readIds: string[];\n}\n\nexport function attendeeFace(\n orgId: string,\n row: EventFaceRow,\n fileUrls: Pick<FileUrls, 'publicImage'>,\n): EventAttendeeFace {\n return {\n orgUserId: row.orgUserId,\n name: row.displayName ?? UNNAMED_MEMBER,\n avatarUrl: row.imageAssetId\n ? fileUrls.publicImage(orgId, row.orgUserId, row.imageAssetId)\n : null,\n };\n}\n\nexport function attendeeView(\n orgId: string,\n row: EventAttendeeRow,\n fileUrls: Pick<FileUrls, 'publicImage'>,\n): EventAttendee {\n return { ...attendeeFace(orgId, row, fileUrls), source: row.source, grantedAt: row.grantedAt };\n}\n"],"mappings":";;AAyBA,SAAgB,aACd,OACA,KACA,UACmB;CACnB,OAAO;EACL,WAAW,IAAI;EACf,MAAM,IAAI,eAAA;EACV,WAAW,IAAI,eACX,SAAS,YAAY,OAAO,IAAI,WAAW,IAAI,YAAY,IAC3D;CACN;AACF;AAEA,SAAgB,aACd,OACA,KACA,UACe;CACf,OAAO;EAAE,GAAG,aAAa,OAAO,KAAK,QAAQ;EAAG,QAAQ,IAAI;EAAQ,WAAW,IAAI;CAAU;AAC/F"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Event } from "../../types/schemas/content.js";
|
|
2
|
+
import "../../content/index.js";
|
|
3
|
+
//#region src/reporting/member/calendar.d.ts
|
|
4
|
+
declare function eventCalendar(event: Event, options: {
|
|
5
|
+
pageUrl: string;
|
|
6
|
+
withLocation: boolean;
|
|
7
|
+
now: Date;
|
|
8
|
+
}): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { eventCalendar };
|
|
11
|
+
//# sourceMappingURL=calendar.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/reporting/member/calendar.ts
|
|
2
|
+
function stamp(date) {
|
|
3
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
4
|
+
}
|
|
5
|
+
function text(value) {
|
|
6
|
+
return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
|
|
7
|
+
}
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
function octets(value) {
|
|
10
|
+
return encoder.encode(value).length;
|
|
11
|
+
}
|
|
12
|
+
function fold(line) {
|
|
13
|
+
const parts = [];
|
|
14
|
+
let part = "";
|
|
15
|
+
let size = 0;
|
|
16
|
+
let width = 75;
|
|
17
|
+
for (const char of line) {
|
|
18
|
+
const length = octets(char);
|
|
19
|
+
if (size + length > width) {
|
|
20
|
+
parts.push(part);
|
|
21
|
+
part = "";
|
|
22
|
+
size = 0;
|
|
23
|
+
width = 74;
|
|
24
|
+
}
|
|
25
|
+
part += char;
|
|
26
|
+
size += length;
|
|
27
|
+
}
|
|
28
|
+
parts.push(part);
|
|
29
|
+
return parts.join("\r\n ");
|
|
30
|
+
}
|
|
31
|
+
function where(event) {
|
|
32
|
+
const venue = event.location.venue;
|
|
33
|
+
if (venue) {
|
|
34
|
+
const address = venue.address;
|
|
35
|
+
const lines = address ? [
|
|
36
|
+
address.line1,
|
|
37
|
+
address.line2,
|
|
38
|
+
address.city,
|
|
39
|
+
address.region,
|
|
40
|
+
address.postalCode,
|
|
41
|
+
address.country
|
|
42
|
+
] : [];
|
|
43
|
+
return [venue.name, ...lines].filter((part) => Boolean(part)).join(", ");
|
|
44
|
+
}
|
|
45
|
+
return event.location.online?.url ?? null;
|
|
46
|
+
}
|
|
47
|
+
function eventCalendar(event, options) {
|
|
48
|
+
const lines = [
|
|
49
|
+
"BEGIN:VCALENDAR",
|
|
50
|
+
"VERSION:2.0",
|
|
51
|
+
"PRODID:-//Headless LMS//Events//EN",
|
|
52
|
+
"CALSCALE:GREGORIAN",
|
|
53
|
+
"METHOD:PUBLISH",
|
|
54
|
+
"BEGIN:VEVENT",
|
|
55
|
+
`UID:${event.id}`,
|
|
56
|
+
`DTSTAMP:${stamp(options.now)}`,
|
|
57
|
+
`DTSTART:${stamp(event.startsAt)}`,
|
|
58
|
+
`DTEND:${stamp(event.endsAt)}`,
|
|
59
|
+
`SUMMARY:${text(event.title)}`,
|
|
60
|
+
`DESCRIPTION:${text(options.pageUrl)}`,
|
|
61
|
+
`URL:${options.pageUrl}`
|
|
62
|
+
];
|
|
63
|
+
if (event.status === "canceled") lines.push("STATUS:CANCELLED");
|
|
64
|
+
if (options.withLocation) {
|
|
65
|
+
const location = where(event);
|
|
66
|
+
if (location) lines.push(`LOCATION:${text(location)}`);
|
|
67
|
+
if (event.location.online) lines.push(`CONFERENCE;VALUE=URI:${event.location.online.url}`);
|
|
68
|
+
}
|
|
69
|
+
lines.push("END:VEVENT", "END:VCALENDAR");
|
|
70
|
+
return `${lines.map(fold).join("\r\n")}\r\n`;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { eventCalendar };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=calendar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar.js","names":[],"sources":["../../../src/reporting/member/calendar.ts"],"sourcesContent":["import type { Event } from '../../content/index.js';\n\nfunction stamp(date: Date): string {\n return date\n .toISOString()\n .replace(/[-:]/g, '')\n .replace(/\\.\\d{3}/, '');\n}\n\nfunction text(value: string): string {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/;/g, '\\\\;')\n .replace(/,/g, '\\\\,')\n .replace(/\\r?\\n/g, '\\\\n');\n}\n\nconst encoder = new TextEncoder();\n\nfunction octets(value: string): number {\n return encoder.encode(value).length;\n}\n\nfunction fold(line: string): string {\n const parts: string[] = [];\n let part = '';\n let size = 0;\n let width = 75;\n for (const char of line) {\n const length = octets(char);\n if (size + length > width) {\n parts.push(part);\n part = '';\n size = 0;\n width = 74;\n }\n part += char;\n size += length;\n }\n parts.push(part);\n return parts.join('\\r\\n ');\n}\n\nfunction where(event: Event): string | null {\n const venue = event.location.venue;\n if (venue) {\n const address = venue.address;\n const lines = address\n ? [\n address.line1,\n address.line2,\n address.city,\n address.region,\n address.postalCode,\n address.country,\n ]\n : [];\n return [venue.name, ...lines].filter((part): part is string => Boolean(part)).join(', ');\n }\n return event.location.online?.url ?? null;\n}\n\nexport function eventCalendar(\n event: Event,\n options: { pageUrl: string; withLocation: boolean; now: Date },\n): string {\n const lines = [\n 'BEGIN:VCALENDAR',\n 'VERSION:2.0',\n 'PRODID:-//Headless LMS//Events//EN',\n 'CALSCALE:GREGORIAN',\n 'METHOD:PUBLISH',\n 'BEGIN:VEVENT',\n `UID:${event.id}`,\n `DTSTAMP:${stamp(options.now)}`,\n `DTSTART:${stamp(event.startsAt)}`,\n `DTEND:${stamp(event.endsAt)}`,\n `SUMMARY:${text(event.title)}`,\n `DESCRIPTION:${text(options.pageUrl)}`,\n `URL:${options.pageUrl}`,\n ];\n if (event.status === 'canceled') {\n lines.push('STATUS:CANCELLED');\n }\n if (options.withLocation) {\n const location = where(event);\n if (location) {\n lines.push(`LOCATION:${text(location)}`);\n }\n if (event.location.online) {\n lines.push(`CONFERENCE;VALUE=URI:${event.location.online.url}`);\n }\n }\n lines.push('END:VEVENT', 'END:VCALENDAR');\n return `${lines.map(fold).join('\\r\\n')}\\r\\n`;\n}\n"],"mappings":";AAEA,SAAS,MAAM,MAAoB;CACjC,OAAO,KACJ,YAAY,CAAC,CACb,QAAQ,SAAS,EAAE,CAAC,CACpB,QAAQ,WAAW,EAAE;AAC1B;AAEA,SAAS,KAAK,OAAuB;CACnC,OAAO,MACJ,QAAQ,OAAO,MAAM,CAAC,CACtB,QAAQ,MAAM,KAAK,CAAC,CACpB,QAAQ,MAAM,KAAK,CAAC,CACpB,QAAQ,UAAU,KAAK;AAC5B;AAEA,MAAM,UAAU,IAAI,YAAY;AAEhC,SAAS,OAAO,OAAuB;CACrC,OAAO,QAAQ,OAAO,KAAK,CAAC,CAAC;AAC/B;AAEA,SAAS,KAAK,MAAsB;CAClC,MAAM,QAAkB,CAAC;CACzB,IAAI,OAAO;CACX,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,MAAM;EACvB,MAAM,SAAS,OAAO,IAAI;EAC1B,IAAI,OAAO,SAAS,OAAO;GACzB,MAAM,KAAK,IAAI;GACf,OAAO;GACP,OAAO;GACP,QAAQ;EACV;EACA,QAAQ;EACR,QAAQ;CACV;CACA,MAAM,KAAK,IAAI;CACf,OAAO,MAAM,KAAK,OAAO;AAC3B;AAEA,SAAS,MAAM,OAA6B;CAC1C,MAAM,QAAQ,MAAM,SAAS;CAC7B,IAAI,OAAO;EACT,MAAM,UAAU,MAAM;EACtB,MAAM,QAAQ,UACV;GACE,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,QAAQ;EACV,IACA,CAAC;EACL,OAAO,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,SAAyB,QAAQ,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;CACzF;CACA,OAAO,MAAM,SAAS,QAAQ,OAAO;AACvC;AAEA,SAAgB,cACd,OACA,SACQ;CACR,MAAM,QAAQ;EACZ;EACA;EACA;EACA;EACA;EACA;EACA,OAAO,MAAM;EACb,WAAW,MAAM,QAAQ,GAAG;EAC5B,WAAW,MAAM,MAAM,QAAQ;EAC/B,SAAS,MAAM,MAAM,MAAM;EAC3B,WAAW,KAAK,MAAM,KAAK;EAC3B,eAAe,KAAK,QAAQ,OAAO;EACnC,OAAO,QAAQ;CACjB;CACA,IAAI,MAAM,WAAW,YACnB,MAAM,KAAK,kBAAkB;CAE/B,IAAI,QAAQ,cAAc;EACxB,MAAM,WAAW,MAAM,KAAK;EAC5B,IAAI,UACF,MAAM,KAAK,YAAY,KAAK,QAAQ,GAAG;EAEzC,IAAI,MAAM,SAAS,QACjB,MAAM,KAAK,wBAAwB,MAAM,SAAS,OAAO,KAAK;CAElE;CACA,MAAM,KAAK,cAAc,eAAe;CACxC,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,CAAC,KAAK,MAAM,EAAE;AACzC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CourseView, Module } from "../../types/schemas/content.js";
|
|
2
|
-
import {
|
|
2
|
+
import { EventAttendeeRow, EventReader } from "../events/index.js";
|
|
3
|
+
import { CommunityHome, CommunityHomeSpace, CommunityNotificationSettingsView, CommunityViewer, CourseProgressView, DirectoryEntry, DirectoryQuery, DirectoryRow, LockedCommunitySpace, MemberViewer, NotificationItemView, NotificationSettingsCategoryView, NotificationUnreadCounts, NotificationView, NotificationsView, OpenCommunitySpace, OrgNotificationSettingsView, SettingsChannelView } from "./model.js";
|
|
3
4
|
import { MemberReportRepository, MemberReportService } from "./ports.js";
|
|
4
5
|
import { MemberReportServiceImpl } from "./service.js";
|
|
5
|
-
export { type CommunityHome, type CommunityHomeSpace, type CourseProgressView, type CourseView, type DirectoryEntry, type DirectoryQuery, type DirectoryRow, type LockedCommunitySpace, type MemberReportRepository, type MemberReportService, MemberReportServiceImpl, type Module, type NotificationView, type NotificationsView, type OpenCommunitySpace };
|
|
6
|
+
export { type CommunityHome, type CommunityHomeSpace, type CommunityNotificationSettingsView, type CommunityViewer, type CourseProgressView, type CourseView, type DirectoryEntry, type DirectoryQuery, type DirectoryRow, type EventAttendeeRow, type EventReader, type LockedCommunitySpace, type MemberReportRepository, type MemberReportService, MemberReportServiceImpl, type MemberViewer, type Module, type NotificationItemView, type NotificationSettingsCategoryView, type NotificationUnreadCounts, type NotificationView, type NotificationsView, type OpenCommunitySpace, type OrgNotificationSettingsView, type SettingsChannelView };
|