@things-factory/compliance 10.1.259 → 10.1.261

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 (70) hide show
  1. package/dist-client/components/my-trainings.js +6 -2
  2. package/dist-client/components/my-trainings.js.map +1 -1
  3. package/dist-client/components/training-session-form.d.ts +3 -0
  4. package/dist-client/components/training-session-form.js +24 -1
  5. package/dist-client/components/training-session-form.js.map +1 -1
  6. package/dist-client/legal-record/legal-child-grid.d.ts +12 -2
  7. package/dist-client/legal-record/legal-child-grid.js +22 -2
  8. package/dist-client/legal-record/legal-child-grid.js.map +1 -1
  9. package/dist-client/legal-record/legal-grid.d.ts +1 -1
  10. package/dist-client/legal-record/legal-grid.js +4 -2
  11. package/dist-client/legal-record/legal-grid.js.map +1 -1
  12. package/dist-client/pages/compliance-case-list.js +1 -1
  13. package/dist-client/pages/compliance-case-list.js.map +1 -1
  14. package/dist-client/pages/dispute-list.js +5 -3
  15. package/dist-client/pages/dispute-list.js.map +1 -1
  16. package/dist-client/pages/invention-list.js +4 -3
  17. package/dist-client/pages/invention-list.js.map +1 -1
  18. package/dist-client/pages/legal-registration-list.js +6 -4
  19. package/dist-client/pages/legal-registration-list.js.map +1 -1
  20. package/dist-client/pages/privacy-record-list.d.ts +1 -1
  21. package/dist-client/pages/privacy-record-list.js +16 -6
  22. package/dist-client/pages/privacy-record-list.js.map +1 -1
  23. package/dist-client/pages/regulation-list.js +2 -2
  24. package/dist-client/pages/regulation-list.js.map +1 -1
  25. package/dist-client/pages/seal-use-list.d.ts +7 -0
  26. package/dist-client/pages/seal-use-list.js +4 -4
  27. package/dist-client/pages/seal-use-list.js.map +1 -1
  28. package/dist-client/pages/training-list.d.ts +16 -2
  29. package/dist-client/pages/training-list.js +63 -31
  30. package/dist-client/pages/training-list.js.map +1 -1
  31. package/dist-client/tsconfig.tsbuildinfo +1 -1
  32. package/dist-server/service/common/employee-reader.d.ts +0 -2
  33. package/dist-server/service/common/employee-reader.js +0 -12
  34. package/dist-server/service/common/employee-reader.js.map +1 -1
  35. package/dist-server/service/compliance-case/compliance-case-service.js +1 -1
  36. package/dist-server/service/compliance-case/compliance-case-service.js.map +1 -1
  37. package/dist-server/service/invention/invention-service.js +2 -1
  38. package/dist-server/service/invention/invention-service.js.map +1 -1
  39. package/dist-server/service/legal-record/attachment-readers.js +3 -3
  40. package/dist-server/service/legal-record/attachment-readers.js.map +1 -1
  41. package/dist-server/service/regulation/regulation-read.js +2 -1
  42. package/dist-server/service/regulation/regulation-read.js.map +1 -1
  43. package/dist-server/service/training/training-checks.d.ts +5 -0
  44. package/dist-server/service/training/training-checks.js +24 -0
  45. package/dist-server/service/training/training-checks.js.map +1 -1
  46. package/dist-server/service/training/training-query.d.ts +2 -0
  47. package/dist-server/service/training/training-query.js +11 -0
  48. package/dist-server/service/training/training-query.js.map +1 -1
  49. package/dist-server/service/training/training-register.js +6 -0
  50. package/dist-server/service/training/training-register.js.map +1 -1
  51. package/dist-server/service/training/training-rules.d.ts +7 -0
  52. package/dist-server/service/training/training-rules.js +8 -0
  53. package/dist-server/service/training/training-rules.js.map +1 -1
  54. package/dist-server/service/training/training-service.d.ts +11 -0
  55. package/dist-server/service/training/training-service.js +108 -6
  56. package/dist-server/service/training/training-service.js.map +1 -1
  57. package/dist-server/service/training/training-type.d.ts +1 -0
  58. package/dist-server/service/training/training-type.js +4 -0
  59. package/dist-server/service/training/training-type.js.map +1 -1
  60. package/dist-server/service/training/training.d.ts +13 -0
  61. package/dist-server/service/training/training.js +51 -1
  62. package/dist-server/service/training/training.js.map +1 -1
  63. package/dist-server/tsconfig.tsbuildinfo +1 -1
  64. package/package.json +19 -18
  65. package/translations/en.json +19 -7
  66. package/translations/es.json +18 -6
  67. package/translations/ja.json +19 -7
  68. package/translations/ko.json +19 -7
  69. package/translations/ms.json +19 -7
  70. package/translations/zh.json +19 -7
