@things-factory/learning 0.0.0-stage → 10.1.260

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.
Files changed (149) hide show
  1. package/dist-client/bootstrap.d.ts +2 -0
  2. package/dist-client/bootstrap.js +4 -0
  3. package/dist-client/bootstrap.js.map +1 -0
  4. package/dist-client/components/learning-assign-form.d.ts +24 -0
  5. package/dist-client/components/learning-assign-form.js +77 -0
  6. package/dist-client/components/learning-assign-form.js.map +1 -0
  7. package/dist-client/components/learning-course-form.d.ts +29 -0
  8. package/dist-client/components/learning-course-form.js +104 -0
  9. package/dist-client/components/learning-course-form.js.map +1 -0
  10. package/dist-client/components/learning-question-form.d.ts +33 -0
  11. package/dist-client/components/learning-question-form.js +142 -0
  12. package/dist-client/components/learning-question-form.js.map +1 -0
  13. package/dist-client/components/learning-todo.d.ts +12 -0
  14. package/dist-client/components/learning-todo.js +43 -0
  15. package/dist-client/components/learning-todo.js.map +1 -0
  16. package/dist-client/components/learning-unit-form.d.ts +41 -0
  17. package/dist-client/components/learning-unit-form.js +143 -0
  18. package/dist-client/components/learning-unit-form.js.map +1 -0
  19. package/dist-client/index.d.ts +2 -0
  20. package/dist-client/index.js +3 -0
  21. package/dist-client/index.js.map +1 -0
  22. package/dist-client/pages/learning-course-editor.d.ts +62 -0
  23. package/dist-client/pages/learning-course-editor.js +463 -0
  24. package/dist-client/pages/learning-course-editor.js.map +1 -0
  25. package/dist-client/pages/learning-course-list.d.ts +37 -0
  26. package/dist-client/pages/learning-course-list.js +88 -0
  27. package/dist-client/pages/learning-course-list.js.map +1 -0
  28. package/dist-client/pages/learning-enrollment-status.d.ts +53 -0
  29. package/dist-client/pages/learning-enrollment-status.js +228 -0
  30. package/dist-client/pages/learning-enrollment-status.js.map +1 -0
  31. package/dist-client/pages/learning-view.d.ts +52 -0
  32. package/dist-client/pages/learning-view.js +371 -0
  33. package/dist-client/pages/learning-view.js.map +1 -0
  34. package/dist-client/pages/my-learning.d.ts +22 -0
  35. package/dist-client/pages/my-learning.js +110 -0
  36. package/dist-client/pages/my-learning.js.map +1 -0
  37. package/dist-client/route.d.ts +1 -0
  38. package/dist-client/route.js +21 -0
  39. package/dist-client/route.js.map +1 -0
  40. package/dist-client/tsconfig.tsbuildinfo +1 -0
  41. package/dist-client/utils/learning-api.d.ts +16 -0
  42. package/dist-client/utils/learning-api.js +36 -0
  43. package/dist-client/utils/learning-api.js.map +1 -0
  44. package/dist-client/utils/learning-words.d.ts +20 -0
  45. package/dist-client/utils/learning-words.js +22 -0
  46. package/dist-client/utils/learning-words.js.map +1 -0
  47. package/dist-server/activities.d.ts +38 -0
  48. package/dist-server/activities.js +42 -0
  49. package/dist-server/activities.js.map +1 -0
  50. package/dist-server/ai-reads.d.ts +9 -0
  51. package/dist-server/ai-reads.js +70 -0
  52. package/dist-server/ai-reads.js.map +1 -0
  53. package/dist-server/attachment-reader.d.ts +1 -0
  54. package/dist-server/attachment-reader.js +23 -0
  55. package/dist-server/attachment-reader.js.map +1 -0
  56. package/dist-server/index.d.ts +4 -0
  57. package/dist-server/index.js +33 -0
  58. package/dist-server/index.js.map +1 -0
  59. package/dist-server/learning-subjects.d.ts +10 -0
  60. package/dist-server/learning-subjects.js +31 -0
  61. package/dist-server/learning-subjects.js.map +1 -0
  62. package/dist-server/role-tasks.d.ts +2 -0
  63. package/dist-server/role-tasks.js +17 -0
  64. package/dist-server/role-tasks.js.map +1 -0
  65. package/dist-server/service/course/course-mutation.d.ts +19 -0
  66. package/dist-server/service/course/course-mutation.js +176 -0
  67. package/dist-server/service/course/course-mutation.js.map +1 -0
  68. package/dist-server/service/course/course-query.d.ts +26 -0
  69. package/dist-server/service/course/course-query.js +172 -0
  70. package/dist-server/service/course/course-query.js.map +1 -0
  71. package/dist-server/service/course/course-question.d.ts +41 -0
  72. package/dist-server/service/course/course-question.js +116 -0
  73. package/dist-server/service/course/course-question.js.map +1 -0
  74. package/dist-server/service/course/course-rules.d.ts +49 -0
  75. package/dist-server/service/course/course-rules.js +89 -0
  76. package/dist-server/service/course/course-rules.js.map +1 -0
  77. package/dist-server/service/course/course-service.d.ts +71 -0
  78. package/dist-server/service/course/course-service.js +402 -0
  79. package/dist-server/service/course/course-service.js.map +1 -0
  80. package/dist-server/service/course/course-type.d.ts +55 -0
  81. package/dist-server/service/course/course-type.js +198 -0
  82. package/dist-server/service/course/course-type.js.map +1 -0
  83. package/dist-server/service/course/course-unit.d.ts +36 -0
  84. package/dist-server/service/course/course-unit.js +123 -0
  85. package/dist-server/service/course/course-unit.js.map +1 -0
  86. package/dist-server/service/course/course-version.d.ts +45 -0
  87. package/dist-server/service/course/course-version.js +151 -0
  88. package/dist-server/service/course/course-version.js.map +1 -0
  89. package/dist-server/service/course/course.d.ts +23 -0
  90. package/dist-server/service/course/course.js +92 -0
  91. package/dist-server/service/course/course.js.map +1 -0
  92. package/dist-server/service/course/index.d.ts +14 -0
  93. package/dist-server/service/course/index.js +24 -0
  94. package/dist-server/service/course/index.js.map +1 -0
  95. package/dist-server/service/enrollment/enrollment-mutation.d.ts +11 -0
  96. package/dist-server/service/enrollment/enrollment-mutation.js +124 -0
  97. package/dist-server/service/enrollment/enrollment-mutation.js.map +1 -0
  98. package/dist-server/service/enrollment/enrollment-query.d.ts +23 -0
  99. package/dist-server/service/enrollment/enrollment-query.js +245 -0
  100. package/dist-server/service/enrollment/enrollment-query.js.map +1 -0
  101. package/dist-server/service/enrollment/enrollment-remind.d.ts +1 -0
  102. package/dist-server/service/enrollment/enrollment-remind.js +45 -0
  103. package/dist-server/service/enrollment/enrollment-remind.js.map +1 -0
  104. package/dist-server/service/enrollment/enrollment-service.d.ts +110 -0
  105. package/dist-server/service/enrollment/enrollment-service.js +417 -0
  106. package/dist-server/service/enrollment/enrollment-service.js.map +1 -0
  107. package/dist-server/service/enrollment/enrollment-status.d.ts +40 -0
  108. package/dist-server/service/enrollment/enrollment-status.js +135 -0
  109. package/dist-server/service/enrollment/enrollment-status.js.map +1 -0
  110. package/dist-server/service/enrollment/enrollment-type.d.ts +95 -0
  111. package/dist-server/service/enrollment/enrollment-type.js +341 -0
  112. package/dist-server/service/enrollment/enrollment-type.js.map +1 -0
  113. package/dist-server/service/enrollment/enrollment.d.ts +52 -0
  114. package/dist-server/service/enrollment/enrollment.js +186 -0
  115. package/dist-server/service/enrollment/enrollment.js.map +1 -0
  116. package/dist-server/service/enrollment/index.d.ts +15 -0
  117. package/dist-server/service/enrollment/index.js +26 -0
  118. package/dist-server/service/enrollment/index.js.map +1 -0
  119. package/dist-server/service/enrollment/learning-ai.d.ts +38 -0
  120. package/dist-server/service/enrollment/learning-ai.js +96 -0
  121. package/dist-server/service/enrollment/learning-ai.js.map +1 -0
  122. package/dist-server/service/enrollment/learning-documents.d.ts +7 -0
  123. package/dist-server/service/enrollment/learning-documents.js +160 -0
  124. package/dist-server/service/enrollment/learning-documents.js.map +1 -0
  125. package/dist-server/service/enrollment/learning-outcome.d.ts +10 -0
  126. package/dist-server/service/enrollment/learning-outcome.js +16 -0
  127. package/dist-server/service/enrollment/learning-outcome.js.map +1 -0
  128. package/dist-server/service/enrollment/progress-rules.d.ts +94 -0
  129. package/dist-server/service/enrollment/progress-rules.js +97 -0
  130. package/dist-server/service/enrollment/progress-rules.js.map +1 -0
  131. package/dist-server/service/enrollment/quiz-attempt.d.ts +34 -0
  132. package/dist-server/service/enrollment/quiz-attempt.js +105 -0
  133. package/dist-server/service/enrollment/quiz-attempt.js.map +1 -0
  134. package/dist-server/service/enrollment/unit-progress.d.ts +30 -0
  135. package/dist-server/service/enrollment/unit-progress.js +114 -0
  136. package/dist-server/service/enrollment/unit-progress.js.map +1 -0
  137. package/dist-server/service/index.d.ts +14 -0
  138. package/dist-server/service/index.js +43 -0
  139. package/dist-server/service/index.js.map +1 -0
  140. package/dist-server/tsconfig.tsbuildinfo +1 -0
  141. package/package.json +56 -4
  142. package/things-factory.config.js +14 -0
  143. package/translations/en.json +255 -0
  144. package/translations/es.json +255 -0
  145. package/translations/ja.json +255 -0
  146. package/translations/ko.json +255 -0
  147. package/translations/ms.json +255 -0
  148. package/translations/zh.json +255 -0
  149. package/README.md +0 -3
