@things-factory/compliance 10.1.259 → 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.
- package/dist-client/components/my-trainings.js +6 -2
- package/dist-client/components/my-trainings.js.map +1 -1
- package/dist-client/components/training-session-form.d.ts +3 -0
- package/dist-client/components/training-session-form.js +24 -1
- package/dist-client/components/training-session-form.js.map +1 -1
- package/dist-client/legal-record/legal-child-grid.d.ts +12 -2
- package/dist-client/legal-record/legal-child-grid.js +22 -2
- package/dist-client/legal-record/legal-child-grid.js.map +1 -1
- package/dist-client/legal-record/legal-grid.d.ts +1 -1
- package/dist-client/legal-record/legal-grid.js +4 -2
- package/dist-client/legal-record/legal-grid.js.map +1 -1
- package/dist-client/pages/compliance-case-list.js +1 -1
- package/dist-client/pages/compliance-case-list.js.map +1 -1
- package/dist-client/pages/dispute-list.js +5 -3
- package/dist-client/pages/dispute-list.js.map +1 -1
- package/dist-client/pages/invention-list.js +4 -3
- package/dist-client/pages/invention-list.js.map +1 -1
- package/dist-client/pages/legal-registration-list.js +6 -4
- package/dist-client/pages/legal-registration-list.js.map +1 -1
- package/dist-client/pages/privacy-record-list.d.ts +1 -1
- package/dist-client/pages/privacy-record-list.js +16 -6
- package/dist-client/pages/privacy-record-list.js.map +1 -1
- package/dist-client/pages/regulation-list.js +2 -2
- package/dist-client/pages/regulation-list.js.map +1 -1
- package/dist-client/pages/seal-use-list.d.ts +7 -0
- package/dist-client/pages/seal-use-list.js +4 -4
- package/dist-client/pages/seal-use-list.js.map +1 -1
- package/dist-client/pages/training-list.d.ts +16 -2
- package/dist-client/pages/training-list.js +63 -31
- package/dist-client/pages/training-list.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/training/training-checks.d.ts +5 -0
- package/dist-server/service/training/training-checks.js +24 -0
- package/dist-server/service/training/training-checks.js.map +1 -1
- package/dist-server/service/training/training-query.d.ts +2 -0
- package/dist-server/service/training/training-query.js +11 -0
- package/dist-server/service/training/training-query.js.map +1 -1
- package/dist-server/service/training/training-register.js +6 -0
- package/dist-server/service/training/training-register.js.map +1 -1
- package/dist-server/service/training/training-rules.d.ts +7 -0
- package/dist-server/service/training/training-rules.js +8 -0
- package/dist-server/service/training/training-rules.js.map +1 -1
- package/dist-server/service/training/training-service.d.ts +11 -0
- package/dist-server/service/training/training-service.js +104 -3
- package/dist-server/service/training/training-service.js.map +1 -1
- package/dist-server/service/training/training-type.d.ts +1 -0
- package/dist-server/service/training/training-type.js +4 -0
- package/dist-server/service/training/training-type.js.map +1 -1
- package/dist-server/service/training/training.d.ts +13 -0
- package/dist-server/service/training/training.js +51 -1
- package/dist-server/service/training/training.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -18
- package/translations/en.json +19 -7
- package/translations/es.json +18 -6
- package/translations/ja.json +19 -7
- package/translations/ko.json +19 -7
- package/translations/ms.json +19 -7
- package/translations/zh.json +19 -7
|
@@ -18,3 +18,8 @@ export declare function trainingCertificatesWaiting(tx: EntityManager, domain: a
|
|
|
18
18
|
employee: Employee;
|
|
19
19
|
waitingDays: number;
|
|
20
20
|
}[]>;
|
|
21
|
+
/**
|
|
22
|
+
* The online sessions of this year past their due day with people not done (ADR-0194 decision 15) - each with how many
|
|
23
|
+
* enrolments are still open. The learning module counts the people; reminding them is its status screen's.
|
|
24
|
+
*/
|
|
25
|
+
export declare function trainingOnlineOverdue(tx: EntityManager, domain: any, today: string): Promise<any[]>;
|
|
@@ -4,7 +4,9 @@ exports.TRAINING_DUE_SOON_DAYS = void 0;
|
|
|
4
4
|
exports.trainingCompletionRates = trainingCompletionRates;
|
|
5
5
|
exports.trainingsDueWithoutSession = trainingsDueWithoutSession;
|
|
6
6
|
exports.trainingCertificatesWaiting = trainingCertificatesWaiting;
|
|
7
|
+
exports.trainingOnlineOverdue = trainingOnlineOverdue;
|
|
7
8
|
const typeorm_1 = require("typeorm");
|
|
9
|
+
const learning_1 = require("@things-factory/learning");
|
|
8
10
|
const organization_1 = require("@things-factory/organization");
|
|
9
11
|
const training_1 = require("./training");
|
|
10
12
|
const training_rules_1 = require("./training-rules");
|
|
@@ -53,4 +55,26 @@ async function trainingCertificatesWaiting(tx, domain, today) {
|
|
|
53
55
|
const employees = rows.length ? await tx.getRepository(organization_1.Employee).find({ where: { id: (0, typeorm_1.In)([...new Set(rows.map(one => one.employeeId))]) } }) : [];
|
|
54
56
|
return rows.map(completion => ({ completion, employee: employees.find(one => one.id === completion.employeeId) ?? null, waitingDays: (0, training_rules_1.trainingDaysLeft)(completion.createdAt.toISOString().slice(0, 10), today) }));
|
|
55
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The online sessions of this year past their due day with people not done (ADR-0194 decision 15) - each with how many
|
|
60
|
+
* enrolments are still open. The learning module counts the people; reminding them is its status screen's.
|
|
61
|
+
*/
|
|
62
|
+
async function trainingOnlineOverdue(tx, domain, today) {
|
|
63
|
+
const year = yearOf(today);
|
|
64
|
+
const kinds = await (0, training_service_1.trainingKindsOf)(tx, domain);
|
|
65
|
+
const sessions = await tx.getRepository(training_1.TrainingSession).find({ where: { domain: { id: domain.id }, year, method: training_1.TrainingMethod.ONLINE } });
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const session of sessions) {
|
|
68
|
+
const kind = kinds.find(one => one.code === session.kind);
|
|
69
|
+
if (!kind?.enabled)
|
|
70
|
+
continue;
|
|
71
|
+
const dueOn = (0, training_rules_1.onlineDueOnOf)(session, kind.dueDay);
|
|
72
|
+
if (dueOn >= today)
|
|
73
|
+
continue;
|
|
74
|
+
const open = await tx.getRepository(learning_1.Enrollment).count({ where: { domain: { id: domain.id }, sourceKind: training_service_1.TRAINING_SESSION_SOURCE, sourceId: session.id, status: (0, typeorm_1.In)([learning_1.EnrollmentStatus.ASSIGNED, learning_1.EnrollmentStatus.IN_PROGRESS]) } });
|
|
75
|
+
if (open)
|
|
76
|
+
out.push({ session, kind: session.kind, dueOn, open, daysOver: (0, training_rules_1.trainingDaysLeft)(dueOn, today) });
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
56
80
|
//# sourceMappingURL=training-checks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"training-checks.js","sourceRoot":"","sources":["../../../server/service/training/training-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Employee } from '@things-factory/organization'\n\nimport { TrainingCompletion, TrainingCompletionStatus, TrainingSession } from './training'\nimport { trainingCompletionRate, trainingDaysLeft, trainingDueOnOf, TrainingState } from './training-rules'\nimport { trainingAudiencesOf, trainingPeopleOf, trainingKindsOf } from './training-service'\n\n/*\n * 관리 점검 (ADR-0146, ADR-0187 decision 19) - what the mandatory trainings say is late or missing, for each kind the\n * domain has on. The application's management check reads these and says them in its own items.\n */\n\n/** A session is to be opened when the kind's due day is this many days away or less. */\nexport const TRAINING_DUE_SOON_DAYS = 30\n\nconst yearOf = (today: string) => Number(today.slice(0, 4))\n\n/** This year's completion of each kind on: how many must take it, how many are done, and the days left to its due day. */\nexport async function trainingCompletionRates(tx: EntityManager, domain: any, today: string) {\n const year = yearOf(today)\n const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled)\n const confirmed = await tx.getRepository(TrainingCompletion).find({ where: { domain: { id: domain.id }, year, status: TrainingCompletionStatus.CONFIRMED } as any })\n const out = []\n for (const kind of kinds) {\n const people = await trainingPeopleOf(tx, domain, trainingAudiencesOf(kind, domain), today)\n const done = new Set(confirmed.filter(one => one.kind === kind.code).map(one => one.employeeId))\n const states: TrainingState[] = people.map(one => (done.has(one.id) ? 'DONE' : 'NOT_DONE'))\n const dueOn = trainingDueOnOf(year, kind.dueDay)\n out.push({ kind: kind.code, year, dueOn, daysLeft: trainingDaysLeft(today, dueOn), people: people.length, done: states.filter(one => one === 'DONE').length, rate: trainingCompletionRate(states) })\n }\n return out\n}\n\n/** The kinds on whose due day comes within 30 days, with no session of this year yet. */\nexport async function trainingsDueWithoutSession(tx: EntityManager, domain: any, today: string) {\n const year = yearOf(today)\n const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled)\n const sessions = await tx.getRepository(TrainingSession).find({ where: { domain: { id: domain.id }, year } as any })\n return kinds\n .map(kind => ({ kind: kind.code, year, dueOn: trainingDueOnOf(year, kind.dueDay) }))\n .map(one => ({ ...one, daysLeft: trainingDaysLeft(today, one.dueOn) }))\n .filter(one => one.daysLeft >= 0 && one.daysLeft <= TRAINING_DUE_SOON_DAYS && !sessions.some(session => session.kind === one.kind))\n}\n\n/** The certificates people sent that wait for the manager, oldest first - of the kinds on. */\nexport async function trainingCertificatesWaiting(tx: EntityManager, domain: any, today: string) {\n const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled).map(one => one.code)\n if (!kinds.length) return []\n const rows = await tx.getRepository(TrainingCompletion).find({\n where: { domain: { id: domain.id }, status: TrainingCompletionStatus.SUBMITTED, kind: In(kinds) } as any,\n order: { createdAt: 'ASC' } as any\n })\n const employees = rows.length ? await tx.getRepository(Employee).find({ where: { id: In([...new Set(rows.map(one => one.employeeId!))]) } as any }) : []\n return rows.map(completion => ({ completion, employee: employees.find(one => one.id === completion.employeeId) ?? null, waitingDays: trainingDaysLeft(completion.createdAt!.toISOString().slice(0, 10), today) }))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"training-checks.js","sourceRoot":"","sources":["../../../server/service/training/training-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Enrollment, EnrollmentStatus } from '@things-factory/learning'\nimport { Employee } from '@things-factory/organization'\n\nimport { TrainingCompletion, TrainingCompletionStatus, TrainingMethod, TrainingSession } from './training'\nimport { onlineDueOnOf, trainingCompletionRate, trainingDaysLeft, trainingDueOnOf, TrainingState } from './training-rules'\nimport { TRAINING_SESSION_SOURCE, trainingAudiencesOf, trainingPeopleOf, trainingKindsOf } from './training-service'\n\n/*\n * 관리 점검 (ADR-0146, ADR-0187 decision 19) - what the mandatory trainings say is late or missing, for each kind the\n * domain has on. The application's management check reads these and says them in its own items.\n */\n\n/** A session is to be opened when the kind's due day is this many days away or less. */\nexport const TRAINING_DUE_SOON_DAYS = 30\n\nconst yearOf = (today: string) => Number(today.slice(0, 4))\n\n/** This year's completion of each kind on: how many must take it, how many are done, and the days left to its due day. */\nexport async function trainingCompletionRates(tx: EntityManager, domain: any, today: string) {\n const year = yearOf(today)\n const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled)\n const confirmed = await tx.getRepository(TrainingCompletion).find({ where: { domain: { id: domain.id }, year, status: TrainingCompletionStatus.CONFIRMED } as any })\n const out = []\n for (const kind of kinds) {\n const people = await trainingPeopleOf(tx, domain, trainingAudiencesOf(kind, domain), today)\n const done = new Set(confirmed.filter(one => one.kind === kind.code).map(one => one.employeeId))\n const states: TrainingState[] = people.map(one => (done.has(one.id) ? 'DONE' : 'NOT_DONE'))\n const dueOn = trainingDueOnOf(year, kind.dueDay)\n out.push({ kind: kind.code, year, dueOn, daysLeft: trainingDaysLeft(today, dueOn), people: people.length, done: states.filter(one => one === 'DONE').length, rate: trainingCompletionRate(states) })\n }\n return out\n}\n\n/** The kinds on whose due day comes within 30 days, with no session of this year yet. */\nexport async function trainingsDueWithoutSession(tx: EntityManager, domain: any, today: string) {\n const year = yearOf(today)\n const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled)\n const sessions = await tx.getRepository(TrainingSession).find({ where: { domain: { id: domain.id }, year } as any })\n return kinds\n .map(kind => ({ kind: kind.code, year, dueOn: trainingDueOnOf(year, kind.dueDay) }))\n .map(one => ({ ...one, daysLeft: trainingDaysLeft(today, one.dueOn) }))\n .filter(one => one.daysLeft >= 0 && one.daysLeft <= TRAINING_DUE_SOON_DAYS && !sessions.some(session => session.kind === one.kind))\n}\n\n/** The certificates people sent that wait for the manager, oldest first - of the kinds on. */\nexport async function trainingCertificatesWaiting(tx: EntityManager, domain: any, today: string) {\n const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled).map(one => one.code)\n if (!kinds.length) return []\n const rows = await tx.getRepository(TrainingCompletion).find({\n where: { domain: { id: domain.id }, status: TrainingCompletionStatus.SUBMITTED, kind: In(kinds) } as any,\n order: { createdAt: 'ASC' } as any\n })\n const employees = rows.length ? await tx.getRepository(Employee).find({ where: { id: In([...new Set(rows.map(one => one.employeeId!))]) } as any }) : []\n return rows.map(completion => ({ completion, employee: employees.find(one => one.id === completion.employeeId) ?? null, waitingDays: trainingDaysLeft(completion.createdAt!.toISOString().slice(0, 10), today) }))\n}\n\n/**\n * The online sessions of this year past their due day with people not done (ADR-0194 decision 15) - each with how many\n * enrolments are still open. The learning module counts the people; reminding them is its status screen's.\n */\nexport async function trainingOnlineOverdue(tx: EntityManager, domain: any, today: string) {\n const year = yearOf(today)\n const kinds = await trainingKindsOf(tx, domain)\n const sessions = await tx.getRepository(TrainingSession).find({ where: { domain: { id: domain.id }, year, method: TrainingMethod.ONLINE } as any })\n const out = []\n for (const session of sessions) {\n const kind = kinds.find(one => one.code === session.kind)\n if (!kind?.enabled) continue\n const dueOn = onlineDueOnOf(session, kind.dueDay)\n if (dueOn >= today) continue\n const open = await tx.getRepository(Enrollment).count({ where: { domain: { id: domain.id }, sourceKind: TRAINING_SESSION_SOURCE, sourceId: session.id, status: In([EnrollmentStatus.ASSIGNED, EnrollmentStatus.IN_PROGRESS]) } as any })\n if (open) out.push({ session, kind: session.kind, dueOn, open, daysOver: trainingDaysLeft(dueOn, today) })\n }\n return out\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Attachment } from '@things-factory/attachment-base';
|
|
2
2
|
import { User } from '@things-factory/auth-base';
|
|
3
|
+
import { Course } from '@things-factory/learning';
|
|
3
4
|
import { Employee } from '@things-factory/organization';
|
|
4
5
|
import { ListParam } from '@things-factory/shell';
|
|
5
6
|
import { TrainingCompletion, TrainingSession } from './training';
|
|
@@ -11,6 +12,7 @@ export declare class TrainingQuery {
|
|
|
11
12
|
trainingStandings(params: ListParam, context: ResolverContext): Promise<TrainingStandingList>;
|
|
12
13
|
myTrainings(year: number, context: ResolverContext): Promise<MyTraining[]>;
|
|
13
14
|
attachments(row: TrainingSession, context: ResolverContext): Promise<Attachment[]>;
|
|
15
|
+
course(row: TrainingSession): Promise<Course | null>;
|
|
14
16
|
creator(row: TrainingSession): Promise<User | null>;
|
|
15
17
|
}
|
|
16
18
|
export declare class TrainingCompletionQuery {
|
|
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const type_graphql_1 = require("type-graphql");
|
|
6
6
|
const attachment_base_1 = require("@things-factory/attachment-base");
|
|
7
7
|
const auth_base_1 = require("@things-factory/auth-base");
|
|
8
|
+
const learning_1 = require("@things-factory/learning");
|
|
8
9
|
const organization_1 = require("@things-factory/organization");
|
|
9
10
|
const shell_1 = require("@things-factory/shell");
|
|
10
11
|
const employee_reader_1 = require("../common/employee-reader");
|
|
@@ -72,6 +73,9 @@ let TrainingQuery = class TrainingQuery {
|
|
|
72
73
|
async attachments(row, context) {
|
|
73
74
|
return await filesOf(training_service_1.TRAINING_SESSION_REF_TYPE, row.id, context);
|
|
74
75
|
}
|
|
76
|
+
async course(row) {
|
|
77
|
+
return row.courseId ? await (0, shell_1.getRepository)(learning_1.Course).findOne({ where: { id: row.courseId } }) : null;
|
|
78
|
+
}
|
|
75
79
|
async creator(row) {
|
|
76
80
|
return row.creatorId ? await (0, shell_1.getRepository)(auth_base_1.User).findOne({ where: { id: row.creatorId } }) : null;
|
|
77
81
|
}
|
|
@@ -130,6 +134,13 @@ tslib_1.__decorate([
|
|
|
130
134
|
tslib_1.__metadata("design:paramtypes", [training_1.TrainingSession, Object]),
|
|
131
135
|
tslib_1.__metadata("design:returntype", Promise)
|
|
132
136
|
], TrainingQuery.prototype, "attachments", null);
|
|
137
|
+
tslib_1.__decorate([
|
|
138
|
+
(0, type_graphql_1.FieldResolver)(type => learning_1.Course, { nullable: true, description: 'The course an ONLINE session is taken on' }),
|
|
139
|
+
tslib_1.__param(0, (0, type_graphql_1.Root)()),
|
|
140
|
+
tslib_1.__metadata("design:type", Function),
|
|
141
|
+
tslib_1.__metadata("design:paramtypes", [training_1.TrainingSession]),
|
|
142
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
143
|
+
], TrainingQuery.prototype, "course", null);
|
|
133
144
|
tslib_1.__decorate([
|
|
134
145
|
(0, type_graphql_1.FieldResolver)(type => auth_base_1.User, { nullable: true }),
|
|
135
146
|
tslib_1.__param(0, (0, type_graphql_1.Root)()),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"training-query.js","sourceRoot":"","sources":["../../../server/service/training/training-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Args, Ctx, Directive, FieldResolver, Int, Query, Resolver, Root, UseMiddleware } from 'type-graphql'\n\nimport { Attachment, readableRows } from '@things-factory/attachment-base'\nimport { User } from '@things-factory/auth-base'\nimport { Employee } from '@things-factory/organization'\nimport { getQueryBuilderFromListParams, getRepository, ListParam, Refusal, SpeakRefusals } from '@things-factory/shell'\n\nimport { todayIn } from '../common/employee-reader'\nimport { TrainingCompletion, TrainingSession } from './training'\nimport { trainingManagerOf, myTrainings, readableStandings, TRAINING_COMPLETION_REF_TYPE, TRAINING_SESSION_REF_TYPE, trainingKindsOf, trainingReaderOf } from './training-service'\nimport { MyTraining, TrainingCompletionList, TrainingKindInDomain, TrainingSessionList, TrainingStandingList } from './training-type'\n\nconst thisYear = (context: ResolverContext) => Number(todayIn(context.state.domain).slice(0, 4))\n\n/** A filter's values as a list - `eq` one, `in` many. */\nconst valuesOf = (params: ListParam, name: string) => {\n const filter = params.filters?.find(one => one.name === name)\n if (!filter || filter.value === undefined || filter.value === null || filter.value === '') return null\n return { values: [filter.value].flat().map(String), not: filter.operator === 'notin' || filter.operator === 'noteq' }\n}\n\n@Resolver(TrainingSession)\n@UseMiddleware(SpeakRefusals)\nexport class TrainingQuery {\n @Directive('@privilege(category: \"training\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => [TrainingKindInDomain], { description: '법정 의무 교육 종류 - each kind as the domain has it: on or off, due day, audience (ADR-0187 decision 15)' })\n async trainingKinds(@Ctx() context: ResolverContext): Promise<TrainingKindInDomain[]> {\n return (await trainingKindsOf(trainingManagerOf(context), context.state.domain)) as any\n }\n\n @Directive('@privilege(category: \"training\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => TrainingSessionList, { description: '교육 회차 - the trainings given, newest first (ADR-0187 decision 16)' })\n async trainingSessions(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<TrainingSessionList> {\n const queryBuilder = getQueryBuilderFromListParams({\n domain: context.state.domain,\n params,\n repository: getRepository(TrainingSession),\n searchables: ['name', 'instructor', 'organisation', 'notes'],\n alias: 'session',\n defaultSortings: [{ name: 'startsOn', desc: true }]\n })\n const [items, total] = await queryBuilder.getManyAndCount()\n return { items, total }\n }\n\n @Query(returns => TrainingCompletionList, {\n description: '이수 기록 - the completions with their certificates: every one to a holder of the trainings, their own to an employee (ADR-0187 decision 17)'\n })\n async trainingCompletions(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<TrainingCompletionList> {\n const reader = await trainingReaderOf(context)\n if (!reader.all && !reader.employee) throw new Refusal('compliance-employees-only', {}, 'only an employee of this company can do this')\n const queryBuilder = getQueryBuilderFromListParams({\n domain: context.state.domain,\n params,\n repository: getRepository(TrainingCompletion),\n searchables: ['organisation', 'returnReason'],\n alias: 'completion',\n defaultSortings: [{ name: 'createdAt', desc: true }]\n })\n if (!reader.all) queryBuilder.andWhere('completion.employee = :mine', { mine: reader.employee!.id })\n const [items, total] = await queryBuilder.getManyAndCount()\n return { items, total }\n }\n\n @Query(returns => TrainingStandingList, {\n description:\n '이수 현황 - where each person stands on each kind on in a year (DONE, WAITING, NOT_DONE): all to a holder, their own to an employee, whether done to a department head for their people (ADR-0187 decisions 17, 19)'\n })\n async trainingStandings(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<TrainingStandingList> {\n const year = Number(valuesOf(params, 'year')?.values[0] ?? thisYear(context))\n let rows = await readableStandings(year, context)\n for (const name of ['kind', 'state']) {\n const asked = valuesOf(params, name)\n if (asked) rows = rows.filter(row => asked.values.includes((row as any)[name]) !== asked.not)\n }\n const search = params.search ?? params.filters?.find(one => one.operator === 'search')?.value\n if (search) rows = rows.filter(row => `${row.employee.name ?? ''} ${row.departmentName ?? ''}`.includes(String(search).replace(/%/g, '').trim()))\n const { page = 1, limit = 0 } = params.pagination ?? ({} as any)\n return { items: (limit > 0 ? rows.slice((page - 1) * limit, page * limit) : rows) as any, total: rows.length }\n }\n\n @Query(returns => [MyTraining], { description: \"내 의무 교육 - the request's employee's trainings of a year, this year when none is given (ADR-0187 decision 10)\" })\n async myTrainings(@Arg('year', type => Int, { nullable: true }) year: number, @Ctx() context: ResolverContext): Promise<MyTraining[]> {\n return (await myTrainings(year ?? thisYear(context), context)) as any\n }\n\n @FieldResolver(type => [Attachment], { description: 'Its material - to a holder of the trainings and to the people it is for' })\n async attachments(@Root() row: TrainingSession, @Ctx() context: ResolverContext): Promise<Attachment[]> {\n return await filesOf(TRAINING_SESSION_REF_TYPE, row.id, context)\n }\n\n @FieldResolver(type => User, { nullable: true })\n async creator(@Root() row: TrainingSession): Promise<User | null> {\n return row.creatorId ? await getRepository(User).findOne({ where: { id: row.creatorId } }) : null\n }\n}\n\n@Resolver(TrainingCompletion)\nexport class TrainingCompletionQuery {\n @FieldResolver(type => Employee, { nullable: true })\n async employee(@Root() row: TrainingCompletion): Promise<Employee | null> {\n return row.employeeId ? await getRepository(Employee).findOne({ where: { id: row.employeeId } }) : null\n }\n\n @FieldResolver(type => TrainingSession, { nullable: true })\n async session(@Root() row: TrainingCompletion): Promise<TrainingSession | null> {\n return row.sessionId ? await getRepository(TrainingSession).findOne({ where: { id: row.sessionId } }) : null\n }\n\n @FieldResolver(type => User, { nullable: true })\n async decidedBy(@Root() row: TrainingCompletion): Promise<User | null> {\n return row.decidedById ? await getRepository(User).findOne({ where: { id: row.decidedById } }) : null\n }\n\n @FieldResolver(type => [Attachment], { description: 'Its certificates - to a holder of the trainings and to the one it is of' })\n async attachments(@Root() row: TrainingCompletion, @Ctx() context: ResolverContext): Promise<Attachment[]> {\n return await filesOf(TRAINING_COMPLETION_REF_TYPE, row.id, context)\n }\n}\n\n/** A record's files the request may open - the module's attachment reader decides each. */\nasync function filesOf(refType: string, refBy: string, context: ResolverContext): Promise<Attachment[]> {\n const rows = await getRepository(Attachment).find({ where: { domain: { id: context.state.domain.id }, refType, refBy } as any, order: { createdAt: 'ASC' } as any })\n return await readableRows(rows, context)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"training-query.js","sourceRoot":"","sources":["../../../server/service/training/training-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Args, Ctx, Directive, FieldResolver, Int, Query, Resolver, Root, UseMiddleware } from 'type-graphql'\n\nimport { Attachment, readableRows } from '@things-factory/attachment-base'\nimport { User } from '@things-factory/auth-base'\nimport { Course } from '@things-factory/learning'\nimport { Employee } from '@things-factory/organization'\nimport { getQueryBuilderFromListParams, getRepository, ListParam, Refusal, SpeakRefusals } from '@things-factory/shell'\n\nimport { todayIn } from '../common/employee-reader'\nimport { TrainingCompletion, TrainingSession } from './training'\nimport { trainingManagerOf, myTrainings, readableStandings, TRAINING_COMPLETION_REF_TYPE, TRAINING_SESSION_REF_TYPE, trainingKindsOf, trainingReaderOf } from './training-service'\nimport { MyTraining, TrainingCompletionList, TrainingKindInDomain, TrainingSessionList, TrainingStandingList } from './training-type'\n\nconst thisYear = (context: ResolverContext) => Number(todayIn(context.state.domain).slice(0, 4))\n\n/** A filter's values as a list - `eq` one, `in` many. */\nconst valuesOf = (params: ListParam, name: string) => {\n const filter = params.filters?.find(one => one.name === name)\n if (!filter || filter.value === undefined || filter.value === null || filter.value === '') return null\n return { values: [filter.value].flat().map(String), not: filter.operator === 'notin' || filter.operator === 'noteq' }\n}\n\n@Resolver(TrainingSession)\n@UseMiddleware(SpeakRefusals)\nexport class TrainingQuery {\n @Directive('@privilege(category: \"training\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => [TrainingKindInDomain], { description: '법정 의무 교육 종류 - each kind as the domain has it: on or off, due day, audience (ADR-0187 decision 15)' })\n async trainingKinds(@Ctx() context: ResolverContext): Promise<TrainingKindInDomain[]> {\n return (await trainingKindsOf(trainingManagerOf(context), context.state.domain)) as any\n }\n\n @Directive('@privilege(category: \"training\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => TrainingSessionList, { description: '교육 회차 - the trainings given, newest first (ADR-0187 decision 16)' })\n async trainingSessions(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<TrainingSessionList> {\n const queryBuilder = getQueryBuilderFromListParams({\n domain: context.state.domain,\n params,\n repository: getRepository(TrainingSession),\n searchables: ['name', 'instructor', 'organisation', 'notes'],\n alias: 'session',\n defaultSortings: [{ name: 'startsOn', desc: true }]\n })\n const [items, total] = await queryBuilder.getManyAndCount()\n return { items, total }\n }\n\n @Query(returns => TrainingCompletionList, {\n description: '이수 기록 - the completions with their certificates: every one to a holder of the trainings, their own to an employee (ADR-0187 decision 17)'\n })\n async trainingCompletions(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<TrainingCompletionList> {\n const reader = await trainingReaderOf(context)\n if (!reader.all && !reader.employee) throw new Refusal('compliance-employees-only', {}, 'only an employee of this company can do this')\n const queryBuilder = getQueryBuilderFromListParams({\n domain: context.state.domain,\n params,\n repository: getRepository(TrainingCompletion),\n searchables: ['organisation', 'returnReason'],\n alias: 'completion',\n defaultSortings: [{ name: 'createdAt', desc: true }]\n })\n if (!reader.all) queryBuilder.andWhere('completion.employee = :mine', { mine: reader.employee!.id })\n const [items, total] = await queryBuilder.getManyAndCount()\n return { items, total }\n }\n\n @Query(returns => TrainingStandingList, {\n description:\n '이수 현황 - where each person stands on each kind on in a year (DONE, WAITING, NOT_DONE): all to a holder, their own to an employee, whether done to a department head for their people (ADR-0187 decisions 17, 19)'\n })\n async trainingStandings(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<TrainingStandingList> {\n const year = Number(valuesOf(params, 'year')?.values[0] ?? thisYear(context))\n let rows = await readableStandings(year, context)\n for (const name of ['kind', 'state']) {\n const asked = valuesOf(params, name)\n if (asked) rows = rows.filter(row => asked.values.includes((row as any)[name]) !== asked.not)\n }\n const search = params.search ?? params.filters?.find(one => one.operator === 'search')?.value\n if (search) rows = rows.filter(row => `${row.employee.name ?? ''} ${row.departmentName ?? ''}`.includes(String(search).replace(/%/g, '').trim()))\n const { page = 1, limit = 0 } = params.pagination ?? ({} as any)\n return { items: (limit > 0 ? rows.slice((page - 1) * limit, page * limit) : rows) as any, total: rows.length }\n }\n\n @Query(returns => [MyTraining], { description: \"내 의무 교육 - the request's employee's trainings of a year, this year when none is given (ADR-0187 decision 10)\" })\n async myTrainings(@Arg('year', type => Int, { nullable: true }) year: number, @Ctx() context: ResolverContext): Promise<MyTraining[]> {\n return (await myTrainings(year ?? thisYear(context), context)) as any\n }\n\n @FieldResolver(type => [Attachment], { description: 'Its material - to a holder of the trainings and to the people it is for' })\n async attachments(@Root() row: TrainingSession, @Ctx() context: ResolverContext): Promise<Attachment[]> {\n return await filesOf(TRAINING_SESSION_REF_TYPE, row.id, context)\n }\n\n @FieldResolver(type => Course, { nullable: true, description: 'The course an ONLINE session is taken on' })\n async course(@Root() row: TrainingSession): Promise<Course | null> {\n return row.courseId ? await getRepository(Course).findOne({ where: { id: row.courseId } as any }) : null\n }\n\n @FieldResolver(type => User, { nullable: true })\n async creator(@Root() row: TrainingSession): Promise<User | null> {\n return row.creatorId ? await getRepository(User).findOne({ where: { id: row.creatorId } }) : null\n }\n}\n\n@Resolver(TrainingCompletion)\nexport class TrainingCompletionQuery {\n @FieldResolver(type => Employee, { nullable: true })\n async employee(@Root() row: TrainingCompletion): Promise<Employee | null> {\n return row.employeeId ? await getRepository(Employee).findOne({ where: { id: row.employeeId } }) : null\n }\n\n @FieldResolver(type => TrainingSession, { nullable: true })\n async session(@Root() row: TrainingCompletion): Promise<TrainingSession | null> {\n return row.sessionId ? await getRepository(TrainingSession).findOne({ where: { id: row.sessionId } }) : null\n }\n\n @FieldResolver(type => User, { nullable: true })\n async decidedBy(@Root() row: TrainingCompletion): Promise<User | null> {\n return row.decidedById ? await getRepository(User).findOne({ where: { id: row.decidedById } }) : null\n }\n\n @FieldResolver(type => [Attachment], { description: 'Its certificates - to a holder of the trainings and to the one it is of' })\n async attachments(@Root() row: TrainingCompletion, @Ctx() context: ResolverContext): Promise<Attachment[]> {\n return await filesOf(TRAINING_COMPLETION_REF_TYPE, row.id, context)\n }\n}\n\n/** A record's files the request may open - the module's attachment reader decides each. */\nasync function filesOf(refType: string, refBy: string, context: ResolverContext): Promise<Attachment[]> {\n const rows = await getRepository(Attachment).find({ where: { domain: { id: context.state.domain.id }, refType, refBy } as any, order: { createdAt: 'ASC' } as any })\n return await readableRows(rows, context)\n}\n"]}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerTrainings = registerTrainings;
|
|
4
4
|
const announcement_1 = require("@things-factory/announcement");
|
|
5
5
|
const attachment_base_1 = require("@things-factory/attachment-base");
|
|
6
|
+
const learning_1 = require("@things-factory/learning");
|
|
6
7
|
const shell_1 = require("@things-factory/shell");
|
|
7
8
|
const training_1 = require("./training");
|
|
8
9
|
const training_read_1 = require("./training-read");
|
|
@@ -23,5 +24,10 @@ function registerTrainings() {
|
|
|
23
24
|
return await (0, training_service_1.mayReadSession)(row, context);
|
|
24
25
|
});
|
|
25
26
|
(0, training_read_1.registerTrainingReads)();
|
|
27
|
+
/* An online session's enrolment completed or cancelled in the learning module (ADR-0194 decision 16). */
|
|
28
|
+
(0, learning_1.registerLearningOutcome)(training_service_1.TRAINING_SESSION_SOURCE, {
|
|
29
|
+
completed: async (enrollment, context) => void (await (0, training_service_1.recordOnlineCompletion)(enrollment, context)),
|
|
30
|
+
cancelled: async (enrollment, reason, context) => await (0, training_service_1.returnOnlineCompletion)(enrollment, reason, context)
|
|
31
|
+
});
|
|
26
32
|
}
|
|
27
33
|
//# sourceMappingURL=training-register.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"training-register.js","sourceRoot":"","sources":["../../../server/service/training/training-register.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"training-register.js","sourceRoot":"","sources":["../../../server/service/training/training-register.ts"],"names":[],"mappings":";;AAuBA,8CAgBC;AAvCD,+DAA0E;AAC1E,qEAA0E;AAC1E,uDAAkE;AAClE,iDAAqD;AAErD,yCAAgE;AAChE,mDAAuD;AACvD,yDAS2B;AAE3B;;;;GAIG;AACH,SAAgB,iBAAiB;IAC/B,IAAA,0CAA2B,EAAC,EAAE,IAAI,EAAE,6CAA0B,EAAE,WAAW,EAAE,+GAA+G,EAAE,CAAC,CAAA;IAC/L,IAAA,0CAAwB,EAAC,+CAA4B,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE;QACnF,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,6BAAkB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,EAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAC1K,OAAO,MAAM,IAAA,oCAAiB,EAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC,CAAC,CAAA;IACF,IAAA,0CAAwB,EAAC,4CAAyB,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE;QAChF,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,0BAAe,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,EAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACvK,OAAO,MAAM,IAAA,iCAAc,EAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IACF,IAAA,qCAAqB,GAAE,CAAA;IACvB,yGAAyG;IACzG,IAAA,kCAAuB,EAAC,0CAAuB,EAAE;QAC/C,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,IAAA,yCAAsB,EAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAClG,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,IAAA,yCAAsB,EAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC;KAC5G,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { registerAnnouncementTrigger } from '@things-factory/announcement'\nimport { registerAttachmentReader } from '@things-factory/attachment-base'\nimport { registerLearningOutcome } from '@things-factory/learning'\nimport { getRepository } from '@things-factory/shell'\n\nimport { TrainingCompletion, TrainingSession } from './training'\nimport { registerTrainingReads } from './training-read'\nimport {\n mayReadCompletion,\n mayReadSession,\n recordOnlineCompletion,\n returnOnlineCompletion,\n TRAINING_COMPLETION_REF_TYPE,\n TRAINING_SCHEDULED_TRIGGER,\n TRAINING_SESSION_REF_TYPE,\n TRAINING_SESSION_SOURCE\n} from './training-service'\n\n/*\n * What the trainings declare at load (ADR-0187 decisions 16, 17, 19): the announcement trigger of a session opened, who\n * opens their files - a certificate to a holder and the one it is of (a department head does not), a session's material\n * to a holder and the people it is for - and the assistant's read.\n */\nexport function registerTrainings(): void {\n registerAnnouncementTrigger({ name: TRAINING_SCHEDULED_TRIGGER, description: 'A mandatory training session opened - tells the people it is for when, how and by whom (ADR-0187 decision 16)' })\n registerAttachmentReader(TRAINING_COMPLETION_REF_TYPE, async (attachment, context) => {\n const row = attachment.refBy ? await getRepository(TrainingCompletion).findOne({ where: { id: attachment.refBy, domain: { id: context.state.domain.id } } as any }) : null\n return await mayReadCompletion(row, context)\n })\n registerAttachmentReader(TRAINING_SESSION_REF_TYPE, async (attachment, context) => {\n const row = attachment.refBy ? await getRepository(TrainingSession).findOne({ where: { id: attachment.refBy, domain: { id: context.state.domain.id } } as any }) : null\n return await mayReadSession(row, context)\n })\n registerTrainingReads()\n /* An online session's enrolment completed or cancelled in the learning module (ADR-0194 decision 16). */\n registerLearningOutcome(TRAINING_SESSION_SOURCE, {\n completed: async (enrollment, context) => void (await recordOnlineCompletion(enrollment, context)),\n cancelled: async (enrollment, reason, context) => await returnOnlineCompletion(enrollment, reason, context)\n })\n}\n"]}
|
|
@@ -43,9 +43,16 @@ export interface TrainingSessionFields {
|
|
|
43
43
|
method?: string | null;
|
|
44
44
|
startsOn?: string | null;
|
|
45
45
|
endsOn?: string | null;
|
|
46
|
+
courseId?: string | null;
|
|
46
47
|
}
|
|
47
48
|
/** What a session asks for before it is opened, or nothing - the kind must be on in the domain. */
|
|
48
49
|
export declare function trainingSessionRefusal(input: TrainingSessionFields, kind: TrainingKindSet | undefined): Refusal | null;
|
|
50
|
+
/** The day an online session's enrolments are due: its last day, else the kind's due day of its year. */
|
|
51
|
+
export declare function onlineDueOnOf(session: {
|
|
52
|
+
endsOn?: string | null;
|
|
53
|
+
startsOn: string;
|
|
54
|
+
year: number;
|
|
55
|
+
}, kindDueDay: string): string;
|
|
49
56
|
export interface TrainingCertificateFields {
|
|
50
57
|
kind?: string | null;
|
|
51
58
|
year?: number | null;
|
|
@@ -6,6 +6,7 @@ exports.trainingDaysLeft = trainingDaysLeft;
|
|
|
6
6
|
exports.trainingDueAt = trainingDueAt;
|
|
7
7
|
exports.trainingSettingRefusal = trainingSettingRefusal;
|
|
8
8
|
exports.trainingSessionRefusal = trainingSessionRefusal;
|
|
9
|
+
exports.onlineDueOnOf = onlineDueOnOf;
|
|
9
10
|
exports.trainingCertificateRefusal = trainingCertificateRefusal;
|
|
10
11
|
exports.trainingDecisionRefusal = trainingDecisionRefusal;
|
|
11
12
|
exports.trainingDeletionRefusal = trainingDeletionRefusal;
|
|
@@ -86,8 +87,15 @@ function trainingSessionRefusal(input, kind) {
|
|
|
86
87
|
return new shell_1.Refusal('training-starts-on-required', {}, 'give the day it is given, YYYY-MM-DD');
|
|
87
88
|
if (input.endsOn && (!DAY.test(input.endsOn) || input.endsOn < input.startsOn))
|
|
88
89
|
return new shell_1.Refusal('training-ends-before-start', {}, 'the last day is on or after the first, YYYY-MM-DD');
|
|
90
|
+
/* An online session is taken on a course of the learning module (ADR-0194 decision 15). */
|
|
91
|
+
if (input.method === 'ONLINE' && !input.courseId)
|
|
92
|
+
return new shell_1.Refusal('training-online-course-required', {}, 'pick the course an online session is taken on');
|
|
89
93
|
return null;
|
|
90
94
|
}
|
|
95
|
+
/** The day an online session's enrolments are due: its last day, else the kind's due day of its year. */
|
|
96
|
+
function onlineDueOnOf(session, kindDueDay) {
|
|
97
|
+
return session.endsOn || trainingDueOnOf(session.year, kindDueDay);
|
|
98
|
+
}
|
|
91
99
|
/**
|
|
92
100
|
* What a certificate a person sends asks for, or nothing: a kind on in the domain, the day completed within the year it
|
|
93
101
|
* counts for and not after today, a file, and no completion of it already done or waiting.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"training-rules.js","sourceRoot":"","sources":["../../../server/service/training/training-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import { Refusal } from '@things-factory/shell'\n\nimport { DEFAULT_DUE_DAY, TrainingKind, trainingKindOf, trainingKinds } from './training-kinds'\n\n/*\n * What the mandatory trainings hold to (ADR-0187 decisions 15-19). Pure - the service reads the rows and the people,\n * these say yes or no, and work out where each person stands.\n */\n\nconst DAY = /^\\d{4}-\\d{2}-\\d{2}$/\nconst DUE_DAY = /^(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/\nconst METHODS = ['COLLECTIVE', 'ONLINE', 'OUTSOURCED', 'POSTED']\nconst SCOPES = ['DOMAIN', 'DEPARTMENT', 'ROLE', 'USER']\n\nexport interface TrainingTarget {\n scope: string\n targetId: string\n targetName?: string | null\n}\n\nexport interface TrainingSettingRow {\n kind: string\n enabled?: boolean | null\n dueDay?: string | null\n audiences?: TrainingTarget[] | null\n memo?: string | null\n}\n\n/** A kind as a domain has it: the declaration with the domain's own word over it. */\nexport interface TrainingKindSet extends TrainingKind {\n enabled: boolean\n dueDay: string\n audiences: TrainingTarget[]\n memo?: string | null\n /** Whether the domain has said anything - a kind with no row reads as declared. */\n set: boolean\n}\n\nexport function trainingKindsInDomain(rows: readonly TrainingSettingRow[]): TrainingKindSet[] {\n return trainingKinds().map(kind => {\n const row = rows.find(one => one.kind === kind.code)\n return {\n ...kind,\n enabled: row ? !!row.enabled : kind.defaultOn,\n dueDay: row?.dueDay || DEFAULT_DUE_DAY,\n audiences: row?.audiences ?? [],\n memo: row?.memo ?? null,\n set: !!row\n }\n })\n}\n\n/** The day a kind is due in a year - Feb 29 falls to Feb 28 in a year that has none. */\nexport function trainingDueOnOf(year: number, dueDay: string): string {\n const [month, day] = (dueDay || DEFAULT_DUE_DAY).split('-').map(Number)\n const last = new Date(Date.UTC(year, month, 0)).getUTCDate()\n return `${year}-${String(month).padStart(2, '0')}-${String(Math.min(day, last)).padStart(2, '0')}`\n}\n\n/** Days from `today` to `day`, both YYYY-MM-DD - negative when it is past. */\nexport function trainingDaysLeft(today: string, day: string): number {\n return Math.round((Date.parse(`${day}T00:00:00Z`) - Date.parse(`${today}T00:00:00Z`)) / 86400000)\n}\n\n/** The instant a calendar day ends in a time zone - a to-do due on a day is due until its last second there. */\nexport function trainingDueAt(day: string, timezone?: string | null): Date {\n const utc = Date.parse(`${day}T23:59:59Z`)\n if (!timezone) return new Date(utc)\n try {\n const parts = Object.fromEntries(\n new Intl.DateTimeFormat('en-US', { timeZone: timezone, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })\n .formatToParts(new Date(utc))\n .map(part => [part.type, part.value])\n )\n const shown = Date.parse(`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}Z`)\n return new Date(utc - (shown - utc))\n } catch {\n return new Date(utc)\n }\n}\n\nexport interface TrainingSettingInput {\n kind?: string | null\n enabled?: boolean | null\n dueDay?: string | null\n audiences?: TrainingTarget[] | null\n}\n\n/** What a kind's setting asks for before it is written, or nothing. */\nexport function trainingSettingRefusal(input: TrainingSettingInput): Refusal | null {\n if (!trainingKindOf(input.kind)) return new Refusal('training-kind-unknown', { kind: input.kind ?? '' }, `there is no kind of training '${input.kind ?? ''}'`)\n if (input.dueDay && !DUE_DAY.test(input.dueDay)) return new Refusal('training-due-day-invalid', {}, 'give the due day of the year as MM-DD, 12-31')\n if ((input.audiences ?? []).some(one => !SCOPES.includes(one?.scope) || !one.targetId)) return new Refusal('training-audience-invalid', {}, 'a target is the domain, a department, a role or a person')\n return null\n}\n\nexport interface TrainingSessionFields {\n kind?: string | null\n year?: number | null\n name?: string | null\n method?: string | null\n startsOn?: string | null\n endsOn?: string | null\n}\n\n/** What a session asks for before it is opened, or nothing - the kind must be on in the domain. */\nexport function trainingSessionRefusal(input: TrainingSessionFields, kind: TrainingKindSet | undefined): Refusal | null {\n if (!kind) return new Refusal('training-kind-unknown', { kind: input.kind ?? '' }, `there is no kind of training '${input.kind ?? ''}'`)\n if (!kind.enabled) return new Refusal('training-kind-off', { kind: kind.code }, `the training ${kind.code} is off in this domain - turn it on first`)\n if (!Number.isInteger(input.year) || (input.year as number) < 2000 || (input.year as number) > 2999) return new Refusal('training-year-invalid', {}, 'give the year it counts for, 2000-2999')\n if (!input.name?.trim()) return new Refusal('training-session-name-required', {}, 'give the session a name')\n if (!METHODS.includes(input.method ?? '')) return new Refusal('training-method-required', {}, 'pick how it is given')\n if (!input.startsOn || !DAY.test(input.startsOn)) return new Refusal('training-starts-on-required', {}, 'give the day it is given, YYYY-MM-DD')\n if (input.endsOn && (!DAY.test(input.endsOn) || input.endsOn < input.startsOn)) return new Refusal('training-ends-before-start', {}, 'the last day is on or after the first, YYYY-MM-DD')\n return null\n}\n\nexport interface TrainingCertificateFields {\n kind?: string | null\n year?: number | null\n completedOn?: string | null\n method?: string | null\n}\n\n/**\n * What a certificate a person sends asks for, or nothing: a kind on in the domain, the day completed within the year it\n * counts for and not after today, a file, and no completion of it already done or waiting.\n */\nexport function trainingCertificateRefusal(\n input: TrainingCertificateFields,\n kind: TrainingKindSet | undefined,\n today: string,\n files: number,\n mine: readonly { status: string }[]\n): Refusal | null {\n if (!kind) return new Refusal('training-kind-unknown', { kind: input.kind ?? '' }, `there is no kind of training '${input.kind ?? ''}'`)\n if (!kind.enabled) return new Refusal('training-kind-off', { kind: kind.code }, `the training ${kind.code} is off in this domain`)\n if (!Number.isInteger(input.year)) return new Refusal('training-year-invalid', {}, 'give the year it counts for, 2000-2999')\n if (!input.completedOn || !DAY.test(input.completedOn)) return new Refusal('training-completed-on-required', {}, 'give the day it was completed, YYYY-MM-DD')\n if (input.completedOn > today) return new Refusal('training-completed-on-future', { today }, `the day completed is on or before today, ${today}`)\n if (Number(input.completedOn.slice(0, 4)) !== input.year) return new Refusal('training-completed-outside-year', { year: input.year }, `the day completed falls in ${input.year}, the year it counts for`)\n if (!METHODS.includes(input.method ?? '')) return new Refusal('training-method-required', {}, 'pick how it is given')\n if (files < 1) return new Refusal('training-certificate-required', {}, 'attach the certificate, one file at least')\n if (mine.some(one => one.status === 'CONFIRMED')) return new Refusal('training-already-done', {}, 'this training is completed for the year already')\n if (mine.some(one => one.status === 'SUBMITTED')) return new Refusal('training-already-submitted', {}, 'a certificate of it is waiting for the manager already')\n return null\n}\n\n/** Only a certificate sent and not decided is confirmed or returned. */\nexport function trainingDecisionRefusal(status: string, decision: 'CONFIRMED' | 'RETURNED', reason?: string | null): Refusal | null {\n if (status !== 'SUBMITTED') return new Refusal('training-not-submitted', {}, 'only a certificate waiting for the manager is confirmed or returned')\n if (decision === 'RETURNED' && !reason?.trim()) return new Refusal('training-return-reason-required', {}, 'say why it is returned')\n return null\n}\n\n/** Completions, sessions and their files are kept (decision 17) - the refusal says what to do instead. */\nexport function trainingDeletionRefusal(): Refusal {\n return new Refusal('training-record-kept', {}, 'training records are kept - return a certificate, or record a new completion, to correct one')\n}\n\n/** Where a person stands on a kind in a year. Only a confirmed completion is done; a certificate sent waits. */\nexport type TrainingState = 'DONE' | 'WAITING' | 'NOT_DONE'\n\nexport function trainingStateOf(completions: readonly { status: string }[]): TrainingState {\n if (completions.some(one => one.status === 'CONFIRMED')) return 'DONE'\n if (completions.some(one => one.status === 'SUBMITTED')) return 'WAITING'\n return 'NOT_DONE'\n}\n\n/** The share of the people who are done, 0-1; null when nobody must take it. */\nexport function trainingCompletionRate(states: readonly TrainingState[]): number | null {\n if (!states.length) return null\n return states.filter(one => one === 'DONE').length / states.length\n}\n"]}
|
|
1
|
+
{"version":3,"file":"training-rules.js","sourceRoot":"","sources":["../../../server/service/training/training-rules.ts"],"names":[],"mappings":";;AAsCA,sDAYC;AAGD,0CAIC;AAGD,4CAEC;AAGD,sCAcC;AAUD,wDAKC;AAaD,wDAWC;AAGD,sCAEC;AAaD,gEAkBC;AAGD,0DAIC;AAGD,0DAEC;AAKD,0CAIC;AAGD,wDAGC;AArLD,iDAA+C;AAE/C,qDAA+F;AAE/F;;;GAGG;AAEH,MAAM,GAAG,GAAG,qBAAqB,CAAA;AACjC,MAAM,OAAO,GAAG,yCAAyC,CAAA;AACzD,MAAM,OAAO,GAAG,CAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAA;AAChE,MAAM,MAAM,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AA0BvD,SAAgB,qBAAqB,CAAC,IAAmC;IACvE,OAAO,IAAA,8BAAa,GAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAA;QACpD,OAAO;YACL,GAAG,IAAI;YACP,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS;YAC7C,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI,gCAAe;YACtC,SAAS,EAAE,GAAG,EAAE,SAAS,IAAI,EAAE;YAC/B,IAAI,EAAE,GAAG,EAAE,IAAI,IAAI,IAAI;YACvB,GAAG,EAAE,CAAC,CAAC,GAAG;SACX,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,wFAAwF;AACxF,SAAgB,eAAe,CAAC,IAAY,EAAE,MAAc;IAC1D,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,gCAAe,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACvE,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;IAC5D,OAAO,GAAG,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;AACpG,CAAC;AAED,8EAA8E;AAC9E,SAAgB,gBAAgB,CAAC,KAAa,EAAE,GAAW;IACzD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAA;AACnG,CAAC;AAED,gHAAgH;AAChH,SAAgB,aAAa,CAAC,GAAW,EAAE,QAAwB;IACjE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,YAAY,CAAC,CAAA;IAC1C,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;IACnC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAC9B,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;aACjL,aAAa,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;aAC5B,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CACxC,CAAA;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;QACpH,OAAO,IAAI,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;IACtB,CAAC;AACH,CAAC;AASD,uEAAuE;AACvE,SAAgB,sBAAsB,CAAC,KAA2B;IAChE,IAAI,CAAC,IAAA,+BAAc,EAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,EAAE,iCAAiC,KAAK,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAA;IAC9J,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,0BAA0B,EAAE,EAAE,EAAE,8CAA8C,CAAC,CAAA;IACnJ,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,2BAA2B,EAAE,EAAE,EAAE,0DAA0D,CAAC,CAAA;IACvM,OAAO,IAAI,CAAA;AACb,CAAC;AAYD,mGAAmG;AACnG,SAAgB,sBAAsB,CAAC,KAA4B,EAAE,IAAiC;IACpG,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,eAAO,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,EAAE,iCAAiC,KAAK,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAA;IACxI,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,gBAAgB,IAAI,CAAC,IAAI,2CAA2C,CAAC,CAAA;IACrJ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,IAAK,KAAK,CAAC,IAAe,GAAG,IAAI,IAAK,KAAK,CAAC,IAAe,GAAG,IAAI;QAAE,OAAO,IAAI,eAAO,CAAC,uBAAuB,EAAE,EAAE,EAAE,wCAAwC,CAAC,CAAA;IAC9L,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;QAAE,OAAO,IAAI,eAAO,CAAC,gCAAgC,EAAE,EAAE,EAAE,yBAAyB,CAAC,CAAA;IAC5G,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,0BAA0B,EAAE,EAAE,EAAE,sBAAsB,CAAC,CAAA;IACrH,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,6BAA6B,EAAE,EAAE,EAAE,sCAAsC,CAAC,CAAA;IAC/I,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,4BAA4B,EAAE,EAAE,EAAE,mDAAmD,CAAC,CAAA;IACzL,2FAA2F;IAC3F,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,QAAQ;QAAE,OAAO,IAAI,eAAO,CAAC,iCAAiC,EAAE,EAAE,EAAE,+CAA+C,CAAC,CAAA;IAC5J,OAAO,IAAI,CAAA;AACb,CAAC;AAED,yGAAyG;AACzG,SAAgB,aAAa,CAAC,OAAmE,EAAE,UAAkB;IACnH,OAAO,OAAO,CAAC,MAAM,IAAI,eAAe,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;AACpE,CAAC;AASD;;;GAGG;AACH,SAAgB,0BAA0B,CACxC,KAAgC,EAChC,IAAiC,EACjC,KAAa,EACb,KAAa,EACb,IAAmC;IAEnC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,eAAO,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,EAAE,iCAAiC,KAAK,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAA;IACxI,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,eAAO,CAAC,mBAAmB,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,gBAAgB,IAAI,CAAC,IAAI,wBAAwB,CAAC,CAAA;IAClI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,uBAAuB,EAAE,EAAE,EAAE,wCAAwC,CAAC,CAAA;IAC5H,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,gCAAgC,EAAE,EAAE,EAAE,2CAA2C,CAAC,CAAA;IAC7J,IAAI,KAAK,CAAC,WAAW,GAAG,KAAK;QAAE,OAAO,IAAI,eAAO,CAAC,8BAA8B,EAAE,EAAE,KAAK,EAAE,EAAE,4CAA4C,KAAK,EAAE,CAAC,CAAA;IACjJ,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI;QAAE,OAAO,IAAI,eAAO,CAAC,iCAAiC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,8BAA8B,KAAK,CAAC,IAAI,0BAA0B,CAAC,CAAA;IACzM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,0BAA0B,EAAE,EAAE,EAAE,sBAAsB,CAAC,CAAA;IACrH,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,+BAA+B,EAAE,EAAE,EAAE,2CAA2C,CAAC,CAAA;IACnH,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,WAAW,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,uBAAuB,EAAE,EAAE,EAAE,iDAAiD,CAAC,CAAA;IACpJ,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,WAAW,CAAC;QAAE,OAAO,IAAI,eAAO,CAAC,4BAA4B,EAAE,EAAE,EAAE,wDAAwD,CAAC,CAAA;IAChK,OAAO,IAAI,CAAA;AACb,CAAC;AAED,wEAAwE;AACxE,SAAgB,uBAAuB,CAAC,MAAc,EAAE,QAAkC,EAAE,MAAsB;IAChH,IAAI,MAAM,KAAK,WAAW;QAAE,OAAO,IAAI,eAAO,CAAC,wBAAwB,EAAE,EAAE,EAAE,qEAAqE,CAAC,CAAA;IACnJ,IAAI,QAAQ,KAAK,UAAU,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE;QAAE,OAAO,IAAI,eAAO,CAAC,iCAAiC,EAAE,EAAE,EAAE,wBAAwB,CAAC,CAAA;IACnI,OAAO,IAAI,CAAA;AACb,CAAC;AAED,0GAA0G;AAC1G,SAAgB,uBAAuB;IACrC,OAAO,IAAI,eAAO,CAAC,sBAAsB,EAAE,EAAE,EAAE,8FAA8F,CAAC,CAAA;AAChJ,CAAC;AAKD,SAAgB,eAAe,CAAC,WAA0C;IACxE,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,WAAW,CAAC;QAAE,OAAO,MAAM,CAAA;IACtE,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,WAAW,CAAC;QAAE,OAAO,SAAS,CAAA;IACzE,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,gFAAgF;AAChF,SAAgB,sBAAsB,CAAC,MAAgC;IACrE,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAC/B,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;AACpE,CAAC","sourcesContent":["import { Refusal } from '@things-factory/shell'\n\nimport { DEFAULT_DUE_DAY, TrainingKind, trainingKindOf, trainingKinds } from './training-kinds'\n\n/*\n * What the mandatory trainings hold to (ADR-0187 decisions 15-19). Pure - the service reads the rows and the people,\n * these say yes or no, and work out where each person stands.\n */\n\nconst DAY = /^\\d{4}-\\d{2}-\\d{2}$/\nconst DUE_DAY = /^(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/\nconst METHODS = ['COLLECTIVE', 'ONLINE', 'OUTSOURCED', 'POSTED']\nconst SCOPES = ['DOMAIN', 'DEPARTMENT', 'ROLE', 'USER']\n\nexport interface TrainingTarget {\n scope: string\n targetId: string\n targetName?: string | null\n}\n\nexport interface TrainingSettingRow {\n kind: string\n enabled?: boolean | null\n dueDay?: string | null\n audiences?: TrainingTarget[] | null\n memo?: string | null\n}\n\n/** A kind as a domain has it: the declaration with the domain's own word over it. */\nexport interface TrainingKindSet extends TrainingKind {\n enabled: boolean\n dueDay: string\n audiences: TrainingTarget[]\n memo?: string | null\n /** Whether the domain has said anything - a kind with no row reads as declared. */\n set: boolean\n}\n\nexport function trainingKindsInDomain(rows: readonly TrainingSettingRow[]): TrainingKindSet[] {\n return trainingKinds().map(kind => {\n const row = rows.find(one => one.kind === kind.code)\n return {\n ...kind,\n enabled: row ? !!row.enabled : kind.defaultOn,\n dueDay: row?.dueDay || DEFAULT_DUE_DAY,\n audiences: row?.audiences ?? [],\n memo: row?.memo ?? null,\n set: !!row\n }\n })\n}\n\n/** The day a kind is due in a year - Feb 29 falls to Feb 28 in a year that has none. */\nexport function trainingDueOnOf(year: number, dueDay: string): string {\n const [month, day] = (dueDay || DEFAULT_DUE_DAY).split('-').map(Number)\n const last = new Date(Date.UTC(year, month, 0)).getUTCDate()\n return `${year}-${String(month).padStart(2, '0')}-${String(Math.min(day, last)).padStart(2, '0')}`\n}\n\n/** Days from `today` to `day`, both YYYY-MM-DD - negative when it is past. */\nexport function trainingDaysLeft(today: string, day: string): number {\n return Math.round((Date.parse(`${day}T00:00:00Z`) - Date.parse(`${today}T00:00:00Z`)) / 86400000)\n}\n\n/** The instant a calendar day ends in a time zone - a to-do due on a day is due until its last second there. */\nexport function trainingDueAt(day: string, timezone?: string | null): Date {\n const utc = Date.parse(`${day}T23:59:59Z`)\n if (!timezone) return new Date(utc)\n try {\n const parts = Object.fromEntries(\n new Intl.DateTimeFormat('en-US', { timeZone: timezone, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })\n .formatToParts(new Date(utc))\n .map(part => [part.type, part.value])\n )\n const shown = Date.parse(`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}Z`)\n return new Date(utc - (shown - utc))\n } catch {\n return new Date(utc)\n }\n}\n\nexport interface TrainingSettingInput {\n kind?: string | null\n enabled?: boolean | null\n dueDay?: string | null\n audiences?: TrainingTarget[] | null\n}\n\n/** What a kind's setting asks for before it is written, or nothing. */\nexport function trainingSettingRefusal(input: TrainingSettingInput): Refusal | null {\n if (!trainingKindOf(input.kind)) return new Refusal('training-kind-unknown', { kind: input.kind ?? '' }, `there is no kind of training '${input.kind ?? ''}'`)\n if (input.dueDay && !DUE_DAY.test(input.dueDay)) return new Refusal('training-due-day-invalid', {}, 'give the due day of the year as MM-DD, 12-31')\n if ((input.audiences ?? []).some(one => !SCOPES.includes(one?.scope) || !one.targetId)) return new Refusal('training-audience-invalid', {}, 'a target is the domain, a department, a role or a person')\n return null\n}\n\nexport interface TrainingSessionFields {\n kind?: string | null\n year?: number | null\n name?: string | null\n method?: string | null\n startsOn?: string | null\n endsOn?: string | null\n courseId?: string | null\n}\n\n/** What a session asks for before it is opened, or nothing - the kind must be on in the domain. */\nexport function trainingSessionRefusal(input: TrainingSessionFields, kind: TrainingKindSet | undefined): Refusal | null {\n if (!kind) return new Refusal('training-kind-unknown', { kind: input.kind ?? '' }, `there is no kind of training '${input.kind ?? ''}'`)\n if (!kind.enabled) return new Refusal('training-kind-off', { kind: kind.code }, `the training ${kind.code} is off in this domain - turn it on first`)\n if (!Number.isInteger(input.year) || (input.year as number) < 2000 || (input.year as number) > 2999) return new Refusal('training-year-invalid', {}, 'give the year it counts for, 2000-2999')\n if (!input.name?.trim()) return new Refusal('training-session-name-required', {}, 'give the session a name')\n if (!METHODS.includes(input.method ?? '')) return new Refusal('training-method-required', {}, 'pick how it is given')\n if (!input.startsOn || !DAY.test(input.startsOn)) return new Refusal('training-starts-on-required', {}, 'give the day it is given, YYYY-MM-DD')\n if (input.endsOn && (!DAY.test(input.endsOn) || input.endsOn < input.startsOn)) return new Refusal('training-ends-before-start', {}, 'the last day is on or after the first, YYYY-MM-DD')\n /* An online session is taken on a course of the learning module (ADR-0194 decision 15). */\n if (input.method === 'ONLINE' && !input.courseId) return new Refusal('training-online-course-required', {}, 'pick the course an online session is taken on')\n return null\n}\n\n/** The day an online session's enrolments are due: its last day, else the kind's due day of its year. */\nexport function onlineDueOnOf(session: { endsOn?: string | null; startsOn: string; year: number }, kindDueDay: string): string {\n return session.endsOn || trainingDueOnOf(session.year, kindDueDay)\n}\n\nexport interface TrainingCertificateFields {\n kind?: string | null\n year?: number | null\n completedOn?: string | null\n method?: string | null\n}\n\n/**\n * What a certificate a person sends asks for, or nothing: a kind on in the domain, the day completed within the year it\n * counts for and not after today, a file, and no completion of it already done or waiting.\n */\nexport function trainingCertificateRefusal(\n input: TrainingCertificateFields,\n kind: TrainingKindSet | undefined,\n today: string,\n files: number,\n mine: readonly { status: string }[]\n): Refusal | null {\n if (!kind) return new Refusal('training-kind-unknown', { kind: input.kind ?? '' }, `there is no kind of training '${input.kind ?? ''}'`)\n if (!kind.enabled) return new Refusal('training-kind-off', { kind: kind.code }, `the training ${kind.code} is off in this domain`)\n if (!Number.isInteger(input.year)) return new Refusal('training-year-invalid', {}, 'give the year it counts for, 2000-2999')\n if (!input.completedOn || !DAY.test(input.completedOn)) return new Refusal('training-completed-on-required', {}, 'give the day it was completed, YYYY-MM-DD')\n if (input.completedOn > today) return new Refusal('training-completed-on-future', { today }, `the day completed is on or before today, ${today}`)\n if (Number(input.completedOn.slice(0, 4)) !== input.year) return new Refusal('training-completed-outside-year', { year: input.year }, `the day completed falls in ${input.year}, the year it counts for`)\n if (!METHODS.includes(input.method ?? '')) return new Refusal('training-method-required', {}, 'pick how it is given')\n if (files < 1) return new Refusal('training-certificate-required', {}, 'attach the certificate, one file at least')\n if (mine.some(one => one.status === 'CONFIRMED')) return new Refusal('training-already-done', {}, 'this training is completed for the year already')\n if (mine.some(one => one.status === 'SUBMITTED')) return new Refusal('training-already-submitted', {}, 'a certificate of it is waiting for the manager already')\n return null\n}\n\n/** Only a certificate sent and not decided is confirmed or returned. */\nexport function trainingDecisionRefusal(status: string, decision: 'CONFIRMED' | 'RETURNED', reason?: string | null): Refusal | null {\n if (status !== 'SUBMITTED') return new Refusal('training-not-submitted', {}, 'only a certificate waiting for the manager is confirmed or returned')\n if (decision === 'RETURNED' && !reason?.trim()) return new Refusal('training-return-reason-required', {}, 'say why it is returned')\n return null\n}\n\n/** Completions, sessions and their files are kept (decision 17) - the refusal says what to do instead. */\nexport function trainingDeletionRefusal(): Refusal {\n return new Refusal('training-record-kept', {}, 'training records are kept - return a certificate, or record a new completion, to correct one')\n}\n\n/** Where a person stands on a kind in a year. Only a confirmed completion is done; a certificate sent waits. */\nexport type TrainingState = 'DONE' | 'WAITING' | 'NOT_DONE'\n\nexport function trainingStateOf(completions: readonly { status: string }[]): TrainingState {\n if (completions.some(one => one.status === 'CONFIRMED')) return 'DONE'\n if (completions.some(one => one.status === 'SUBMITTED')) return 'WAITING'\n return 'NOT_DONE'\n}\n\n/** The share of the people who are done, 0-1; null when nobody must take it. */\nexport function trainingCompletionRate(states: readonly TrainingState[]): number | null {\n if (!states.length) return null\n return states.filter(one => one === 'DONE').length / states.length\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EntityManager } from 'typeorm';
|
|
2
2
|
import { Employee } from '@things-factory/organization';
|
|
3
|
+
import { Enrollment } from '@things-factory/learning';
|
|
3
4
|
import { TrainingCompletion, TrainingSession, TrainingSetting } from './training';
|
|
4
5
|
import { TrainingTarget, TrainingKindSet, TrainingState } from './training-rules';
|
|
5
6
|
import { NewTrainingSession, TrainingCertificateInput, TrainingSessionPatch, TrainingSettingPatch } from './training-type';
|
|
@@ -8,6 +9,8 @@ export declare const TRAINING_COMPLETION_REF_TYPE = "TrainingCompletion";
|
|
|
8
9
|
export declare const TRAINING_SCHEDULED_TRIGGER = "compliance.training-scheduled";
|
|
9
10
|
/** The employee's screen of their trainings - where a to-do and an announcement lead. */
|
|
10
11
|
export declare const MY_TRAININGS_ADDRESS = "regulations?tab=trainings";
|
|
12
|
+
/** The source an online session's enrolments are asked under (ADR-0194 decision 15) - `<source>:<session id>`. */
|
|
13
|
+
export declare const TRAINING_SESSION_SOURCE = "compliance.training-session";
|
|
11
14
|
/** The request's transaction, or the data source's manager for a query that runs without one. */
|
|
12
15
|
export declare function trainingManagerOf(context: any): EntityManager;
|
|
13
16
|
/** The kinds as the domain has them. */
|
|
@@ -63,6 +66,14 @@ export interface TrainingYearOpening {
|
|
|
63
66
|
* to-do the domain could not hand out before).
|
|
64
67
|
*/
|
|
65
68
|
export declare function openTrainingYear(kindCode: string, year: number, context: any): Promise<TrainingYearOpening>;
|
|
69
|
+
/**
|
|
70
|
+
* An enrolment of an online session completed: a confirmed completion of the session's kind and year, with the
|
|
71
|
+
* learning module's evidence - no manager's confirmation needed. Runs in the completion's transaction: what fails here
|
|
72
|
+
* takes the completion back. Someone done already for the kind and year is left as they are.
|
|
73
|
+
*/
|
|
74
|
+
export declare function recordOnlineCompletion(enrollment: Enrollment, context: any): Promise<TrainingCompletion | null>;
|
|
75
|
+
/** A completed enrolment a manager cancelled: the completion made from it is returned with the reason - kept, not deleted. */
|
|
76
|
+
export declare function returnOnlineCompletion(enrollment: Enrollment, reason: string, context: any): Promise<void>;
|
|
66
77
|
export interface TrainingStandingRow {
|
|
67
78
|
id: string;
|
|
68
79
|
kind: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MY_TRAININGS_ADDRESS = exports.TRAINING_SCHEDULED_TRIGGER = exports.TRAINING_COMPLETION_REF_TYPE = exports.TRAINING_SESSION_REF_TYPE = void 0;
|
|
3
|
+
exports.TRAINING_SESSION_SOURCE = exports.MY_TRAININGS_ADDRESS = exports.TRAINING_SCHEDULED_TRIGGER = exports.TRAINING_COMPLETION_REF_TYPE = exports.TRAINING_SESSION_REF_TYPE = void 0;
|
|
4
4
|
exports.trainingManagerOf = trainingManagerOf;
|
|
5
5
|
exports.trainingKindsOf = trainingKindsOf;
|
|
6
6
|
exports.trainingAudiencesOf = trainingAudiencesOf;
|
|
@@ -15,6 +15,8 @@ exports.confirmTrainingCompletions = confirmTrainingCompletions;
|
|
|
15
15
|
exports.returnTrainingCompletion = returnTrainingCompletion;
|
|
16
16
|
exports.deleteTrainingRecords = deleteTrainingRecords;
|
|
17
17
|
exports.openTrainingYear = openTrainingYear;
|
|
18
|
+
exports.recordOnlineCompletion = recordOnlineCompletion;
|
|
19
|
+
exports.returnOnlineCompletion = returnOnlineCompletion;
|
|
18
20
|
exports.standingsOf = standingsOf;
|
|
19
21
|
exports.readableStandings = readableStandings;
|
|
20
22
|
exports.myTrainings = myTrainings;
|
|
@@ -26,6 +28,7 @@ const attachment_base_1 = require("@things-factory/attachment-base");
|
|
|
26
28
|
const translator_1 = require("@things-factory/i18n-base/translator");
|
|
27
29
|
const organization_1 = require("@things-factory/organization");
|
|
28
30
|
const employee_profile_rules_js_1 = require("@things-factory/organization/dist-server/service/employee-profile/employee-profile-rules.js");
|
|
31
|
+
const learning_1 = require("@things-factory/learning");
|
|
29
32
|
const shell_1 = require("@things-factory/shell");
|
|
30
33
|
const employee_reader_1 = require("../common/employee-reader");
|
|
31
34
|
const legal_todo_1 = require("../common/legal-todo");
|
|
@@ -41,6 +44,8 @@ exports.TRAINING_COMPLETION_REF_TYPE = 'TrainingCompletion';
|
|
|
41
44
|
exports.TRAINING_SCHEDULED_TRIGGER = 'compliance.training-scheduled';
|
|
42
45
|
/** The employee's screen of their trainings - where a to-do and an announcement lead. */
|
|
43
46
|
exports.MY_TRAININGS_ADDRESS = 'regulations?tab=trainings';
|
|
47
|
+
/** The source an online session's enrolments are asked under (ADR-0194 decision 15) - `<source>:<session id>`. */
|
|
48
|
+
exports.TRAINING_SESSION_SOURCE = 'compliance.training-session';
|
|
44
49
|
const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
45
50
|
const kindWord = (t, code) => t(`text.training-kind.${code}`);
|
|
46
51
|
/* ---------------------------------------------------------------- kinds and people */
|
|
@@ -149,12 +154,15 @@ async function openTrainingSession(input, attach, context) {
|
|
|
149
154
|
instructor: text(input.instructor),
|
|
150
155
|
organisation: text(input.organisation),
|
|
151
156
|
audiences: audienceRows(input.audiences),
|
|
157
|
+
course: input.method === training_1.TrainingMethod.ONLINE && input.courseId ? { id: input.courseId } : null,
|
|
152
158
|
notes: text(input.notes),
|
|
153
159
|
creator: user,
|
|
154
160
|
updater: user
|
|
155
161
|
});
|
|
156
162
|
for (const file of attach ?? [])
|
|
157
163
|
await (0, attachment_base_1.createAttachment)(null, { attachment: { file, refType: exports.TRAINING_SESSION_REF_TYPE, refBy: saved.id } }, context);
|
|
164
|
+
if (saved.method === training_1.TrainingMethod.ONLINE)
|
|
165
|
+
await enrollOnlineSession(saved, kind, context);
|
|
158
166
|
const t = context.t ?? ((key) => key);
|
|
159
167
|
const when = saved.endsOn && saved.endsOn !== saved.startsOn ? `${saved.startsOn} ~ ${saved.endsOn}` : saved.startsOn;
|
|
160
168
|
await (0, announcement_1.announce)(context, {
|
|
@@ -173,6 +181,37 @@ async function openTrainingSession(input, attach, context) {
|
|
|
173
181
|
});
|
|
174
182
|
return saved;
|
|
175
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Opens an online session to its audience (ADR-0194 decision 15): each person still working here is enrolled in the
|
|
186
|
+
* session's course, due by its last day (the kind's due day when it has none). The session's to-do is the enrolment's -
|
|
187
|
+
* so the year's to-do a person was handed for the kind is closed, to leave one.
|
|
188
|
+
*/
|
|
189
|
+
async function enrollOnlineSession(session, kind, context) {
|
|
190
|
+
const { domain, user, tx } = context.state;
|
|
191
|
+
const people = (await trainingPeopleOf(tx, domain, trainingAudiencesOf(kind, domain, session.audiences), (0, employee_reader_1.todayIn)(domain))).filter(one => one.user);
|
|
192
|
+
const courseId = session.course?.id ?? session.courseId;
|
|
193
|
+
await (0, learning_1.enroll)({ kind: exports.TRAINING_SESSION_SOURCE, id: session.id }, people.map(one => one.user.id), courseId, (0, training_rules_1.onlineDueOnOf)(session, kind.dueDay), context);
|
|
194
|
+
const repository = tx.getRepository(training_1.TrainingAssignment);
|
|
195
|
+
for (const person of people) {
|
|
196
|
+
const assignment = await repository.findOne({ where: { domain: { id: domain.id }, employee: { id: person.id }, kind: session.kind, year: session.year } });
|
|
197
|
+
if (!assignment?.todoInstanceId || assignment.closedAt)
|
|
198
|
+
continue;
|
|
199
|
+
await (0, legal_todo_1.closeLegalTodo)(tx, domain, assignment.todoInstanceId, user, '온라인 회차 수강');
|
|
200
|
+
assignment.todoInstanceId = null;
|
|
201
|
+
assignment.updater = user;
|
|
202
|
+
await repository.save(assignment);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** The people of a kind and year who hold an open enrolment of an online session - their to-do is that enrolment's. */
|
|
206
|
+
async function onlineLearnersOf(tx, domain, kind, year) {
|
|
207
|
+
const sessions = await tx.getRepository(training_1.TrainingSession).find({ where: { domain: { id: domain.id }, kind, year, method: training_1.TrainingMethod.ONLINE } });
|
|
208
|
+
if (!sessions.length)
|
|
209
|
+
return new Set();
|
|
210
|
+
const open = await tx.getRepository(learning_1.Enrollment).find({
|
|
211
|
+
where: { domain: { id: domain.id }, sourceKind: exports.TRAINING_SESSION_SOURCE, sourceId: (0, typeorm_1.In)(sessions.map(one => one.id)), status: (0, typeorm_1.In)([learning_1.EnrollmentStatus.ASSIGNED, learning_1.EnrollmentStatus.IN_PROGRESS]) }
|
|
212
|
+
});
|
|
213
|
+
return new Set(open.map(one => one.userId));
|
|
214
|
+
}
|
|
176
215
|
/** What a session says may be corrected - its kind and year stay, the completions of it count for them. */
|
|
177
216
|
async function updateTrainingSessions(patches, context) {
|
|
178
217
|
const { domain, user, tx } = context.state;
|
|
@@ -183,11 +222,14 @@ async function updateTrainingSessions(patches, context) {
|
|
|
183
222
|
const row = await repository.findOne({ where: { id: patch.id, domain: { id: domain.id } } });
|
|
184
223
|
if (!row)
|
|
185
224
|
throw new shell_1.Refusal('training-not-found', {}, 'the record was not found. reload and try again.');
|
|
225
|
+
/* An online session's people are enrolled in its course - it stays online (ADR-0194 decision 15). */
|
|
226
|
+
if (patch.method !== undefined && patch.method !== row.method && (row.method === training_1.TrainingMethod.ONLINE || patch.method === training_1.TrainingMethod.ONLINE))
|
|
227
|
+
throw new shell_1.Refusal('training-online-method-fixed', {}, 'an online session stays online, and another stays as it is - open a new session instead');
|
|
186
228
|
for (const field of ['name', 'method', 'startsOn', 'endsOn', 'instructor', 'organisation', 'notes'])
|
|
187
229
|
if (patch[field] !== undefined)
|
|
188
230
|
row[field] = field === 'name' || field === 'method' || field === 'startsOn' ? (patch[field] ?? '').trim() : text(patch[field]);
|
|
189
231
|
/* An earlier session of a kind turned off since is still corrected - only what it says is asked. */
|
|
190
|
-
const refusal = (0, training_rules_1.trainingSessionRefusal)(row, { ...kinds.find(one => one.code === row.kind), enabled: true });
|
|
232
|
+
const refusal = (0, training_rules_1.trainingSessionRefusal)({ ...row, courseId: row.courseId }, { ...kinds.find(one => one.code === row.kind), enabled: true });
|
|
191
233
|
if (refusal)
|
|
192
234
|
throw refusal;
|
|
193
235
|
row.updater = user;
|
|
@@ -333,9 +375,11 @@ async function openTrainingYear(kindCode, year, context) {
|
|
|
333
375
|
const people = await trainingPeopleOf(tx, domain, trainingAudiencesOf(kind, domain), today);
|
|
334
376
|
const repository = tx.getRepository(training_1.TrainingAssignment);
|
|
335
377
|
const done = new Set((await tx.getRepository(training_1.TrainingCompletion).find({ where: { domain: { id: domain.id }, kind: kindCode, year, status: training_1.TrainingCompletionStatus.CONFIRMED } })).map(one => one.employeeId));
|
|
378
|
+
/* Someone enrolled in an online session of the kind has that enrolment's to-do - none twice. */
|
|
379
|
+
const online = await onlineLearnersOf(tx, domain, kindCode, year);
|
|
336
380
|
let handedOut = 0;
|
|
337
381
|
for (const person of people) {
|
|
338
|
-
if (done.has(person.id) || !person.user)
|
|
382
|
+
if (done.has(person.id) || !person.user || online.has(person.user.id))
|
|
339
383
|
continue;
|
|
340
384
|
const assignment = (await repository.findOne({ where: { domain: { id: domain.id }, employee: { id: person.id }, kind: kindCode, year } })) ??
|
|
341
385
|
(await repository.save({ domain, employee: person, kind: kindCode, year, dueOn, creator: user, updater: user }));
|
|
@@ -360,6 +404,63 @@ async function openTrainingYear(kindCode, year, context) {
|
|
|
360
404
|
}
|
|
361
405
|
return { people: people.length, done: people.filter(one => done.has(one.id)).length, handedOut };
|
|
362
406
|
}
|
|
407
|
+
/* ---------------------------------------------------------------- online completions (ADR-0194 decision 16) */
|
|
408
|
+
const dayIn = (at, timezone) => new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at));
|
|
409
|
+
/**
|
|
410
|
+
* An enrolment of an online session completed: a confirmed completion of the session's kind and year, with the
|
|
411
|
+
* learning module's evidence - no manager's confirmation needed. Runs in the completion's transaction: what fails here
|
|
412
|
+
* takes the completion back. Someone done already for the kind and year is left as they are.
|
|
413
|
+
*/
|
|
414
|
+
async function recordOnlineCompletion(enrollment, context) {
|
|
415
|
+
const { domain, tx } = context.state;
|
|
416
|
+
const session = await tx.getRepository(training_1.TrainingSession).findOne({ where: { id: enrollment.sourceId, domain: { id: domain.id } } });
|
|
417
|
+
if (!session)
|
|
418
|
+
throw new shell_1.Refusal('training-not-found', {}, 'the session of this enrolment was not found');
|
|
419
|
+
const employee = await tx.getRepository(organization_1.Employee).findOne({ where: { domain: { id: domain.id }, user: { id: enrollment.userId } } });
|
|
420
|
+
if (!employee)
|
|
421
|
+
throw new shell_1.Refusal('training-employee-unknown', {}, 'the learner must be an employee working in this domain');
|
|
422
|
+
if ((0, training_rules_1.trainingStateOf)(await completionsOf(tx, domain, employee.id, session.kind, session.year)) === 'DONE')
|
|
423
|
+
return null;
|
|
424
|
+
const version = enrollment.versionId ? await tx.getRepository(learning_1.CourseVersion).findOne({ where: { id: enrollment.versionId }, withDeleted: true }) : null;
|
|
425
|
+
const saved = await tx.getRepository(training_1.TrainingCompletion).save({
|
|
426
|
+
domain,
|
|
427
|
+
employee,
|
|
428
|
+
kind: session.kind,
|
|
429
|
+
year: session.year,
|
|
430
|
+
session,
|
|
431
|
+
completedOn: dayIn(enrollment.completedAt ?? new Date(), domain.timezone),
|
|
432
|
+
method: training_1.TrainingMethod.ONLINE,
|
|
433
|
+
status: training_1.TrainingCompletionStatus.CONFIRMED,
|
|
434
|
+
decidedAt: new Date(),
|
|
435
|
+
learningEnrollmentId: enrollment.id,
|
|
436
|
+
learningEvidence: {
|
|
437
|
+
minutes: Math.floor((enrollment.learnedSeconds ?? 0) / 60),
|
|
438
|
+
score: enrollment.score ?? null,
|
|
439
|
+
attempts: enrollment.attempts ?? 0,
|
|
440
|
+
acknowledgedAt: enrollment.acknowledgedAt ?? null,
|
|
441
|
+
version: version?.number ?? null,
|
|
442
|
+
certificateNo: enrollment.certificateNo ?? null
|
|
443
|
+
},
|
|
444
|
+
creator: context.state.user,
|
|
445
|
+
updater: context.state.user
|
|
446
|
+
});
|
|
447
|
+
await closeTodoOf(tx, domain, employee.id, session.kind, session.year, context.state.user);
|
|
448
|
+
return saved;
|
|
449
|
+
}
|
|
450
|
+
/** A completed enrolment a manager cancelled: the completion made from it is returned with the reason - kept, not deleted. */
|
|
451
|
+
async function returnOnlineCompletion(enrollment, reason, context) {
|
|
452
|
+
const { domain, user, tx } = context.state;
|
|
453
|
+
const repository = tx.getRepository(training_1.TrainingCompletion);
|
|
454
|
+
const row = await repository.findOne({ where: { domain: { id: domain.id }, learningEnrollmentId: enrollment.id } });
|
|
455
|
+
if (!row || row.status !== training_1.TrainingCompletionStatus.CONFIRMED)
|
|
456
|
+
return;
|
|
457
|
+
row.status = training_1.TrainingCompletionStatus.RETURNED;
|
|
458
|
+
row.returnReason = reason;
|
|
459
|
+
row.decidedAt = new Date();
|
|
460
|
+
row.decidedBy = user;
|
|
461
|
+
row.updater = user;
|
|
462
|
+
await repository.save(row);
|
|
463
|
+
}
|
|
363
464
|
/** Where everyone of every kind on stands in a year - the people of each kind's audience, by name. */
|
|
364
465
|
async function standingsOf(tx, domain, year, today) {
|
|
365
466
|
const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled);
|