@@ -1 +1 @@
1
- {"version":3,"file":"training-checks.js","sourceRoot":"","sources":["../../../server/service/training/training-checks.ts"],"names":[],"mappings":";;;AAmBA,0DAaC;AAGD,gEAQC;AAGD,kEASC;AAvDD,qCAA2C;AAE3C,+DAAuD;AAEvD,yCAA0F;AAC1F,qDAA2G;AAC3G,yDAA2F;AAE3F;;;GAGG;AAEH,wFAAwF;AAC3E,QAAA,sBAAsB,GAAG,EAAE,CAAA;AAExC,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AAE3D,0HAA0H;AACnH,KAAK,UAAU,uBAAuB,CAAC,EAAiB,EAAE,MAAW,EAAE,KAAa;IACzF,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC1B,MAAM,KAAK,GAAG,CAAC,MAAM,IAAA,kCAAe,EAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAC5E,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,6BAAkB,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,mCAAwB,CAAC,SAAS,EAAS,EAAE,CAAC,CAAA;IACpK,MAAM,GAAG,GAAG,EAAE,CAAA;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,MAAM,IAAA,mCAAgB,EAAC,EAAE,EAAE,MAAM,EAAE,IAAA,sCAAmB,EAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAA;QAC3F,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAA;QAChG,MAAM,MAAM,GAAoB,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;QAC3F,MAAM,KAAK,GAAG,IAAA,gCAAe,EAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAChD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAA,iCAAgB,EAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,IAAA,uCAAsB,EAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACtM,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,yFAAyF;AAClF,KAAK,UAAU,0BAA0B,CAAC,EAAiB,EAAE,MAAW,EAAE,KAAa;IAC5F,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC1B,MAAM,KAAK,GAAG,CAAC,MAAM,IAAA,kCAAe,EAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAC5E,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,0BAAe,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAS,EAAE,CAAC,CAAA;IACpH,OAAO,KAAK;SACT,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAA,gCAAe,EAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;SACnF,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,IAAA,iCAAgB,EAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;SACtE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,IAAI,8BAAsB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;AACvI,CAAC;AAED,8FAA8F;AACvF,KAAK,UAAU,2BAA2B,CAAC,EAAiB,EAAE,MAAW,EAAE,KAAa;IAC7F,MAAM,KAAK,GAAG,CAAC,MAAM,IAAA,kCAAe,EAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACjG,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,EAAE,CAAA;IAC5B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,6BAAkB,CAAC,CAAC,IAAI,CAAC;QAC3D,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,mCAAwB,CAAC,SAAS,EAAE,IAAI,EAAE,IAAA,YAAE,EAAC,KAAK,CAAC,EAAS;QACxG,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAS;KACnC,CAAC,CAAA;IACF,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,aAAa,CAAC,uBAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAA,YAAE,EAAC,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAW,CAAC,CAAC,CAAC,CAAC,EAAS,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACxJ,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,EAAE,WAAW,EAAE,IAAA,iCAAgB,EAAC,UAAU,CAAC,SAAU,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;AACpN,CAAC","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":";;;;AAAA,+CAAkH;AAElH,qEAA0E;AAC1E,yDAAgD;AAChD,uDAAiD;AACjD,+DAAuD;AACvD,iDAAuH;AAEvH,+DAAmD;AACnD,yCAAgE;AAChE,yDAAkL;AAClL,mDAAqI;AAErI,MAAM,QAAQ,GAAG,CAAC,OAAwB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAA,yBAAO,EAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AAEhG,yDAAyD;AACzD,MAAM,QAAQ,GAAG,CAAC,MAAiB,EAAE,IAAY,EAAE,EAAE;IACnD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;IAC7D,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IACtG,OAAO,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAA;AACvH,CAAC,CAAA;AAIM,IAAM,aAAa,GAAnB,MAAM,aAAa;IAGlB,AAAN,KAAK,CAAC,aAAa,CAAQ,OAAwB;QACjD,OAAO,CAAC,MAAM,IAAA,kCAAe,EAAC,IAAA,oCAAiB,EAAC,OAAO,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAQ,CAAA;IACzF,CAAC;IAIK,AAAN,KAAK,CAAC,gBAAgB,CAA0B,MAAiB,EAAS,OAAwB;QAChG,MAAM,YAAY,GAAG,IAAA,qCAA6B,EAAC;YACjD,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM;YAC5B,MAAM;YACN,UAAU,EAAE,IAAA,qBAAa,EAAC,0BAAe,CAAC;YAC1C,WAAW,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,OAAO,CAAC;YAC5D,KAAK,EAAE,SAAS;YAChB,eAAe,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SACpD,CAAC,CAAA;QACF,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,YAAY,CAAC,eAAe,EAAE,CAAA;QAC3D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzB,CAAC;IAKK,AAAN,KAAK,CAAC,mBAAmB,CAA0B,MAAiB,EAAS,OAAwB;QACnG,MAAM,MAAM,GAAG,MAAM,IAAA,mCAAgB,EAAC,OAAO,CAAC,CAAA;QAC9C,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,MAAM,IAAI,eAAO,CAAC,2BAA2B,EAAE,EAAE,EAAE,8CAA8C,CAAC,CAAA;QACvI,MAAM,YAAY,GAAG,IAAA,qCAA6B,EAAC;YACjD,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM;YAC5B,MAAM;YACN,UAAU,EAAE,IAAA,qBAAa,EAAC,6BAAkB,CAAC;YAC7C,WAAW,EAAE,CAAC,cAAc,EAAE,cAAc,CAAC;YAC7C,KAAK,EAAE,YAAY;YACnB,eAAe,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SACrD,CAAC,CAAA;QACF,IAAI,CAAC,MAAM,CAAC,GAAG;YAAE,YAAY,CAAC,QAAQ,CAAC,6BAA6B,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,QAAS,CAAC,EAAE,EAAE,CAAC,CAAA;QACpG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,YAAY,CAAC,eAAe,EAAE,CAAA;QAC3D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;IACzB,CAAC;IAMK,AAAN,KAAK,CAAC,iBAAiB,CAA0B,MAAiB,EAAS,OAAwB;QACjG,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QAC7E,IAAI,IAAI,GAAG,MAAM,IAAA,oCAAiB,EAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QACjD,KAAK,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YACpC,IAAI,KAAK;gBAAE,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAE,GAAW,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,CAAA;QAC/F,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,KAAK,CAAA;QAC7F,IAAI,MAAM;YAAE,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,IAAI,GAAG,CAAC,cAAc,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;QACjJ,MAAM,EAAE,IAAI,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,UAAU,IAAK,EAAU,CAAA;QAChE,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAA;IAChH,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CAA+C,IAAY,EAAS,OAAwB;QAC3G,OAAO,CAAC,MAAM,IAAA,8BAAW,EAAC,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAQ,CAAA;IACvE,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CAAS,GAAoB,EAAS,OAAwB;QAC7E,OAAO,MAAM,OAAO,CAAC,4CAAyB,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAClE,CAAC;IAGK,AAAN,KAAK,CAAC,MAAM,CAAS,GAAoB;QACvC,OAAO,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,iBAAM,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAS,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1G,CAAC;IAGK,AAAN,KAAK,CAAC,OAAO,CAAS,GAAoB;QACxC,OAAO,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACnG,CAAC;CACF,CAAA;AA7EY,sCAAa;AAGlB;IAFL,IAAA,wBAAS,EAAC,gFAAgF,CAAC;IAC3F,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,oCAAoB,CAAC,EAAE,EAAE,WAAW,EAAE,mGAAmG,EAAE,CAAC;IAC1I,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;kDAEzB;AAIK;IAFL,IAAA,wBAAS,EAAC,gFAAgF,CAAC;IAC3F,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,mCAAmB,EAAE,EAAE,WAAW,EAAE,kEAAkE,EAAE,CAAC;IACnG,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IAAqB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAAjB,iBAAS;;qDAWhE;AAKK;IAHL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,sCAAsB,EAAE;QACxC,WAAW,EAAE,0IAA0I;KACxJ,CAAC;IACyB,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IAAqB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAAjB,iBAAS;;wDAcnE;AAMK;IAJL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oCAAoB,EAAE;QACtC,WAAW,EACT,iNAAiN;KACpN,CAAC;IACuB,mBAAA,IAAA,mBAAI,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAS,CAAC,CAAA;IAAqB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAAjB,iBAAS;;sDAWjE;AAGK;IADL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,0BAAU,CAAC,EAAE,EAAE,WAAW,EAAE,6GAA6G,EAAE,CAAC;IAC5I,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAAgB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;gDAEnF;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,4BAAU,CAAC,EAAE,EAAE,WAAW,EAAE,yEAAyE,EAAE,CAAC;IAC7G,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAAwB,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAAvB,0BAAe;;gDAE7C;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC;IAC7F,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAM,0BAAe;;2CAExC;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjC,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAM,0BAAe;;4CAEzC;wBA5EU,aAAa;IAFzB,IAAA,uBAAQ,EAAC,0BAAe,CAAC;IACzB,IAAA,4BAAa,EAAC,qBAAa,CAAC;GAChB,aAAa,CA6EzB;AAGM,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAE5B,AAAN,KAAK,CAAC,QAAQ,CAAS,GAAuB;QAC5C,OAAO,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,uBAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACzG,CAAC;IAGK,AAAN,KAAK,CAAC,OAAO,CAAS,GAAuB;QAC3C,OAAO,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,0BAAe,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAC9G,CAAC;IAGK,AAAN,KAAK,CAAC,SAAS,CAAS,GAAuB;QAC7C,OAAO,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,IAAA,qBAAa,EAAC,gBAAI,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACvG,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CAAS,GAAuB,EAAS,OAAwB;QAChF,OAAO,MAAM,OAAO,CAAC,+CAA4B,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IACrE,CAAC;CACF,CAAA;AApBY,0DAAuB;AAE5B;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,uBAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACpC,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAM,6BAAkB;;uDAE7C;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,0BAAe,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5C,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAM,6BAAkB;;sDAE5C;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC/B,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAM,6BAAkB;;wDAE9C;AAGK;IADL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,4BAAU,CAAC,EAAE,EAAE,WAAW,EAAE,yEAAyE,EAAE,CAAC;IAC7G,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA2B,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CAA1B,6BAAkB;;0DAEhD;kCAnBU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,6BAAkB,CAAC;GAChB,uBAAuB,CAoBnC;AAED,2FAA2F;AAC3F,KAAK,UAAU,OAAO,CAAC,OAAe,EAAE,KAAa,EAAE,OAAwB;IAC7E,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,4BAAU,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAS,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAS,EAAE,CAAC,CAAA;IACpK,OAAO,MAAM,IAAA,8BAAY,EAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC1C,CAAC","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":";;AAaA,8CAWC;AAxBD,+DAA0E;AAC1E,qEAA0E;AAC1E,iDAAqD;AAErD,yCAAgE;AAChE,mDAAuD;AACvD,yDAA2J;AAE3J;;;;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;AACzB,CAAC","sourcesContent":["import { registerAnnouncementTrigger } from '@things-factory/announcement'\nimport { registerAttachmentReader } from '@things-factory/attachment-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { TrainingCompletion, TrainingSession } from './training'\nimport { registerTrainingReads } from './training-read'\nimport { mayReadCompletion, mayReadSession, TRAINING_COMPLETION_REF_TYPE, TRAINING_SCHEDULED_TRIGGER, TRAINING_SESSION_REF_TYPE } 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}\n"]}
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":"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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 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;
@@ -23,9 +25,11 @@ exports.mayReadSession = mayReadSession;
23
25
  const typeorm_1 = require("typeorm");