@@ -0,0 +1,15 @@
1
+ import { Enrollment } from './enrollment';
2
+ import { QuizAttempt } from './quiz-attempt';
3
+ import { UnitProgress } from './unit-progress';
4
+ import { EnrollmentMutation } from './enrollment-mutation';
5
+ import { EnrollmentQuery } from './enrollment-query';
6
+ export * from './enrollment';
7
+ export * from './quiz-attempt';
8
+ export * from './unit-progress';
9
+ export * from './learning-outcome';
10
+ export { cancelEnrollment, DIRECT_SOURCE_KIND, enroll, learningAddressOf, withdraw } from './enrollment-service';
11
+ export type { LearningSource } from './enrollment-service';
12
+ export { LEARNING_CERTIFICATE_KIND, LEARNING_REGISTER_KIND } from './learning-documents';
13
+ export declare const entities: (typeof Enrollment | typeof QuizAttempt | typeof UnitProgress)[];
14
+ export declare const resolvers: (typeof EnrollmentMutation | typeof EnrollmentQuery)[];
15
+ export declare const subscribers: any[];
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subscribers = exports.resolvers = exports.entities = exports.LEARNING_REGISTER_KIND = exports.LEARNING_CERTIFICATE_KIND = exports.withdraw = exports.learningAddressOf = exports.enroll = exports.DIRECT_SOURCE_KIND = exports.cancelEnrollment = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const enrollment_1 = require("./enrollment");
6
+ const quiz_attempt_1 = require("./quiz-attempt");
7
+ const unit_progress_1 = require("./unit-progress");
8
+ const enrollment_mutation_1 = require("./enrollment-mutation");
9
+ const enrollment_query_1 = require("./enrollment-query");
10
+ tslib_1.__exportStar(require("./enrollment"), exports);
11
+ tslib_1.__exportStar(require("./quiz-attempt"), exports);
12
+ tslib_1.__exportStar(require("./unit-progress"), exports);
13
+ tslib_1.__exportStar(require("./learning-outcome"), exports);
14
+ var enrollment_service_1 = require("./enrollment-service");
15
+ Object.defineProperty(exports, "cancelEnrollment", { enumerable: true, get: function () { return enrollment_service_1.cancelEnrollment; } });
16
+ Object.defineProperty(exports, "DIRECT_SOURCE_KIND", { enumerable: true, get: function () { return enrollment_service_1.DIRECT_SOURCE_KIND; } });
17
+ Object.defineProperty(exports, "enroll", { enumerable: true, get: function () { return enrollment_service_1.enroll; } });
18
+ Object.defineProperty(exports, "learningAddressOf", { enumerable: true, get: function () { return enrollment_service_1.learningAddressOf; } });
19
+ Object.defineProperty(exports, "withdraw", { enumerable: true, get: function () { return enrollment_service_1.withdraw; } });
20
+ var learning_documents_1 = require("./learning-documents");
21
+ Object.defineProperty(exports, "LEARNING_CERTIFICATE_KIND", { enumerable: true, get: function () { return learning_documents_1.LEARNING_CERTIFICATE_KIND; } });
22
+ Object.defineProperty(exports, "LEARNING_REGISTER_KIND", { enumerable: true, get: function () { return learning_documents_1.LEARNING_REGISTER_KIND; } });
23
+ exports.entities = [enrollment_1.Enrollment, unit_progress_1.UnitProgress, quiz_attempt_1.QuizAttempt];
24
+ exports.resolvers = [enrollment_query_1.EnrollmentQuery, enrollment_mutation_1.EnrollmentMutation];
25
+ exports.subscribers = [];
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/enrollment/index.ts"],"names":[],"mappings":";;;;AAAA,6CAAyC;AACzC,iDAA4C;AAC5C,mDAA8C;AAC9C,+DAA0D;AAC1D,yDAAoD;AAEpD,uDAA4B;AAC5B,yDAA8B;AAC9B,0DAA+B;AAC/B,6DAAkC;AAClC,2DAAgH;AAAvG,sHAAA,gBAAgB,OAAA;AAAE,wHAAA,kBAAkB,OAAA;AAAE,4GAAA,MAAM,OAAA;AAAE,uHAAA,iBAAiB,OAAA;AAAE,8GAAA,QAAQ,OAAA;AAElF,2DAAwF;AAA/E,+HAAA,yBAAyB,OAAA;AAAE,4HAAA,sBAAsB,OAAA;AAE7C,QAAA,QAAQ,GAAG,CAAC,uBAAU,EAAE,4BAAY,EAAE,0BAAW,CAAC,CAAA;AAClD,QAAA,SAAS,GAAG,CAAC,kCAAe,EAAE,wCAAkB,CAAC,CAAA;AACjD,QAAA,WAAW,GAAG,EAAE,CAAA","sourcesContent":["import { Enrollment } from './enrollment'\nimport { QuizAttempt } from './quiz-attempt'\nimport { UnitProgress } from './unit-progress'\nimport { EnrollmentMutation } from './enrollment-mutation'\nimport { EnrollmentQuery } from './enrollment-query'\n\nexport * from './enrollment'\nexport * from './quiz-attempt'\nexport * from './unit-progress'\nexport * from './learning-outcome'\nexport { cancelEnrollment, DIRECT_SOURCE_KIND, enroll, learningAddressOf, withdraw } from './enrollment-service'\nexport type { LearningSource } from './enrollment-service'\nexport { LEARNING_CERTIFICATE_KIND, LEARNING_REGISTER_KIND } from './learning-documents'\n\nexport const entities = [Enrollment, UnitProgress, QuizAttempt]\nexport const resolvers = [EnrollmentQuery, EnrollmentMutation]\nexport const subscribers = []\n"]}
@@ -0,0 +1,38 @@
1
+ export interface ReadableEnrollment {
2
+ id: string;
3
+ learner: string;
4
+ departmentName: string | null;
5
+ course: string;
6
+ state: string;
7
+ dueOn: string | null;
8
+ daysLeft: number | null;
9
+ startedAt: Date | null;
10
+ completedAt: Date | null;
11
+ unitsDone: number;
12
+ units: number;
13
+ }
14
+ /** The enrolments the person may read, as rows the assistant lists - cancelled ones left out. */
15
+ export declare function readableEnrollments(context: any): Promise<ReadableEnrollment[]>;
16
+ export interface CourseContent {
17
+ course: string;
18
+ version: number;
19
+ units: {
20
+ seq: number;
21
+ kind: string;
22
+ name: string;
23
+ text: string | null;
24
+ }[];
25
+ }
26
+ /**
27
+ * The units of one version as the assistant reads them. With an enrolment - the person's own - its version and nothing
28
+ * else, refused while a quiz attempt of it is open: the assistant does not answer the quiz. With a version - a manager's.
29
+ */
30
+ export declare function courseContent(args: {
31
+ enrollmentId?: string | null;
32
+ versionId?: string | null;
33
+ }, context: any): Promise<CourseContent>;
34
+ /** "a\n*b\nc" - one choice a line, the right ones starting with `*` - as the editor's choices. */
35
+ export declare function choicesOfText(text: string): {
36
+ text: string;
37
+ correct: boolean;
38
+ }[];
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readableEnrollments = readableEnrollments;
4
+ exports.courseContent = courseContent;
5
+ exports.choicesOfText = choicesOfText;
6
+ /*
7
+ * What the assistant reads and enters of learning (ADR-0194 decision 12), as the person asking:
8
+ *
9
+ * - `learning.enrollments` - the enrolments the person may read: a manager every one, a department head their people's
10
+ * (status only), anyone else their own - 「기한이 일주일 남았는데 시작하지 않은 사람」.
11
+ * - `learning.course-content` - the units of one version, to answer a question on a course or to draft its questions:
12
+ * a learner the version of their own enrolment and nothing else, never while a quiz attempt of theirs is open; a manager
13
+ * any version.
14
+ * - `learning.question-draft` - questions drafted from the units, entered into a draft version only, each marked drafted
15
+ * until a manager looks it over and saves it; a version with one is not published.
16
+ */
17
+ const typeorm_1 = require("typeorm");
18
+ const shell_1 = require("@things-factory/shell");
19
+ const course_1 = require("../course/course");
20
+ const course_version_1 = require("../course/course-version");
21
+ const course_service_1 = require("../course/course-service");
22
+ const enrollment_1 = require("./enrollment");
23
+ const enrollment_service_1 = require("./enrollment-service");
24
+ const enrollment_status_1 = require("./enrollment-status");
25
+ const quiz_attempt_1 = require("./quiz-attempt");
26
+ const daysBetween = (from, to) => Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000);
27
+ /** The enrolments the person may read, as rows the assistant lists - cancelled ones left out. */
28
+ async function readableEnrollments(context) {
29
+ const { domain, user } = context.state;
30
+ let rows;
31
+ if (await (0, course_service_1.holds)(context, 'course', 'query'))
32
+ rows = await (0, enrollment_status_1.enrollmentStatus)({}, context);
33
+ else if ((await (0, enrollment_status_1.headedDepartmentsOf)(context)).length)
34
+ rows = await (0, enrollment_status_1.teamEnrollmentStatus)({}, context);
35
+ else {
36
+ const mine = await (0, course_service_1.learningManagerOf)(context).getRepository(enrollment_1.Enrollment).find({ where: { domain: { id: domain.id }, user: { id: user.id } } });
37
+ rows = await (0, enrollment_status_1.rowsOf)((0, course_service_1.learningManagerOf)(context), domain, mine);
38
+ }
39
+ const today = (0, enrollment_service_1.todayIn)(domain);
40
+ return rows
41
+ .filter(row => row.state !== enrollment_1.EnrollmentStatus.CANCELLED)
42
+ .map(row => ({
43
+ id: row.enrollment.id,
44
+ learner: row.user?.name ?? '',
45
+ departmentName: row.departmentName,
46
+ course: row.course?.name ?? '',
47
+ state: row.state,
48
+ dueOn: row.enrollment.dueOn ?? null,
49
+ daysLeft: row.enrollment.dueOn ? daysBetween(today, row.enrollment.dueOn) : null,
50
+ startedAt: row.enrollment.startedAt ?? null,
51
+ completedAt: row.enrollment.completedAt ?? null,
52
+ unitsDone: row.unitsDone,
53
+ units: row.units
54
+ }));
55
+ }
56
+ /**
57
+ * The units of one version as the assistant reads them. With an enrolment - the person's own - its version and nothing
58
+ * else, refused while a quiz attempt of it is open: the assistant does not answer the quiz. With a version - a manager's.
59
+ */
60
+ async function courseContent(args, context) {
61
+ const { domain } = context.state;
62
+ const tx = (0, course_service_1.learningManagerOf)(context);
63
+ let version;
64
+ if (args.enrollmentId) {
65
+ const enrollment = await (0, enrollment_service_1.myEnrollmentOf)(context, args.enrollmentId, tx);
66
+ const open = await tx.getRepository(quiz_attempt_1.QuizAttempt).count({ where: { enrollment: { id: enrollment.id }, submittedAt: (0, typeorm_1.IsNull)() } });
67
+ if (open && enrollment_1.OPEN_ENROLLMENT_STATUSES.includes(enrollment.status))
68
+ throw new shell_1.Refusal('learning-quiz-in-progress', {}, 'a quiz attempt is open - the course is not explained until it is sent');
69
+ version = await (0, enrollment_service_1.shownVersionOf)(tx, domain, enrollment);
70
+ }
71
+ else if (args.versionId) {
72
+ if (!(await (0, course_service_1.holds)(context, 'course', 'query')))
73
+ throw new shell_1.Refusal('learning-not-found', {}, 'the record was not found. reload and try again.');
74
+ version = await tx.getRepository(course_version_1.CourseVersion).findOne({ where: { id: args.versionId, domain: { id: domain.id } }, withDeleted: true });
75
+ }
76
+ else
77
+ throw new shell_1.Refusal('learning-content-what', {}, 'name the enrolment or the version');
78
+ if (!version)
79
+ throw new shell_1.Refusal('learning-not-found', {}, 'the record was not found. reload and try again.');
80
+ const course = await tx.getRepository(course_1.Course).findOneBy({ id: version.courseId });
81
+ const units = await (0, course_service_1.unitsOf)(tx, version.id);
82
+ return {
83
+ course: course?.name ?? '',
84
+ version: version.number,
85
+ units: units.map(unit => ({ seq: unit.seq, kind: unit.kind, name: unit.name, text: unit.kind === 'MARKDOWN' ? unit.body ?? null : unit.kind === 'LINK' ? unit.url ?? null : null }))
86
+ };
87
+ }
88
+ /** "a\n*b\nc" - one choice a line, the right ones starting with `*` - as the editor's choices. */
89
+ function choicesOfText(text) {
90
+ return String(text ?? '')
91
+ .split(/\r?\n/)
92
+ .map(line => line.trim())
93
+ .filter(Boolean)
94
+ .map(line => ({ text: line.replace(/^\*\s*/, ''), correct: line.startsWith('*') }));
95
+ }
96
+ //# sourceMappingURL=learning-ai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learning-ai.js","sourceRoot":"","sources":["../../../server/service/enrollment/learning-ai.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the assistant reads and enters of learning (ADR-0194 decision 12), as the person asking:\n *\n * - `learning.enrollments` - the enrolments the person may read: a manager every one, a department head their people's\n * (status only), anyone else their own - 「기한이 일주일 남았는데 시작하지 않은 사람」.\n * - `learning.course-content` - the units of one version, to answer a question on a course or to draft its questions:\n * a learner the version of their own enrolment and nothing else, never while a quiz attempt of theirs is open; a manager\n * any version.\n * - `learning.question-draft` - questions drafted from the units, entered into a draft version only, each marked drafted\n * until a manager looks it over and saves it; a version with one is not published.\n */\nimport { EntityManager, IsNull } from 'typeorm'\n\nimport { Refusal } from '@things-factory/shell'\n\nimport { Course } from '../course/course'\nimport { CourseUnit } from '../course/course-unit'\nimport { CourseVersion } from '../course/course-version'\nimport { holds, learningManagerOf, unitsOf } from '../course/course-service'\nimport { Enrollment, EnrollmentStatus, OPEN_ENROLLMENT_STATUSES } from './enrollment'\nimport { myEnrollmentOf, shownVersionOf, todayIn } from './enrollment-service'\nimport { enrollmentStatus, headedDepartmentsOf, rowsOf, StatusRow, teamEnrollmentStatus } from './enrollment-status'\nimport { QuizAttempt } from './quiz-attempt'\n\nexport interface ReadableEnrollment {\n id: string\n learner: string\n departmentName: string | null\n course: string\n state: string\n dueOn: string | null\n daysLeft: number | null\n startedAt: Date | null\n completedAt: Date | null\n unitsDone: number\n units: number\n}\n\nconst daysBetween = (from: string, to: string) => Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000)\n\n/** The enrolments the person may read, as rows the assistant lists - cancelled ones left out. */\nexport async function readableEnrollments(context: any): Promise<ReadableEnrollment[]> {\n const { domain, user } = context.state\n let rows: StatusRow[]\n if (await holds(context, 'course', 'query')) rows = await enrollmentStatus({}, context)\n else if ((await headedDepartmentsOf(context)).length) rows = await teamEnrollmentStatus({}, context)\n else {\n const mine = await learningManagerOf(context).getRepository(Enrollment).find({ where: { domain: { id: domain.id }, user: { id: user.id } } as any })\n rows = await rowsOf(learningManagerOf(context), domain, mine)\n }\n const today = todayIn(domain)\n return rows\n .filter(row => row.state !== EnrollmentStatus.CANCELLED)\n .map(row => ({\n id: row.enrollment.id,\n learner: row.user?.name ?? '',\n departmentName: row.departmentName,\n course: row.course?.name ?? '',\n state: row.state,\n dueOn: row.enrollment.dueOn ?? null,\n daysLeft: row.enrollment.dueOn ? daysBetween(today, row.enrollment.dueOn) : null,\n startedAt: row.enrollment.startedAt ?? null,\n completedAt: row.enrollment.completedAt ?? null,\n unitsDone: row.unitsDone,\n units: row.units\n }))\n}\n\nexport interface CourseContent {\n course: string\n version: number\n units: { seq: number; kind: string; name: string; text: string | null }[]\n}\n\n/**\n * The units of one version as the assistant reads them. With an enrolment - the person's own - its version and nothing\n * else, refused while a quiz attempt of it is open: the assistant does not answer the quiz. With a version - a manager's.\n */\nexport async function courseContent(args: { enrollmentId?: string | null; versionId?: string | null }, context: any): Promise<CourseContent> {\n const { domain } = context.state\n const tx: EntityManager = learningManagerOf(context)\n let version: CourseVersion | null\n if (args.enrollmentId) {\n const enrollment = await myEnrollmentOf(context, args.enrollmentId, tx)\n const open = await tx.getRepository(QuizAttempt).count({ where: { enrollment: { id: enrollment.id }, submittedAt: IsNull() } as any })\n if (open && (OPEN_ENROLLMENT_STATUSES as string[]).includes(enrollment.status))\n throw new Refusal('learning-quiz-in-progress', {}, 'a quiz attempt is open - the course is not explained until it is sent')\n version = await shownVersionOf(tx, domain, enrollment)\n } else if (args.versionId) {\n if (!(await holds(context, 'course', 'query'))) throw new Refusal('learning-not-found', {}, 'the record was not found. reload and try again.')\n version = await tx.getRepository(CourseVersion).findOne({ where: { id: args.versionId, domain: { id: domain.id } } as any, withDeleted: true })\n } else throw new Refusal('learning-content-what', {}, 'name the enrolment or the version')\n if (!version) throw new Refusal('learning-not-found', {}, 'the record was not found. reload and try again.')\n const course = await tx.getRepository(Course).findOneBy({ id: version.courseId! })\n const units: CourseUnit[] = await unitsOf(tx, version.id)\n return {\n course: course?.name ?? '',\n version: version.number,\n units: units.map(unit => ({ seq: unit.seq, kind: unit.kind, name: unit.name, text: unit.kind === 'MARKDOWN' ? unit.body ?? null : unit.kind === 'LINK' ? unit.url ?? null : null }))\n }\n}\n\n/** \"a\\n*b\\nc\" - one choice a line, the right ones starting with `*` - as the editor's choices. */\nexport function choicesOfText(text: string): { text: string; correct: boolean }[] {\n return String(text ?? '')\n .split(/\\r?\\n/)\n .map(line => line.trim())\n .filter(Boolean)\n .map(line => ({ text: line.replace(/^\\*\\s*/, ''), correct: line.startsWith('*') }))\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export declare const LEARNING_CERTIFICATE_KIND = "learning.certificate";
2
+ export declare const LEARNING_REGISTER_KIND = "learning.completion-register";
3
+ /** The certificate's template in code - a domain's own `PDFTemplate` of the kind prints in its place. */
4
+ export declare const CERTIFICATE_TEMPLATE = "<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n@page { size: A4; margin: 0 }\nbody { margin: 0; font-family: \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif; color: #222 }\n.page { position: relative; box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 30mm 24mm }\n.number { position: absolute; top: 16mm; left: 24mm; font-size: 9.5pt; color: #666 }\nh1 { margin: 6mm 0 16mm; text-align: center; font-size: 26pt; letter-spacing: 12pt }\ntable { width: 100%; border-collapse: collapse; font-size: 11.5pt }\nth, td { border: 1px solid #444; padding: 3.2mm 4mm; text-align: left; vertical-align: middle }\nth { background: #f2f2f2; font-weight: 600; text-align: center }\n.statement { margin: 22mm 0 0; text-align: center; font-size: 13pt; line-height: 2 }\n.date { margin-top: 18mm; text-align: center; font-size: 13pt; letter-spacing: 1pt }\n.issuer { position: relative; margin: 20mm auto 0; width: max-content; font-size: 14pt; line-height: 2; text-align: left; padding-right: 22mm }\n.issuer img { position: absolute; right: 0; top: 2mm; width: 22mm; opacity: .9 }\nth { width: 32mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">\uBC1C\uAE09\uBC88\uD638 {{issue.number}}</div>\n<h1>\uC218\uB8CC\uC99D</h1>\n<table>\n<tr><th>\uC131\uBA85</th><td>{{learner.name}}</td></tr>\n<tr><th>\uACFC\uC815</th><td>{{course.name}} (\uAC1C\uC815\uBCF8 {{course.version}})</td></tr>\n<tr><th>\uC774\uC218\uC77C</th><td>{{completion.completedOn}}</td></tr>\n<tr><th>\uD559\uC2B5 \uC2DC\uAC04</th><td>{{completion.minutes}}\uBD84</td></tr>\n{{#if completion.score}}<tr><th>\uD3C9\uAC00 \uC810\uC218</th><td>{{completion.score}}\uC810</td></tr>{{/if}}\n<tr><th>\uC218\uB8CC \uBC88\uD638</th><td>{{completion.certificateNo}}</td></tr>\n</table>\n<p class=\"statement\">\uC704 \uC0AC\uB78C\uC740 \uC704 \uACFC\uC815\uC744 \uC774\uC218\uD558\uC600\uC74C\uC744 \uC99D\uBA85\uD569\uB2C8\uB2E4.</p>\n<div class=\"date\">{{issue.issuedOn}}</div>\n<div class=\"issuer\">\n<div>\uC0C1\uD638 : {{company.name}}</div>\n{{#if company.address}}<div>\uC8FC\uC18C : {{company.address}}</div>{{/if}}\n<div>\uB300\uD45C\uC774\uC0AC : {{company.representative}}</div>\n{{#if company.seal}}<img src=\"{{company.seal}}\" alt=\"\">{{/if}}\n</div>\n</div></body></html>";
5
+ /** The completion register's template - one row per completion. */
6
+ export declare const REGISTER_TEMPLATE = "<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n@page { size: A4; margin: 0 }\nbody { margin: 0; font-family: \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif; color: #222 }\n.page { position: relative; box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 30mm 24mm }\n.number { position: absolute; top: 16mm; left: 24mm; font-size: 9.5pt; color: #666 }\nh1 { margin: 6mm 0 16mm; text-align: center; font-size: 26pt; letter-spacing: 12pt }\ntable { width: 100%; border-collapse: collapse; font-size: 11.5pt }\nth, td { border: 1px solid #444; padding: 3.2mm 4mm; text-align: left; vertical-align: middle }\nth { background: #f2f2f2; font-weight: 600; text-align: center }\n.statement { margin: 22mm 0 0; text-align: center; font-size: 13pt; line-height: 2 }\n.date { margin-top: 18mm; text-align: center; font-size: 13pt; letter-spacing: 1pt }\n.issuer { position: relative; margin: 20mm auto 0; width: max-content; font-size: 14pt; line-height: 2; text-align: left; padding-right: 22mm }\n.issuer img { position: absolute; right: 0; top: 2mm; width: 22mm; opacity: .9 }\n.page { padding: 24mm 16mm }\nh1 { font-size: 20pt; letter-spacing: 4pt; margin-bottom: 8mm }\n.meta { margin-bottom: 6mm; font-size: 10.5pt }\ntable { font-size: 9.5pt }\nth, td { padding: 2mm 2.4mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">\uBC1C\uAE09\uBC88\uD638 {{issue.number}}</div>\n<h1>\uC774\uC218 \uB300\uC7A5</h1>\n<div class=\"meta\">\uACFC\uC815 : {{course.name}} \u00B7 \uAE30\uC900\uC77C : {{issue.asOf}} \u00B7 \uC774\uC218\uC790 {{count}}\uBA85</div>\n<table>\n<tr><th>\uBC88\uD638</th><th>\uC131\uBA85</th><th>\uBD80\uC11C</th><th>\uAC1C\uC815\uBCF8</th><th>\uC774\uC218\uC77C</th><th>\uD559\uC2B5 \uC2DC\uAC04(\uBD84)</th><th>\uC810\uC218</th><th>\uC218\uB8CC \uBC88\uD638</th></tr>\n{{#each rows}}<tr><td>{{this.no}}</td><td>{{this.name}}</td><td>{{this.department}}</td><td>{{this.version}}</td><td>{{this.completedOn}}</td><td>{{this.minutes}}</td><td>{{this.score}}</td><td>{{this.certificateNo}}</td></tr>{{/each}}\n</table>\n<div class=\"date\">{{issue.issuedOn}}</div>\n<div class=\"issuer\">\n<div>\uC0C1\uD638 : {{company.name}}</div>\n{{#if company.address}}<div>\uC8FC\uC18C : {{company.address}}</div>{{/if}}\n<div>\uB300\uD45C\uC774\uC0AC : {{company.representative}}</div>\n{{#if company.seal}}<img src=\"{{company.seal}}\" alt=\"\">{{/if}}\n</div>\n</div></body></html>";
7
+ export declare function registerLearningDocuments(): void;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REGISTER_TEMPLATE = exports.CERTIFICATE_TEMPLATE = exports.LEARNING_REGISTER_KIND = exports.LEARNING_CERTIFICATE_KIND = void 0;
4
+ exports.registerLearningDocuments = registerLearningDocuments;
5
+ /*
6
+ * The documents this module issues through the pdf module (ADR-0194 decisions 9, 10 · ADR-0176): the certificate of a completed
7
+ * enrolment (`learning.certificate`) and the completion register of a course (`learning.completion-register`). The pdf module
8
+ * numbers and prints them with 「우리 회사」 - the company's name, representative and seal as the application keeps them.
9
+ */
10
+ const auth_base_1 = require("@things-factory/auth-base");
11
+ const pdf_1 = require("@things-factory/pdf");
12
+ const shell_1 = require("@things-factory/shell");
13
+ const course_1 = require("../course/course");
14
+ const course_version_1 = require("../course/course-version");
15
+ const course_service_1 = require("../course/course-service");
16
+ const enrollment_1 = require("./enrollment");
17
+ const enrollment_status_1 = require("./enrollment-status");
18
+ exports.LEARNING_CERTIFICATE_KIND = 'learning.certificate';
19
+ exports.LEARNING_REGISTER_KIND = 'learning.completion-register';
20
+ const STYLE = `@page { size: A4; margin: 0 }
21
+ body { margin: 0; font-family: "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif; color: #222 }
22
+ .page { position: relative; box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 30mm 24mm }
23
+ .number { position: absolute; top: 16mm; left: 24mm; font-size: 9.5pt; color: #666 }
24
+ h1 { margin: 6mm 0 16mm; text-align: center; font-size: 26pt; letter-spacing: 12pt }
25
+ table { width: 100%; border-collapse: collapse; font-size: 11.5pt }
26
+ th, td { border: 1px solid #444; padding: 3.2mm 4mm; text-align: left; vertical-align: middle }
27
+ th { background: #f2f2f2; font-weight: 600; text-align: center }
28
+ .statement { margin: 22mm 0 0; text-align: center; font-size: 13pt; line-height: 2 }
29
+ .date { margin-top: 18mm; text-align: center; font-size: 13pt; letter-spacing: 1pt }
30
+ .issuer { position: relative; margin: 20mm auto 0; width: max-content; font-size: 14pt; line-height: 2; text-align: left; padding-right: 22mm }
31
+ .issuer img { position: absolute; right: 0; top: 2mm; width: 22mm; opacity: .9 }`;
32
+ const ISSUER = `<div class="issuer">
33
+ <div>상호 : {{company.name}}</div>
34
+ {{#if company.address}}<div>주소 : {{company.address}}</div>{{/if}}
35
+ <div>대표이사 : {{company.representative}}</div>
36
+ {{#if company.seal}}<img src="{{company.seal}}" alt="">{{/if}}
37
+ </div>`;
38
+ /** The certificate's template in code - a domain's own `PDFTemplate` of the kind prints in its place. */
39
+ exports.CERTIFICATE_TEMPLATE = `<!doctype html><html lang="ko"><head><meta charset="utf-8"><style>
40
+ ${STYLE}
41
+ th { width: 32mm }
42
+ </style></head><body><div class="page">
43
+ <div class="number">발급번호 {{issue.number}}</div>
44
+ <h1>수료증</h1>
45
+ <table>
46
+ <tr><th>성명</th><td>{{learner.name}}</td></tr>
47
+ <tr><th>과정</th><td>{{course.name}} (개정본 {{course.version}})</td></tr>
48
+ <tr><th>이수일</th><td>{{completion.completedOn}}</td></tr>
49
+ <tr><th>학습 시간</th><td>{{completion.minutes}}분</td></tr>
50
+ {{#if completion.score}}<tr><th>평가 점수</th><td>{{completion.score}}점</td></tr>{{/if}}
51
+ <tr><th>수료 번호</th><td>{{completion.certificateNo}}</td></tr>
52
+ </table>
53
+ <p class="statement">위 사람은 위 과정을 이수하였음을 증명합니다.</p>
54
+ <div class="date">{{issue.issuedOn}}</div>
55
+ ${ISSUER}
56
+ </div></body></html>`;
57
+ /** The completion register's template - one row per completion. */
58
+ exports.REGISTER_TEMPLATE = `<!doctype html><html lang="ko"><head><meta charset="utf-8"><style>
59
+ ${STYLE}
60
+ .page { padding: 24mm 16mm }
61
+ h1 { font-size: 20pt; letter-spacing: 4pt; margin-bottom: 8mm }
62
+ .meta { margin-bottom: 6mm; font-size: 10.5pt }
63
+ table { font-size: 9.5pt }
64
+ th, td { padding: 2mm 2.4mm }
65
+ </style></head><body><div class="page">
66
+ <div class="number">발급번호 {{issue.number}}</div>
67
+ <h1>이수 대장</h1>
68
+ <div class="meta">과정 : {{course.name}} · 기준일 : {{issue.asOf}} · 이수자 {{count}}명</div>
69
+ <table>
70
+ <tr><th>번호</th><th>성명</th><th>부서</th><th>개정본</th><th>이수일</th><th>학습 시간(분)</th><th>점수</th><th>수료 번호</th></tr>
71
+ {{#each rows}}<tr><td>{{this.no}}</td><td>{{this.name}}</td><td>{{this.department}}</td><td>{{this.version}}</td><td>{{this.completedOn}}</td><td>{{this.minutes}}</td><td>{{this.score}}</td><td>{{this.certificateNo}}</td></tr>{{/each}}
72
+ </table>
73
+ <div class="date">{{issue.issuedOn}}</div>
74
+ ${ISSUER}
75
+ </div></body></html>`;
76
+ const dayOf = (at, timezone) => at ? new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at)) : '';
77
+ async function enrollmentFor(context, id) {
78
+ return id ? await (0, shell_1.getRepository)(enrollment_1.Enrollment, context.state.tx).findOne({ where: { id, domain: { id: context.state.domain.id } } }) : null;
79
+ }
80
+ /** A certificate is issued and read by the person it is of, once completed, and by a manager. */
81
+ async function mayHaveCertificate(context, id) {
82
+ const enrollment = await enrollmentFor(context, id ?? '');
83
+ if (!enrollment || enrollment.status !== enrollment_1.EnrollmentStatus.COMPLETED)
84
+ return false;
85
+ return enrollment.userId === context.state.user?.id || (await (0, course_service_1.holds)(context, 'course', 'query'));
86
+ }
87
+ function registerLearningDocuments() {
88
+ (0, pdf_1.registerIssuedDocumentKind)({
89
+ kind: exports.LEARNING_CERTIFICATE_KIND,
90
+ prefix: '수료',
91
+ refType: 'Enrollment',
92
+ template: { content_template: exports.CERTIFICATE_TEMPLATE, page_size: 'A4' },
93
+ async dataOf(context, refId) {
94
+ const enrollment = await enrollmentFor(context, refId);
95
+ if (!enrollment || enrollment.status !== enrollment_1.EnrollmentStatus.COMPLETED)
96
+ throw new shell_1.Refusal('learning-not-completed', {}, 'a certificate is issued for a completed enrolment');
97
+ const tx = context.state.tx;
98
+ const course = await (0, shell_1.getRepository)(course_1.Course, tx).findOneBy({ id: enrollment.courseId });
99
+ const version = await (0, shell_1.getRepository)(course_version_1.CourseVersion, tx).findOne({ where: { id: enrollment.versionId }, withDeleted: true });
100
+ const learner = await (0, shell_1.getRepository)(auth_base_1.User, tx).findOneBy({ id: enrollment.userId });
101
+ return {
102
+ subjectName: `${learner?.name ?? ''} · ${course?.name ?? ''}`,
103
+ data: {
104
+ learner: { name: learner?.name ?? '' },
105
+ course: { name: course?.name ?? '', version: version?.number ?? '' },
106
+ completion: {
107
+ completedOn: dayOf(enrollment.completedAt, context.state.domain.timezone),
108
+ minutes: Math.floor((enrollment.learnedSeconds ?? 0) / 60),
109
+ score: enrollment.score ?? null,
110
+ certificateNo: enrollment.certificateNo ?? ''
111
+ }
112
+ }
113
+ };
114
+ },
115
+ async mayIssue(context, refId) {
116
+ return await mayHaveCertificate(context, refId);
117
+ },
118
+ async mayRead(context, row) {
119
+ return await mayHaveCertificate(context, row.refId);
120
+ }
121
+ });
122
+ (0, pdf_1.registerIssuedDocumentKind)({
123
+ kind: exports.LEARNING_REGISTER_KIND,
124
+ prefix: '이수대장',
125
+ refType: 'Course',
126
+ asOf: true,
127
+ template: { content_template: exports.REGISTER_TEMPLATE, page_size: 'A4' },
128
+ async dataOf(context, refId, input) {
129
+ const course = await (0, shell_1.getRepository)(course_1.Course, context.state.tx).findOne({ where: { id: refId, domain: { id: context.state.domain.id } } });
130
+ if (!course)
131
+ throw new shell_1.Refusal('learning-not-found', {}, 'the record was not found. reload and try again.');
132
+ const timezone = context.state.domain.timezone;
133
+ const rows = (await (0, enrollment_status_1.completionRegister)(course.id, context)).filter(row => !input.asOf || dayOf(row.enrollment.completedAt, timezone) <= input.asOf);
134
+ return {
135
+ subjectName: course.name,
136
+ data: {
137
+ course: { name: course.name },
138
+ count: rows.length,
139
+ rows: rows.map((row, index) => ({
140
+ no: index + 1,
141
+ name: row.user?.name ?? '',
142
+ department: row.departmentName ?? '',
143
+ version: row.versionNumber ?? '',
144
+ completedOn: dayOf(row.enrollment.completedAt, timezone),
145
+ minutes: Math.floor((row.enrollment.learnedSeconds ?? 0) / 60),
146
+ score: row.enrollment.score ?? '',
147
+ certificateNo: row.enrollment.certificateNo ?? ''
148
+ }))
149
+ }
150
+ };
151
+ },
152
+ async mayIssue(context) {
153
+ return await (0, course_service_1.holds)(context, 'course', 'query');
154
+ },
155
+ async mayRead(context) {
156
+ return await (0, course_service_1.holds)(context, 'course', 'query');
157
+ }
158
+ });
159
+ }
160
+ //# sourceMappingURL=learning-documents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learning-documents.js","sourceRoot":"","sources":["../../../server/service/enrollment/learning-documents.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The documents this module issues through the pdf module (ADR-0194 decisions 9, 10 · ADR-0176): the certificate of a completed\n * enrolment (`learning.certificate`) and the completion register of a course (`learning.completion-register`). The pdf module\n * numbers and prints them with 「우리 회사」 - the company's name, representative and seal as the application keeps them.\n */\nimport { User } from '@things-factory/auth-base'\nimport { registerIssuedDocumentKind } from '@things-factory/pdf'\nimport { getRepository, Refusal } from '@things-factory/shell'\n\nimport { Course } from '../course/course'\nimport { CourseVersion } from '../course/course-version'\nimport { holds } from '../course/course-service'\nimport { Enrollment, EnrollmentStatus } from './enrollment'\nimport { completionRegister } from './enrollment-status'\n\nexport const LEARNING_CERTIFICATE_KIND = 'learning.certificate'\nexport const LEARNING_REGISTER_KIND = 'learning.completion-register'\n\nconst STYLE = `@page { size: A4; margin: 0 }\nbody { margin: 0; font-family: \"Apple SD Gothic Neo\", \"Malgun Gothic\", \"Noto Sans KR\", sans-serif; color: #222 }\n.page { position: relative; box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 30mm 24mm }\n.number { position: absolute; top: 16mm; left: 24mm; font-size: 9.5pt; color: #666 }\nh1 { margin: 6mm 0 16mm; text-align: center; font-size: 26pt; letter-spacing: 12pt }\ntable { width: 100%; border-collapse: collapse; font-size: 11.5pt }\nth, td { border: 1px solid #444; padding: 3.2mm 4mm; text-align: left; vertical-align: middle }\nth { background: #f2f2f2; font-weight: 600; text-align: center }\n.statement { margin: 22mm 0 0; text-align: center; font-size: 13pt; line-height: 2 }\n.date { margin-top: 18mm; text-align: center; font-size: 13pt; letter-spacing: 1pt }\n.issuer { position: relative; margin: 20mm auto 0; width: max-content; font-size: 14pt; line-height: 2; text-align: left; padding-right: 22mm }\n.issuer img { position: absolute; right: 0; top: 2mm; width: 22mm; opacity: .9 }`\n\nconst ISSUER = `<div class=\"issuer\">\n<div>상호 : {{company.name}}</div>\n{{#if company.address}}<div>주소 : {{company.address}}</div>{{/if}}\n<div>대표이사 : {{company.representative}}</div>\n{{#if company.seal}}<img src=\"{{company.seal}}\" alt=\"\">{{/if}}\n</div>`\n\n/** The certificate's template in code - a domain's own `PDFTemplate` of the kind prints in its place. */\nexport const CERTIFICATE_TEMPLATE = `<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n${STYLE}\nth { width: 32mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">발급번호 {{issue.number}}</div>\n<h1>수료증</h1>\n<table>\n<tr><th>성명</th><td>{{learner.name}}</td></tr>\n<tr><th>과정</th><td>{{course.name}} (개정본 {{course.version}})</td></tr>\n<tr><th>이수일</th><td>{{completion.completedOn}}</td></tr>\n<tr><th>학습 시간</th><td>{{completion.minutes}}분</td></tr>\n{{#if completion.score}}<tr><th>평가 점수</th><td>{{completion.score}}점</td></tr>{{/if}}\n<tr><th>수료 번호</th><td>{{completion.certificateNo}}</td></tr>\n</table>\n<p class=\"statement\">위 사람은 위 과정을 이수하였음을 증명합니다.</p>\n<div class=\"date\">{{issue.issuedOn}}</div>\n${ISSUER}\n</div></body></html>`\n\n/** The completion register's template - one row per completion. */\nexport const REGISTER_TEMPLATE = `<!doctype html><html lang=\"ko\"><head><meta charset=\"utf-8\"><style>\n${STYLE}\n.page { padding: 24mm 16mm }\nh1 { font-size: 20pt; letter-spacing: 4pt; margin-bottom: 8mm }\n.meta { margin-bottom: 6mm; font-size: 10.5pt }\ntable { font-size: 9.5pt }\nth, td { padding: 2mm 2.4mm }\n</style></head><body><div class=\"page\">\n<div class=\"number\">발급번호 {{issue.number}}</div>\n<h1>이수 대장</h1>\n<div class=\"meta\">과정 : {{course.name}} · 기준일 : {{issue.asOf}} · 이수자 {{count}}명</div>\n<table>\n<tr><th>번호</th><th>성명</th><th>부서</th><th>개정본</th><th>이수일</th><th>학습 시간(분)</th><th>점수</th><th>수료 번호</th></tr>\n{{#each rows}}<tr><td>{{this.no}}</td><td>{{this.name}}</td><td>{{this.department}}</td><td>{{this.version}}</td><td>{{this.completedOn}}</td><td>{{this.minutes}}</td><td>{{this.score}}</td><td>{{this.certificateNo}}</td></tr>{{/each}}\n</table>\n<div class=\"date\">{{issue.issuedOn}}</div>\n${ISSUER}\n</div></body></html>`\n\nconst dayOf = (at: Date | string | null | undefined, timezone?: string | null) =>\n at ? new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at)) : ''\n\nasync function enrollmentFor(context: any, id: string): Promise<Enrollment | null> {\n return id ? await getRepository(Enrollment, context.state.tx).findOne({ where: { id, domain: { id: context.state.domain.id } } as any }) : null\n}\n\n/** A certificate is issued and read by the person it is of, once completed, and by a manager. */\nasync function mayHaveCertificate(context: any, id: string | null | undefined): Promise<boolean> {\n const enrollment = await enrollmentFor(context, id ?? '')\n if (!enrollment || enrollment.status !== EnrollmentStatus.COMPLETED) return false\n return enrollment.userId === context.state.user?.id || (await holds(context, 'course', 'query'))\n}\n\nexport function registerLearningDocuments(): void {\n registerIssuedDocumentKind({\n kind: LEARNING_CERTIFICATE_KIND,\n prefix: '수료',\n refType: 'Enrollment',\n template: { content_template: CERTIFICATE_TEMPLATE, page_size: 'A4' },\n async dataOf(context, refId) {\n const enrollment = await enrollmentFor(context, refId)\n if (!enrollment || enrollment.status !== EnrollmentStatus.COMPLETED) throw new Refusal('learning-not-completed', {}, 'a certificate is issued for a completed enrolment')\n const tx = context.state.tx\n const course = await getRepository(Course, tx).findOneBy({ id: enrollment.courseId! })\n const version = await getRepository(CourseVersion, tx).findOne({ where: { id: enrollment.versionId! } as any, withDeleted: true })\n const learner = await getRepository(User, tx).findOneBy({ id: enrollment.userId! })\n return {\n subjectName: `${learner?.name ?? ''} · ${course?.name ?? ''}`,\n data: {\n learner: { name: learner?.name ?? '' },\n course: { name: course?.name ?? '', version: version?.number ?? '' },\n completion: {\n completedOn: dayOf(enrollment.completedAt, context.state.domain.timezone),\n minutes: Math.floor((enrollment.learnedSeconds ?? 0) / 60),\n score: enrollment.score ?? null,\n certificateNo: enrollment.certificateNo ?? ''\n }\n }\n }\n },\n async mayIssue(context, refId) {\n return await mayHaveCertificate(context, refId)\n },\n async mayRead(context, row) {\n return await mayHaveCertificate(context, row.refId)\n }\n })\n\n registerIssuedDocumentKind({\n kind: LEARNING_REGISTER_KIND,\n prefix: '이수대장',\n refType: 'Course',\n asOf: true,\n template: { content_template: REGISTER_TEMPLATE, page_size: 'A4' },\n async dataOf(context, refId, input) {\n const course = await getRepository(Course, context.state.tx).findOne({ where: { id: refId, domain: { id: context.state.domain.id } } as any })\n if (!course) throw new Refusal('learning-not-found', {}, 'the record was not found. reload and try again.')\n const timezone = context.state.domain.timezone\n const rows = (await completionRegister(course.id, context)).filter(row => !input.asOf || dayOf(row.enrollment.completedAt, timezone) <= input.asOf)\n return {\n subjectName: course.name,\n data: {\n course: { name: course.name },\n count: rows.length,\n rows: rows.map((row, index) => ({\n no: index + 1,\n name: row.user?.name ?? '',\n department: row.departmentName ?? '',\n version: row.versionNumber ?? '',\n completedOn: dayOf(row.enrollment.completedAt, timezone),\n minutes: Math.floor((row.enrollment.learnedSeconds ?? 0) / 60),\n score: row.enrollment.score ?? '',\n certificateNo: row.enrollment.certificateNo ?? ''\n }))\n }\n }\n },\n async mayIssue(context) {\n return await holds(context, 'course', 'query')\n },\n async mayRead(context) {\n return await holds(context, 'course', 'query')\n }\n })\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import type { Enrollment } from './enrollment';
2
+ export interface LearningOutcomeHandler {
3
+ /** The enrolment was completed - its evidence is on it. Throwing takes the completion back. */
4
+ completed(enrollment: Enrollment, context: any): Promise<void>;
5
+ /** A completed enrolment was cancelled by a manager, with the reason. Throwing takes the cancellation back. */
6
+ cancelled?(enrollment: Enrollment, reason: string, context: any): Promise<void>;
7
+ }
8
+ /** Registers the handler of a source kind (`compliance.training-session`) - a second registration replaces the first. */
9
+ export declare function registerLearningOutcome(sourceKind: string, handler: LearningOutcomeHandler | null): void;
10
+ export declare function learningOutcomeOf(sourceKind: string): LearningOutcomeHandler | undefined;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerLearningOutcome = registerLearningOutcome;
4
+ exports.learningOutcomeOf = learningOutcomeOf;
5
+ const handlers = new Map();
6
+ /** Registers the handler of a source kind (`compliance.training-session`) - a second registration replaces the first. */
7
+ function registerLearningOutcome(sourceKind, handler) {
8
+ if (handler)
9
+ handlers.set(sourceKind, handler);
10
+ else
11
+ handlers.delete(sourceKind);
12
+ }
13
+ function learningOutcomeOf(sourceKind) {
14
+ return handlers.get(sourceKind);
15
+ }
16
+ //# sourceMappingURL=learning-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learning-outcome.js","sourceRoot":"","sources":["../../../server/service/enrollment/learning-outcome.ts"],"names":[],"mappings":";;AAiBA,0DAGC;AAED,8CAEC;AAVD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE1D,yHAAyH;AACzH,SAAgB,uBAAuB,CAAC,UAAkB,EAAE,OAAsC;IAChG,IAAI,OAAO;QAAE,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;;QACzC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;AAClC,CAAC;AAED,SAAgB,iBAAiB,CAAC,UAAkB;IAClD,OAAO,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;AACjC,CAAC","sourcesContent":["/*\n * Telling whoever asked for an enrolment that it was completed or cancelled (ADR-0194 decisions 7, 8) - the same shape\n * as accounting's `registerPaymentTarget`. The module that asks for enrolments registers one handler per source kind;\n * this module calls it in the completion's own transaction, so a handler that fails takes the completion back with it.\n */\nimport type { Enrollment } from './enrollment'\n\nexport interface LearningOutcomeHandler {\n /** The enrolment was completed - its evidence is on it. Throwing takes the completion back. */\n completed(enrollment: Enrollment, context: any): Promise<void>\n /** A completed enrolment was cancelled by a manager, with the reason. Throwing takes the cancellation back. */\n cancelled?(enrollment: Enrollment, reason: string, context: any): Promise<void>\n}\n\nconst handlers = new Map<string, LearningOutcomeHandler>()\n\n/** Registers the handler of a source kind (`compliance.training-session`) - a second registration replaces the first. */\nexport function registerLearningOutcome(sourceKind: string, handler: LearningOutcomeHandler | null): void {\n if (handler) handlers.set(sourceKind, handler)\n else handlers.delete(sourceKind)\n}\n\nexport function learningOutcomeOf(sourceKind: string): LearningOutcomeHandler | undefined {\n return handlers.get(sourceKind)\n}\n"]}
@@ -0,0 +1,94 @@
1
+ /** The longest a tick is counted for, in seconds - the screen sends one every 15 seconds looked at. */
2
+ export declare const TICK_SECONDS = 15;
3
+ /** How far ahead of what was watched a position still reads as watching - the video's own step between two updates. */
4
+ export declare const POSITION_SLACK_SECONDS = 2;
5
+ /** How long a PDF or a text is looked at to be done when its unit says nothing. */
6
+ export declare const DEFAULT_UNIT_SECONDS = 60;
7
+ export interface ProgressNow {
8
+ learnedSeconds: number;
9
+ watchedSeconds: number;
10
+ lastTickAt?: Date | null;
11
+ }
12
+ export interface Tick {
13
+ /** What the screen says it looked at since its last tick - it may lower what is counted, never raise it. */
14
+ seconds: number;
15
+ /** Where the video stands, in seconds. */
16
+ position: number;
17
+ }
18
+ export type TickVerdict = {
19
+ refusal: 'position-ahead';
20
+ allowed: number;
21
+ } | {
22
+ add: number;
23
+ watchedSeconds: number;
24
+ positionSeconds: number;
25
+ };
26
+ /**
27
+ * What one tick does (decision 6). The time added is the gap since the last tick, at most 15 seconds and at most what
28
+ * the screen says - a tick with no tick before it starts the clock and adds nothing. A video's position may not be ahead
29
+ * of what was watched by more than the time that passed: a move ahead is refused, a move back is free.
30
+ */
31
+ export declare function tickVerdict(progress: ProgressNow, tick: Tick, now: Date, video: boolean): TickVerdict;
32
+ export interface UnitShape {
33
+ kind: string;
34
+ durationSeconds?: number | null;
35
+ minSeconds?: number | null;
36
+ }
37
+ /** Whether a unit counts toward completion - a link does not: the server cannot know what was watched elsewhere. */
38
+ export declare const counts: (unit: UnitShape) => boolean;
39
+ /**
40
+ * Whether a unit is done (decision 3): a video watched to its end (its length known) **and** its running time counted
41
+ * by the server - a skip or a faster playback reaches neither; a PDF or a text looked at for its time. A link is never
42
+ * done - it does not count.
43
+ */
44
+ export declare function unitDone(unit: UnitShape, progress: {
45
+ learnedSeconds: number;
46
+ watchedSeconds: number;
47
+ }): boolean;
48
+ export interface CompletionFacts {
49
+ /** The units that count, and the ones of them done. */
50
+ units: number;
51
+ unitsDone: number;
52
+ learnedSeconds: number;
53
+ minSeconds: number;
54
+ /** The version has a quiz - questions and a pass score. */
55
+ quiz: boolean;
56
+ passed: boolean;
57
+ /** The version asks for an acknowledgement. */
58
+ acknowledgement: boolean;
59
+ acknowledged: boolean;
60
+ }
61
+ export type Missing = 'units' | 'time' | 'quiz' | 'acknowledgement';
62
+ /**
63
+ * What still stands between an enrolment and its completion (decision 8) - every unit done, the least time, the quiz
64
+ * passed when there is one, the acknowledgement confirmed when one is asked. Complete when nothing is missing.
65
+ */
66
+ export declare function missingOf(facts: CompletionFacts): Missing[];
67
+ export interface QuestionKey {
68
+ id: string;
69
+ kind: string;
70
+ choices: {
71
+ id: string;
72
+ correct: boolean;
73
+ }[];
74
+ }
75
+ /**
76
+ * A quiz's score, out of 100 (decision 4): each question is right when the choices picked are exactly the right ones -
77
+ * one for SINGLE and TRUE_FALSE, every right one and no other for MULTIPLE.
78
+ */
79
+ export declare function scoreOf(questions: QuestionKey[], answers: Record<string, string[]>): number;
80
+ /** The questions an attempt draws - `count` of them (all when empty), in a shuffled order, each with its choices shuffled. */
81
+ export declare function drawQuestions<Q extends {
82
+ id: string;
83
+ choices: {
84
+ id: string;
85
+ }[];
86
+ }>(questions: Q[], count: number | null | undefined, random?: () => number): {
87
+ questionId: string;
88
+ choiceIds: string[];
89
+ }[];
90
+ /** The day an enrolment reads as overdue: open, and its due day before today. */
91
+ export declare const overdue: (enrollment: {
92
+ status: string;
93
+ dueOn?: string | null;
94
+ }, today: string) => boolean;