24
26
  const announcement_1 = require("@things-factory/announcement");
25
27
  const attachment_base_1 = require("@things-factory/attachment-base");
28
+ const auth_base_1 = require("@things-factory/auth-base");
26
29
  const translator_1 = require("@things-factory/i18n-base/translator");
27
30
  const organization_1 = require("@things-factory/organization");
28
31
  const employee_profile_rules_js_1 = require("@things-factory/organization/dist-server/service/employee-profile/employee-profile-rules.js");
32
+ const learning_1 = require("@things-factory/learning");
29
33
  const shell_1 = require("@things-factory/shell");
30
34
  const employee_reader_1 = require("../common/employee-reader");
31
35
  const legal_todo_1 = require("../common/legal-todo");
@@ -41,6 +45,8 @@ exports.TRAINING_COMPLETION_REF_TYPE = 'TrainingCompletion';
41
45
  exports.TRAINING_SCHEDULED_TRIGGER = 'compliance.training-scheduled';
42
46
  /** The employee's screen of their trainings - where a to-do and an announcement lead. */
43
47
  exports.MY_TRAININGS_ADDRESS = 'regulations?tab=trainings';
48
+ /** The source an online session's enrolments are asked under (ADR-0194 decision 15) - `<source>:<session id>`. */
49
+ exports.TRAINING_SESSION_SOURCE = 'compliance.training-session';
44
50
  const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
45
51
  const kindWord = (t, code) => t(`text.training-kind.${code}`);
46
52
  /* ---------------------------------------------------------------- kinds and people */
@@ -89,7 +95,7 @@ const audienceRows = (audiences) => (audiences ?? []).map(one => ({ scope: one.s
89
95
  async function trainingReaderOf(context) {
90
96
  const { domain } = context.state;
91
97
  const tx = trainingManagerOf(context);
92
- const all = await (0, employee_reader_1.holds)(context, 'training', 'query');
98
+ const all = await (0, auth_base_1.requestHolds)(context, 'training', 'query');
93
99
  const employee = await (0, employee_reader_1.workingEmployeeOf)(context);
94
100
  let headed = [];
95
101
  if (employee) {
@@ -149,12 +155,15 @@ async function openTrainingSession(input, attach, context) {
149
155
  instructor: text(input.instructor),
150
156
  organisation: text(input.organisation),
151
157
  audiences: audienceRows(input.audiences),
158
+ course: input.method === training_1.TrainingMethod.ONLINE && input.courseId ? { id: input.courseId } : null,
152
159
  notes: text(input.notes),
153
160
  creator: user,
154
161
  updater: user
155
162
  });
156
163
  for (const file of attach ?? [])
157
164
  await (0, attachment_base_1.createAttachment)(null, { attachment: { file, refType: exports.TRAINING_SESSION_REF_TYPE, refBy: saved.id } }, context);
165
+ if (saved.method === training_1.TrainingMethod.ONLINE)
166
+ await enrollOnlineSession(saved, kind, context);
158
167
  const t = context.t ?? ((key) => key);
159
168
  const when = saved.endsOn && saved.endsOn !== saved.startsOn ? `${saved.startsOn} ~ ${saved.endsOn}` : saved.startsOn;
160
169
  await (0, announcement_1.announce)(context, {
@@ -173,6 +182,37 @@ async function openTrainingSession(input, attach, context) {
173
182
  });
174
183
  return saved;
175
184
  }
185
+ /**
186
+ * Opens an online session to its audience (ADR-0194 decision 15): each person still working here is enrolled in the
187
+ * 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 -
188
+ * so the year's to-do a person was handed for the kind is closed, to leave one.
189
+ */
190
+ async function enrollOnlineSession(session, kind, context) {
191
+ const { domain, user, tx } = context.state;
192
+ const people = (await trainingPeopleOf(tx, domain, trainingAudiencesOf(kind, domain, session.audiences), (0, employee_reader_1.todayIn)(domain))).filter(one => one.user);
193
+ const courseId = session.course?.id ?? session.courseId;
194
+ 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);
195
+ const repository = tx.getRepository(training_1.TrainingAssignment);
196
+ for (const person of people) {
197
+ const assignment = await repository.findOne({ where: { domain: { id: domain.id }, employee: { id: person.id }, kind: session.kind, year: session.year } });
198
+ if (!assignment?.todoInstanceId || assignment.closedAt)
199
+ continue;
200
+ await (0, legal_todo_1.closeLegalTodo)(tx, domain, assignment.todoInstanceId, user, '온라인 회차 수강');
201
+ assignment.todoInstanceId = null;
202
+ assignment.updater = user;
203
+ await repository.save(assignment);
204
+ }
205
+ }
206
+ /** The people of a kind and year who hold an open enrolment of an online session - their to-do is that enrolment's. */
207
+ async function onlineLearnersOf(tx, domain, kind, year) {
208
+ const sessions = await tx.getRepository(training_1.TrainingSession).find({ where: { domain: { id: domain.id }, kind, year, method: training_1.TrainingMethod.ONLINE } });
209
+ if (!sessions.length)
210
+ return new Set();
211
+ const open = await tx.getRepository(learning_1.Enrollment).find({
212
+ 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]) }
213
+ });
214
+ return new Set(open.map(one => one.userId));
215
+ }
176
216
  /** What a session says may be corrected - its kind and year stay, the completions of it count for them. */
177
217
  async function updateTrainingSessions(patches, context) {
178
218
  const { domain, user, tx } = context.state;
@@ -183,11 +223,14 @@ async function updateTrainingSessions(patches, context) {
183
223
  const row = await repository.findOne({ where: { id: patch.id, domain: { id: domain.id } } });
184
224
  if (!row)
185
225
  throw new shell_1.Refusal('training-not-found', {}, 'the record was not found. reload and try again.');
226
+ /* An online session's people are enrolled in its course - it stays online (ADR-0194 decision 15). */
227
+ if (patch.method !== undefined && patch.method !== row.method && (row.method === training_1.TrainingMethod.ONLINE || patch.method === training_1.TrainingMethod.ONLINE))
228
+ 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
229
  for (const field of ['name', 'method', 'startsOn', 'endsOn', 'instructor', 'organisation', 'notes'])
187
230
  if (patch[field] !== undefined)
188
231
  row[field] = field === 'name' || field === 'method' || field === 'startsOn' ? (patch[field] ?? '').trim() : text(patch[field]);
189
232
  /* 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 });
233
+ const refusal = (0, training_rules_1.trainingSessionRefusal)({ ...row, courseId: row.courseId }, { ...kinds.find(one => one.code === row.kind), enabled: true });
191
234
  if (refusal)
192
235
  throw refusal;
193
236
  row.updater = user;
@@ -333,9 +376,11 @@ async function openTrainingYear(kindCode, year, context) {
333
376
  const people = await trainingPeopleOf(tx, domain, trainingAudiencesOf(kind, domain), today);
334
377
  const repository = tx.getRepository(training_1.TrainingAssignment);
335
378
  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));
379
+ /* Someone enrolled in an online session of the kind has that enrolment's to-do - none twice. */
380
+ const online = await onlineLearnersOf(tx, domain, kindCode, year);
336
381
  let handedOut = 0;
337
382
  for (const person of people) {
338
- if (done.has(person.id) || !person.user)
383
+ if (done.has(person.id) || !person.user || online.has(person.user.id))
339
384
  continue;
340
385
  const assignment = (await repository.findOne({ where: { domain: { id: domain.id }, employee: { id: person.id }, kind: kindCode, year } })) ??
341
386
  (await repository.save({ domain, employee: person, kind: kindCode, year, dueOn, creator: user, updater: user }));
@@ -360,6 +405,63 @@ async function openTrainingYear(kindCode, year, context) {
360
405
  }
361
406
  return { people: people.length, done: people.filter(one => done.has(one.id)).length, handedOut };
362
407
  }
408
+ /* ---------------------------------------------------------------- online completions (ADR-0194 decision 16) */
409
+ const dayIn = (at, timezone) => new Intl.DateTimeFormat('en-CA', { timeZone: timezone || 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at));
410
+ /**
411
+ * An enrolment of an online session completed: a confirmed completion of the session's kind and year, with the
412
+ * learning module's evidence - no manager's confirmation needed. Runs in the completion's transaction: what fails here
413
+ * takes the completion back. Someone done already for the kind and year is left as they are.
414
+ */
415
+ async function recordOnlineCompletion(enrollment, context) {
416
+ const { domain, tx } = context.state;
417
+ const session = await tx.getRepository(training_1.TrainingSession).findOne({ where: { id: enrollment.sourceId, domain: { id: domain.id } } });
418
+ if (!session)
419
+ throw new shell_1.Refusal('training-not-found', {}, 'the session of this enrolment was not found');
420
+ const employee = await tx.getRepository(organization_1.Employee).findOne({ where: { domain: { id: domain.id }, user: { id: enrollment.userId } } });
421
+ if (!employee)
422
+ throw new shell_1.Refusal('training-employee-unknown', {}, 'the learner must be an employee working in this domain');
423
+ if ((0, training_rules_1.trainingStateOf)(await completionsOf(tx, domain, employee.id, session.kind, session.year)) === 'DONE')
424
+ return null;
425
+ const version = enrollment.versionId ? await tx.getRepository(learning_1.CourseVersion).findOne({ where: { id: enrollment.versionId }, withDeleted: true }) : null;
426
+ const saved = await tx.getRepository(training_1.TrainingCompletion).save({
427
+ domain,
428
+ employee,
429
+ kind: session.kind,
430
+ year: session.year,
431
+ session,
432
+ completedOn: dayIn(enrollment.completedAt ?? new Date(), domain.timezone),
433
+ method: training_1.TrainingMethod.ONLINE,
434
+ status: training_1.TrainingCompletionStatus.CONFIRMED,
435
+ decidedAt: new Date(),
436
+ learningEnrollmentId: enrollment.id,
437
+ learningEvidence: {
438
+ minutes: Math.floor((enrollment.learnedSeconds ?? 0) / 60),
439
+ score: enrollment.score ?? null,
440
+ attempts: enrollment.attempts ?? 0,
441
+ acknowledgedAt: enrollment.acknowledgedAt ?? null,
442
+ version: version?.number ?? null,
443
+ certificateNo: enrollment.certificateNo ?? null
444
+ },
445
+ creator: context.state.user,
446
+ updater: context.state.user
447
+ });
448
+ await closeTodoOf(tx, domain, employee.id, session.kind, session.year, context.state.user);
449
+ return saved;
450
+ }
451
+ /** A completed enrolment a manager cancelled: the completion made from it is returned with the reason - kept, not deleted. */
452
+ async function returnOnlineCompletion(enrollment, reason, context) {
453
+ const { domain, user, tx } = context.state;
454
+ const repository = tx.getRepository(training_1.TrainingCompletion);
455
+ const row = await repository.findOne({ where: { domain: { id: domain.id }, learningEnrollmentId: enrollment.id } });
456
+ if (!row || row.status !== training_1.TrainingCompletionStatus.CONFIRMED)
457
+ return;
458
+ row.status = training_1.TrainingCompletionStatus.RETURNED;
459
+ row.returnReason = reason;
460
+ row.decidedAt = new Date();
461
+ row.decidedBy = user;
462
+ row.updater = user;
463
+ await repository.save(row);
464
+ }
363
465
  /** Where everyone of every kind on stands in a year - the people of each kind's audience, by name. */
364
466
  async function standingsOf(tx, domain, year, today) {
365
467
  const kinds = (await trainingKindsOf(tx, domain)).filter(one => one.enabled);
@@ -431,7 +533,7 @@ async function myTrainings(year, context) {
431
533
  async function mayReadCompletion(row, context) {
432
534
  if (!row)
433
535
  return false;
434
- if (await (0, employee_reader_1.holds)(context, 'training', 'query'))
536
+ if (await (0, auth_base_1.requestHolds)(context, 'training', 'query'))
435
537
  return true;
436
538
  const employee = await (0, employee_reader_1.workingEmployeeOf)(context);
437
539
  return !!employee && employee.id === row.employeeId;
@@ -440,7 +542,7 @@ async function mayReadCompletion(row, context) {
440
542
  async function mayReadSession(row, context) {
441
543
  if (!row)
442
544
  return false;
443
- if (await (0, employee_reader_1.holds)(context, 'training', 'query'))
545
+ if (await (0, auth_base_1.requestHolds)(context, 'training', 'query'))
444
546
  return true;
445
547
  const { domain } = context.state;
446
548
  const employee = await (0, employee_reader_1.workingEmployeeOf)(context);