@things-factory/learning 10.1.260 → 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 (94) hide show
  1. package/dist-client/components/learning-material-form.d.ts +41 -0
  2. package/dist-client/components/learning-material-form.js +202 -0
  3. package/dist-client/components/learning-material-form.js.map +1 -0
  4. package/dist-client/components/learning-unit-form.d.ts +3 -0
  5. package/dist-client/components/learning-unit-form.js +10 -2
  6. package/dist-client/components/learning-unit-form.js.map +1 -1
  7. package/dist-client/pages/learning-course-editor.d.ts +1 -0
  8. package/dist-client/pages/learning-course-editor.js +19 -6
  9. package/dist-client/pages/learning-course-editor.js.map +1 -1
  10. package/dist-client/pages/learning-course-list.d.ts +1 -1
  11. package/dist-client/pages/learning-course-list.js +6 -2
  12. package/dist-client/pages/learning-course-list.js.map +1 -1
  13. package/dist-client/pages/learning-enrollment-status.d.ts +28 -16
  14. package/dist-client/pages/learning-enrollment-status.js +95 -112
  15. package/dist-client/pages/learning-enrollment-status.js.map +1 -1
  16. package/dist-client/pages/learning-view.d.ts +11 -0
  17. package/dist-client/pages/learning-view.js +94 -19
  18. package/dist-client/pages/learning-view.js.map +1 -1
  19. package/dist-client/tsconfig.tsbuildinfo +1 -1
  20. package/dist-client/utils/learning-api.d.ts +48 -3
  21. package/dist-client/utils/learning-api.js +51 -2
  22. package/dist-client/utils/learning-api.js.map +1 -1
  23. package/dist-client/utils/material-source.d.ts +12 -0
  24. package/dist-client/utils/material-source.js +77 -0
  25. package/dist-client/utils/material-source.js.map +1 -0
  26. package/dist-server/ai-reads.js +1 -1
  27. package/dist-server/ai-reads.js.map +1 -1
  28. package/dist-server/service/course/course-mutation.d.ts +1 -1
  29. package/dist-server/service/course/course-mutation.js +7 -6
  30. package/dist-server/service/course/course-mutation.js.map +1 -1
  31. package/dist-server/service/course/course-query.d.ts +1 -0
  32. package/dist-server/service/course/course-query.js +14 -3
  33. package/dist-server/service/course/course-query.js.map +1 -1
  34. package/dist-server/service/course/course-rules.d.ts +3 -1
  35. package/dist-server/service/course/course-rules.js +5 -1
  36. package/dist-server/service/course/course-rules.js.map +1 -1
  37. package/dist-server/service/course/course-service.d.ts +5 -4
  38. package/dist-server/service/course/course-service.js +10 -14
  39. package/dist-server/service/course/course-service.js.map +1 -1
  40. package/dist-server/service/course/course-type.d.ts +1 -0
  41. package/dist-server/service/course/course-type.js +4 -0
  42. package/dist-server/service/course/course-type.js.map +1 -1
  43. package/dist-server/service/course/course-unit.d.ts +3 -2
  44. package/dist-server/service/course/course-unit.js +7 -2
  45. package/dist-server/service/course/course-unit.js.map +1 -1
  46. package/dist-server/service/course/index.d.ts +2 -1
  47. package/dist-server/service/course/index.js +2 -1
  48. package/dist-server/service/course/index.js.map +1 -1
  49. package/dist-server/service/course/material-audio.d.ts +1 -0
  50. package/dist-server/service/course/material-audio.js +43 -0
  51. package/dist-server/service/course/material-audio.js.map +1 -0
  52. package/dist-server/service/course/material-draft-mutation.d.ts +13 -0
  53. package/dist-server/service/course/material-draft-mutation.js +55 -0
  54. package/dist-server/service/course/material-draft-mutation.js.map +1 -0
  55. package/dist-server/service/course/material-draft-rules.d.ts +38 -0
  56. package/dist-server/service/course/material-draft-rules.js +83 -0
  57. package/dist-server/service/course/material-draft-rules.js.map +1 -0
  58. package/dist-server/service/course/material-draft-service.d.ts +15 -0
  59. package/dist-server/service/course/material-draft-service.js +162 -0
  60. package/dist-server/service/course/material-draft-service.js.map +1 -0
  61. package/dist-server/service/enrollment/enrollment-query.d.ts +5 -4
  62. package/dist-server/service/enrollment/enrollment-query.js +26 -35
  63. package/dist-server/service/enrollment/enrollment-query.js.map +1 -1
  64. package/dist-server/service/enrollment/enrollment-service.js +13 -3
  65. package/dist-server/service/enrollment/enrollment-service.js.map +1 -1
  66. package/dist-server/service/enrollment/enrollment-status.d.ts +31 -18
  67. package/dist-server/service/enrollment/enrollment-status.js +101 -42
  68. package/dist-server/service/enrollment/enrollment-status.js.map +1 -1
  69. package/dist-server/service/enrollment/enrollment-type.d.ts +1 -14
  70. package/dist-server/service/enrollment/enrollment-type.js +6 -54
  71. package/dist-server/service/enrollment/enrollment-type.js.map +1 -1
  72. package/dist-server/service/enrollment/enrollment.d.ts +1 -0
  73. package/dist-server/service/enrollment/enrollment.js +5 -0
  74. package/dist-server/service/enrollment/enrollment.js.map +1 -1
  75. package/dist-server/service/enrollment/learning-ai.d.ts +9 -2
  76. package/dist-server/service/enrollment/learning-ai.js +25 -28
  77. package/dist-server/service/enrollment/learning-ai.js.map +1 -1
  78. package/dist-server/service/enrollment/learning-documents.js +3 -4
  79. package/dist-server/service/enrollment/learning-documents.js.map +1 -1
  80. package/dist-server/service/enrollment/progress-rules.d.ts +36 -11
  81. package/dist-server/service/enrollment/progress-rules.js +47 -12
  82. package/dist-server/service/enrollment/progress-rules.js.map +1 -1
  83. package/dist-server/service/enrollment/unit-progress.d.ts +4 -0
  84. package/dist-server/service/enrollment/unit-progress.js +20 -0
  85. package/dist-server/service/enrollment/unit-progress.js.map +1 -1
  86. package/dist-server/service/index.d.ts +1 -1
  87. package/dist-server/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +14 -13
  89. package/translations/en.json +30 -7
  90. package/translations/es.json +30 -7
  91. package/translations/ja.json +30 -7
  92. package/translations/ko.json +30 -7
  93. package/translations/ms.json +30 -7
  94. package/translations/zh.json +30 -7
@@ -1,12 +1,11 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
+ import '@operato/data-grist/ox-filters-form.js';
2
3
  import '@operato/data-grist/ox-grist.js';
3
- import '@operato/input/ox-select.js';
4
- import '@operato/popup/ox-popup-list.js';
5
4
  import gql from 'graphql-tag';
6
- import { css, html, nothing } from 'lit';
5
+ import { html } from 'lit';
7
6
  import { customElement, query, state } from 'lit/decorators.js';
8
7
  import { DataGrist } from '@operato/data-grist';
9
- import { MutationController, QueryController } from '@operato/graphql';
8
+ import { client, MutationController, QUERY_ANSWERED, QueryController } from '@operato/graphql';
10
9
  import { i18next, localize } from '@operato/i18n';
11
10
  import { notify } from '@operato/layout';
12
11
  import { OxPrompt } from '@operato/popup/ox-prompt.js';
@@ -17,63 +16,36 @@ import { hasPrivilege } from '@things-factory/auth-base/dist-client';
17
16
  import { issueDocument } from '@things-factory/pdf/dist-client';
18
17
  import { STATUS_ROW_FIELDS } from '../utils/learning-api.js';
19
18
  import { dayOf, minutesOf, STATES, stateWord, t } from '../utils/learning-words.js';
19
+ const TEAM_ROW_FIELDS = 'id state departmentName units unitsDone user { id name } course { id name } enrollment { id status dueOn completedAt }';
20
20
  /**
21
- * 수강 현황 (ADR-0194 decisions 10, 11) - where every enrolment of a course stands: who, their department, the state
22
- * today, units done, minutes, score and the completion day, with counts on top. A manager reminds and cancels from here
23
- * and issues the completion register (PDF, through the issued document ledger) or exports it (Excel). A department head
24
- * sees the people of their departments - the state and days only.
21
+ * 수강 현황 (ADR-0194 decisions 10, 11) - where enrolments stand: who, their department, the state today, units done,
22
+ * minutes, score and the completion day. The conditions - course, state with its counts, search, sort, page - are the
23
+ * grid's, kept in the address, and read by the server. A manager reminds and cancels from here and issues the completion
24
+ * register of the course filtered (PDF, through the issued document ledger) or exports the list (Excel). A department
25
+ * head sees the people of their departments - the state and days only.
25
26
  */
26
27
  let LearningEnrollmentStatus = class LearningEnrollmentStatus extends localize(i18next)(PageView) {
27
28
  constructor() {
28
29
  super(...arguments);
29
- this.courseId = '';
30
- this.stateFilter = '';
30
+ /* The courses the course filter offers - a manager's only. */
31
31
  this.courses = new QueryController(this, gql `query { courses(pagination: { page: 1, limit: 500 }) { items { id name } } }`, {
32
32
  variables: () => (this.manager ? {} : undefined)
33
33
  });
34
- this.rows = new QueryController(this, gql `query ($filter: EnrollmentStatusFilter) { enrollmentStatus(filter: $filter) { items { ${STATUS_ROW_FIELDS} } summary { total assigned inProgress completed overdue } } }`, { variables: () => (this.manager === true ? { filter: this.filter } : undefined), keepPrevious: true });
35
- this.teamRows = new QueryController(this, gql `query ($filter: EnrollmentStatusFilter) { teamEnrollmentStatus(filter: $filter) { items { id state departmentName units unitsDone user { id name } course { id name } enrollment { id status dueOn completedAt } } summary { total assigned inProgress completed overdue } } }`, { variables: () => (this.manager === false ? { filter: this.filter } : undefined), keepPrevious: true });
36
34
  this.reminder = new MutationController(this, gql `mutation ($ids: [String!]!) { remindEnrollments(ids: $ids) }`);
37
- this.canceller = new MutationController(this, gql `mutation ($id: String!, $reason: String!) { cancelEnrollment(id: $id, reason: $reason) { id } }`, { refetch: () => [this.rows] });
35
+ this.canceller = new MutationController(this, gql `mutation ($id: String!, $reason: String!) { cancelEnrollment(id: $id, reason: $reason) { id } }`);
38
36
  }
39
- static { this.styles = [
40
- WorkSurfacePageStyles,
41
- CommonGristStyles,
42
- CommonHeaderStyles,
43
- ScrollbarStyles,
44
- css `
45
- .filters {
46
- display: flex;
47
- flex-wrap: wrap;
48
- align-items: center;
49
- gap: var(--spacing-medium, 8px);
50
- }
51
- ox-select {
52
- min-width: 160px;
53
- }
54
- .counts {
55
- display: flex;
56
- flex-wrap: wrap;
57
- gap: var(--spacing-small, 4px) var(--spacing-large, 12px);
58
- color: var(--ui-muted);
59
- font-size: var(--ui-t-small, 12px);
60
- }
61
- .counts strong {
62
- color: var(--ui-ink);
63
- font-variant-numeric: tabular-nums;
64
- }
65
- `
66
- ]; }
67
- get filter() {
68
- return { ...(this.courseId ? { courseId: this.courseId } : {}), ...(this.stateFilter ? { state: this.stateFilter } : {}) };
69
- }
70
- get list() {
71
- return (this.manager ? this.rows.data?.enrollmentStatus : this.teamRows.data?.teamEnrollmentStatus) ?? { items: [], summary: null };
37
+ static { this.styles = [WorkSurfacePageStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles]; }
38
+ /** The course the grid's `course` filter names - the completion register is of it. */
39
+ get filteredCourse() {
40
+ const id = this.grist?.filters?.find((one) => one.name === 'course')?.value;
41
+ return id ? (this.courses.data?.courses?.items ?? []).find((one) => one.id === id) : undefined;
72
42
  }
73
43
  get context() {
74
- const course = this.courseList.find(one => one.id === this.courseId);
44
+ const course = this.filteredCourse;
75
45
  return {
76
46
  title: t('title.learning-enrollment-status'),
47
+ search: { handler: (search) => (this.grist.searchText = search), value: this.grist?.searchText || '' },
48
+ filter: { handler: () => this.grist.toggleHeadroom() },
77
49
  ...(this.manager
78
50
  ? {
79
51
  actions: [
@@ -86,23 +58,34 @@ let LearningEnrollmentStatus = class LearningEnrollmentStatus extends localize(i
86
58
  : {})
87
59
  };
88
60
  }
89
- get courseList() {
90
- return this.courses.data?.courses?.items ?? [];
91
- }
92
- async pageUpdated(changes, lifecycle) {
93
- if (!this.active)
94
- return;
95
- if (changes.params || changes.active) {
96
- const course = lifecycle?.params?.course;
97
- if (course && course !== this.courseId)
98
- this.courseId = course;
99
- }
61
+ render() {
62
+ return this.gristConfig
63
+ ? html `<ox-grist
64
+ class="page-fill"
65
+ search-mode="term"
66
+ .mode=${isMobileDevice() ? 'CARD' : 'GRID'}
67
+ .config=${this.gristConfig}
68
+ .fetchHandler=${this.fetchHandler.bind(this)}
69
+ .countHandler=${this.countHandler.bind(this)}
70
+ ?url-params-sensitive=${this.active}
71
+ >
72
+ <div slot="headroom" class="header">
73
+ <div class="filters"><ox-filters-form without-search></ox-filters-form></div>
74
+ </div>
75
+ </ox-grist>`
76
+ : html ``;
100
77
  }
101
78
  async pageInitialized() {
102
79
  this.manager = !!(await hasPrivilege({ category: 'course', privilege: 'query', domainOwnerGranted: true }));
103
80
  this.gristConfig = this.configOf(this.manager);
104
81
  this.updateContext();
105
82
  }
83
+ /** The courses for the course filter, once the read of them has answered. */
84
+ async courseOptions() {
85
+ if (this.courses.loading)
86
+ await new Promise(resolve => this.addEventListener(QUERY_ANSWERED, resolve, { once: true }));
87
+ return [{ value: '', display: t('text.learning-course-all') }, ...(this.courses.data?.courses?.items ?? []).map((one) => ({ value: one.id, display: one.name }))];
88
+ }
106
89
  recordOf(row) {
107
90
  const e = row.enrollment ?? {};
108
91
  return {
@@ -121,63 +104,67 @@ let LearningEnrollmentStatus = class LearningEnrollmentStatus extends localize(i
121
104
  };
122
105
  }
123
106
  configOf(manager) {
124
- const column = (name, header, width, extra = {}) => ({ type: 'string', name, header, width, record: { editable: false, ...extra } });
107
+ const column = (name, header, width, extra = {}) => ({ type: 'string', name, header, width, sortable: false, ...extra, record: { editable: false, ...extra.record } });
125
108
  return {
126
109
  rows: { appendable: false, selectable: manager ? { multiple: true } : false },
127
- pagination: { infinite: true },
110
+ pagination: { limit: 100, pages: [50, 100, 500] },
128
111
  emptyNote: { title: t('text.learning-no-enrollment') },
129
112
  list: { fields: ['name', 'state'], details: ['course', 'dueOn'] },
130
113
  columns: [
131
114
  ...(manager ? [{ type: 'gutter', gutterName: 'row-selector', multiple: true }] : []),
132
115
  column('name', t('field.learning-learner'), 120),
133
116
  column('department', t('field.learning-department'), 120),
134
- column('course', t('field.learning-course-name'), 220),
135
- column('state', t('field.learning-state'), 100, { renderer: (value) => stateWord(value) }),
136
- column('dueOn', t('field.learning-due-on'), 110),
137
- column('units', t('field.learning-units-done'), 90, { align: 'right' }),
138
- ...(manager ? [column('minutes', t('field.learning-minutes'), 120, { align: 'right' }), column('score', t('field.learning-score'), 70, { align: 'right' })] : []),
117
+ column('course', t('field.learning-course-name'), 220, manager ? { filter: { type: 'select', operator: 'eq', options: () => this.courseOptions() } } : {}),
118
+ column('state', t('field.learning-state'), 100, {
119
+ record: { renderer: (value) => stateWord(value) },
120
+ /* Chips with their counts; none pressed is every state. */
121
+ filter: { type: 'select-buttons', operator: 'in', options: STATES.map(value => ({ value, display: stateWord(value) })) }
122
+ }),
123
+ column('dueOn', t('field.learning-due-on'), 110, { sortable: true }),
124
+ column('units', t('field.learning-units-done'), 90, { record: { align: 'right' } }),
125
+ ...(manager ? [column('minutes', t('field.learning-minutes'), 120, { record: { align: 'right' } }), column('score', t('field.learning-score'), 70, { record: { align: 'right' } })] : []),
139
126
  column('completedOn', t('field.learning-completed-on'), 110),
140
127
  ...(manager ? [column('certificateNo', t('field.learning-certificate-no'), 180)] : [])
141
- ]
128
+ ],
129
+ sorters: [{ name: 'dueOn' }]
142
130
  };
143
131
  }
144
- render() {
145
- const summary = this.list.summary;
146
- const states = [{ value: '', display: t('text.learning-state-all') }, ...STATES.map(value => ({ value, display: stateWord(value) }))];
147
- const courses = [{ id: '', name: t('text.learning-course-all') }, ...this.courseList];
148
- const records = this.list.items.map((row) => this.recordOf(row));
149
- return html `<ox-grist class="page-fill" .mode=${isMobileDevice() ? 'CARD' : 'GRID'} .config=${this.gristConfig} .data=${{ records, total: records.length }}>
150
- <div slot="headroom" class="header">
151
- <div class="filters">
152
- ${this.manager
153
- ? html `<ox-select .value=${this.courseId} aria-label=${t('field.learning-course-name')} @change=${(e) => this.pick('course', String(e.detail ?? ''))}>
154
- <span slot="label">${courses.find(one => one.id === this.courseId)?.name ?? ''}</span>
155
- <ox-popup-list align-left nowrap with-search>${courses.map(one => html `<div option value=${one.id}>${one.name}</div>`)}</ox-popup-list>
156
- </ox-select>`
157
- : nothing}
158
- <ox-select .value=${this.stateFilter} aria-label=${t('field.learning-state')} @change=${(e) => this.pick('state', String(e.detail ?? ''))}>
159
- <span slot="label">${states.find(one => one.value === this.stateFilter)?.display}</span>
160
- <ox-popup-list align-left nowrap>${states.map(one => html `<div option value=${one.value}>${one.display}</div>`)}</ox-popup-list>
161
- </ox-select>
162
- ${summary
163
- ? html `<div class="counts" role="status">
164
- <span>${t('text.learning-count-total')} <strong>${summary.total}</strong></span>
165
- <span>${t('text.learning-count-assigned')} <strong>${summary.assigned}</strong></span>
166
- <span>${t('text.learning-count-in-progress')} <strong>${summary.inProgress}</strong></span>
167
- <span>${t('text.learning-count-completed')} <strong>${summary.completed}</strong></span>
168
- <span>${t('text.learning-count-overdue')} <strong>${summary.overdue}</strong></span>
169
- </div>`
170
- : nothing}
171
- </div>
172
- </div>
173
- </ox-grist>`;
174
- }
175
- pick(which, value) {
176
- if (which === 'course' && value !== this.courseId)
177
- this.courseId = value;
178
- if (which === 'state' && value !== this.stateFilter)
179
- this.stateFilter = value;
132
+ async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [], search, signal }) {
133
+ const door = this.manager ? 'enrollmentStatus' : 'teamEnrollmentStatus';
134
+ const response = await client.query({
135
+ query: gql `
136
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $search: String) {
137
+ responses: ${door}(filters: $filters, pagination: $pagination, sortings: $sortings, search: $search) {
138
+ items { ${this.manager ? STATUS_ROW_FIELDS : TEAM_ROW_FIELDS} }
139
+ total
140
+ }
141
+ }
142
+ `,
143
+ variables: { filters, pagination: { page, limit }, sortings, search: search?.trim() || undefined },
144
+ fetchPolicy: 'no-cache',
145
+ context: { fetchOptions: { signal }, queryDeduplication: false }
146
+ });
147
+ if (response.errors?.length)
148
+ throw response.errors[0];
149
+ /* The register action names the course filtered - said again with each read. */
180
150
  this.updateContext();
151
+ return { total: response.data.responses.total, records: response.data.responses.items.map((row) => this.recordOf(row)) };
152
+ }
153
+ /* The state chips' numbers, under the list's other conditions - the chips' own left out, counted by the server. */
154
+ async countHandler({ filters = [], search, signal }) {
155
+ const response = await client.query({
156
+ query: gql `
157
+ query ($filters: [Filter!], $search: String) {
158
+ enrollmentStateCounts(filters: $filters, search: $search) { value count }
159
+ }
160
+ `,
161
+ variables: { filters: filters.filter((one) => one.name !== 'state'), search: search?.trim() || undefined },
162
+ fetchPolicy: 'no-cache',
163
+ context: { fetchOptions: { signal }, queryDeduplication: false }
164
+ });
165
+ if (response.errors?.length)
166
+ throw response.errors[0];
167
+ return { state: Object.fromEntries(response.data.enrollmentStateCounts.map((row) => [row.value, row.count])) };
181
168
  }
182
169
  selectedOpen() {
183
170
  return (this.grist?.selected ?? []).filter((one) => one.open);
@@ -197,18 +184,12 @@ let LearningEnrollmentStatus = class LearningEnrollmentStatus extends localize(i
197
184
  const reason = await OxPrompt.prompt({ title: t('button.learning-cancel-enrollment'), text: t('field.learning-cancel-reason'), multiline: true });
198
185
  if (reason === null || !String(reason).trim())
199
186
  return;
200
- if (await this.canceller.run({ id: one.id, reason: String(reason).trim() }))
187
+ if (await this.canceller.run({ id: one.id, reason: String(reason).trim() })) {
201
188
  notify({ message: t('text.learning-cancelled') });
189
+ this.grist.fetch();
190
+ }
202
191
  }
203
192
  };
204
- __decorate([
205
- state(),
206
- __metadata("design:type", Object)
207
- ], LearningEnrollmentStatus.prototype, "courseId", void 0);
208
- __decorate([
209
- state(),
210
- __metadata("design:type", Object)
211
- ], LearningEnrollmentStatus.prototype, "stateFilter", void 0);
212
193
  __decorate([
213
194
  state(),
214
195
  __metadata("design:type", Boolean)
@@ -219,7 +200,9 @@ __decorate([
219
200
  ], LearningEnrollmentStatus.prototype, "gristConfig", void 0);
220
201
  __decorate([
221
202
  query('ox-grist'),
222
- __metadata("design:type", DataGrist)
203
+ __metadata("design:type", DataGrist
204
+ /* The courses the course filter offers - a manager's only. */
205
+ )
223
206
  ], LearningEnrollmentStatus.prototype, "grist", void 0);
224
207
  LearningEnrollmentStatus = __decorate([
225
208
  customElement('learning-enrollment-status')
@@ -1 +1 @@
1
- {"version":3,"file":"learning-enrollment-status.js","sourceRoot":"","sources":["../../client/pages/learning-enrollment-status.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist/ox-grist.js'\nimport '@operato/input/ox-select.js'\nimport '@operato/popup/ox-popup-list.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, nothing } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport { DataGrist } from '@operato/data-grist'\nimport { MutationController, QueryController } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\nimport { issueDocument } from '@things-factory/pdf/dist-client'\n\nimport { STATUS_ROW_FIELDS } from '../utils/learning-api.js'\nimport { dayOf, minutesOf, STATES, stateWord, t } from '../utils/learning-words.js'\n\n/**\n * 수강 현황 (ADR-0194 decisions 10, 11) - where every enrolment of a course stands: who, their department, the state\n * today, units done, minutes, score and the completion day, with counts on top. A manager reminds and cancels from here\n * and issues the completion register (PDF, through the issued document ledger) or exports it (Excel). A department head\n * sees the people of their departments - the state and days only.\n */\n@customElement('learning-enrollment-status')\nexport class LearningEnrollmentStatus extends localize(i18next)(PageView) {\n static styles = [\n WorkSurfacePageStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n .filters {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n ox-select {\n min-width: 160px;\n }\n .counts {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-small, 4px) var(--spacing-large, 12px);\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 12px);\n }\n .counts strong {\n color: var(--ui-ink);\n font-variant-numeric: tabular-nums;\n }\n `\n ]\n\n @state() private courseId = ''\n @state() private stateFilter = ''\n /* A manager reads everyone; anyone else reads their departments - known once asked. */\n @state() private manager?: boolean\n @state() private gristConfig: any\n @query('ox-grist') private grist!: DataGrist\n\n private courses = new QueryController(this, gql`query { courses(pagination: { page: 1, limit: 500 }) { items { id name } } }`, {\n variables: () => (this.manager ? {} : undefined)\n })\n private rows = new QueryController(\n this,\n gql`query ($filter: EnrollmentStatusFilter) { enrollmentStatus(filter: $filter) { items { ${STATUS_ROW_FIELDS} } summary { total assigned inProgress completed overdue } } }`,\n { variables: () => (this.manager === true ? { filter: this.filter } : undefined), keepPrevious: true }\n )\n private teamRows = new QueryController(\n this,\n gql`query ($filter: EnrollmentStatusFilter) { teamEnrollmentStatus(filter: $filter) { items { id state departmentName units unitsDone user { id name } course { id name } enrollment { id status dueOn completedAt } } summary { total assigned inProgress completed overdue } } }`,\n { variables: () => (this.manager === false ? { filter: this.filter } : undefined), keepPrevious: true }\n )\n private reminder = new MutationController(this, gql`mutation ($ids: [String!]!) { remindEnrollments(ids: $ids) }`)\n private canceller = new MutationController(this, gql`mutation ($id: String!, $reason: String!) { cancelEnrollment(id: $id, reason: $reason) { id } }`, { refetch: () => [this.rows] })\n\n private get filter() {\n return { ...(this.courseId ? { courseId: this.courseId } : {}), ...(this.stateFilter ? { state: this.stateFilter } : {}) }\n }\n\n private get list(): any {\n return (this.manager ? this.rows.data?.enrollmentStatus : this.teamRows.data?.teamEnrollmentStatus) ?? { items: [], summary: null }\n }\n\n get context() {\n const course = this.courseList.find(one => one.id === this.courseId)\n return {\n title: t('title.learning-enrollment-status'),\n ...(this.manager\n ? {\n actions: [\n { title: t('button.learning-remind'), icon: 'notifications_active', action: () => this.remind() },\n { title: t('button.learning-cancel-enrollment'), icon: 'block', action: () => this.cancel() },\n ...(course ? [{ title: t('button.learning-register-pdf'), icon: 'picture_as_pdf', action: () => issueDocument({ kind: 'learning.completion-register', refId: course.id, withAsOf: true }) }] : [])\n ],\n exportable: { name: course ? t('text.learning-register-name', { course: course.name }) : t('title.learning-enrollment-status'), data: () => this.grist.exportRecords() }\n }\n : {})\n }\n }\n\n private get courseList(): { id: string; name: string }[] {\n return this.courses.data?.courses?.items ?? []\n }\n\n async pageUpdated(changes: any, lifecycle: any) {\n if (!this.active) return\n if (changes.params || changes.active) {\n const course = lifecycle?.params?.course\n if (course && course !== this.courseId) this.courseId = course\n }\n }\n\n async pageInitialized() {\n this.manager = !!(await hasPrivilege({ category: 'course', privilege: 'query', domainOwnerGranted: true }))\n this.gristConfig = this.configOf(this.manager)\n this.updateContext()\n }\n\n private recordOf(row: any) {\n const e = row.enrollment ?? {}\n return {\n id: row.id,\n name: row.user?.name ?? '',\n department: row.departmentName ?? '',\n course: row.course?.name ?? '',\n state: row.state,\n dueOn: e.dueOn ?? '',\n units: `${row.unitsDone}/${row.units}`,\n minutes: e.learnedSeconds != null ? minutesOf(e.learnedSeconds) : null,\n score: e.score ?? null,\n completedOn: dayOf(e.completedAt),\n certificateNo: e.certificateNo ?? '',\n open: e.status === 'ASSIGNED' || e.status === 'IN_PROGRESS'\n }\n }\n\n private configOf(manager: boolean) {\n const column = (name: string, header: string, width: any, extra: any = {}) => ({ type: 'string', name, header, width, record: { editable: false, ...extra } })\n return {\n rows: { appendable: false, selectable: manager ? { multiple: true } : false },\n pagination: { infinite: true },\n emptyNote: { title: t('text.learning-no-enrollment') },\n list: { fields: ['name', 'state'], details: ['course', 'dueOn'] },\n columns: [\n ...(manager ? [{ type: 'gutter', gutterName: 'row-selector', multiple: true }] : []),\n column('name', t('field.learning-learner'), 120),\n column('department', t('field.learning-department'), 120),\n column('course', t('field.learning-course-name'), 220),\n column('state', t('field.learning-state'), 100, { renderer: (value: string) => stateWord(value) }),\n column('dueOn', t('field.learning-due-on'), 110),\n column('units', t('field.learning-units-done'), 90, { align: 'right' }),\n ...(manager ? [column('minutes', t('field.learning-minutes'), 120, { align: 'right' }), column('score', t('field.learning-score'), 70, { align: 'right' })] : []),\n column('completedOn', t('field.learning-completed-on'), 110),\n ...(manager ? [column('certificateNo', t('field.learning-certificate-no'), 180)] : [])\n ]\n }\n }\n\n render() {\n const summary = this.list.summary\n const states = [{ value: '', display: t('text.learning-state-all') }, ...STATES.map(value => ({ value, display: stateWord(value) }))]\n const courses = [{ id: '', name: t('text.learning-course-all') }, ...this.courseList]\n const records = this.list.items.map((row: any) => this.recordOf(row))\n return html`<ox-grist class=\"page-fill\" .mode=${isMobileDevice() ? 'CARD' : 'GRID'} .config=${this.gristConfig} .data=${{ records, total: records.length }}>\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n ${this.manager\n ? html`<ox-select .value=${this.courseId} aria-label=${t('field.learning-course-name')} @change=${(e: CustomEvent) => this.pick('course', String(e.detail ?? ''))}>\n <span slot=\"label\">${courses.find(one => one.id === this.courseId)?.name ?? ''}</span>\n <ox-popup-list align-left nowrap with-search>${courses.map(one => html`<div option value=${one.id}>${one.name}</div>`)}</ox-popup-list>\n </ox-select>`\n : nothing}\n <ox-select .value=${this.stateFilter} aria-label=${t('field.learning-state')} @change=${(e: CustomEvent) => this.pick('state', String(e.detail ?? ''))}>\n <span slot=\"label\">${states.find(one => one.value === this.stateFilter)?.display}</span>\n <ox-popup-list align-left nowrap>${states.map(one => html`<div option value=${one.value}>${one.display}</div>`)}</ox-popup-list>\n </ox-select>\n ${summary\n ? html`<div class=\"counts\" role=\"status\">\n <span>${t('text.learning-count-total')} <strong>${summary.total}</strong></span>\n <span>${t('text.learning-count-assigned')} <strong>${summary.assigned}</strong></span>\n <span>${t('text.learning-count-in-progress')} <strong>${summary.inProgress}</strong></span>\n <span>${t('text.learning-count-completed')} <strong>${summary.completed}</strong></span>\n <span>${t('text.learning-count-overdue')} <strong>${summary.overdue}</strong></span>\n </div>`\n : nothing}\n </div>\n </div>\n </ox-grist>`\n }\n\n private pick(which: 'course' | 'state', value: string) {\n if (which === 'course' && value !== this.courseId) this.courseId = value\n if (which === 'state' && value !== this.stateFilter) this.stateFilter = value\n this.updateContext()\n }\n\n private selectedOpen(): any[] {\n return (this.grist?.selected ?? []).filter((one: any) => one.open)\n }\n\n private async remind() {\n const ids = this.selectedOpen().map(one => one.id)\n if (!ids.length) return notify({ level: 'warn', message: t('text.learning-pick-open') })\n const done = await this.reminder.run({ ids })\n if (done) notify({ message: t('text.learning-reminded', { count: done.remindEnrollments }) })\n }\n\n private async cancel() {\n const [one] = this.grist?.selected ?? []\n if (!one) return notify({ level: 'warn', message: t('text.learning-pick-one') })\n const reason = await OxPrompt.prompt({ title: t('button.learning-cancel-enrollment'), text: t('field.learning-cancel-reason'), multiline: true })\n if (reason === null || !String(reason).trim()) return\n if (await this.canceller.run({ id: one.id, reason: String(reason).trim() })) notify({ message: t('text.learning-cancelled') })\n }\n}\n\n"]}
1
+ {"version":3,"file":"learning-enrollment-status.js","sourceRoot":"","sources":["../../client/pages/learning-enrollment-status.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist/ox-filters-form.js'\nimport '@operato/data-grist/ox-grist.js'\n\nimport gql from 'graphql-tag'\nimport { html } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, MutationController, QUERY_ANSWERED, QueryController } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\nimport { issueDocument } from '@things-factory/pdf/dist-client'\n\nimport { STATUS_ROW_FIELDS } from '../utils/learning-api.js'\nimport { dayOf, minutesOf, STATES, stateWord, t } from '../utils/learning-words.js'\n\nconst TEAM_ROW_FIELDS = 'id state departmentName units unitsDone user { id name } course { id name } enrollment { id status dueOn completedAt }'\n\n/**\n * 수강 현황 (ADR-0194 decisions 10, 11) - where enrolments stand: who, their department, the state today, units done,\n * minutes, score and the completion day. The conditions - course, state with its counts, search, sort, page - are the\n * grid's, kept in the address, and read by the server. A manager reminds and cancels from here and issues the completion\n * register of the course filtered (PDF, through the issued document ledger) or exports the list (Excel). A department\n * head sees the people of their departments - the state and days only.\n */\n@customElement('learning-enrollment-status')\nexport class LearningEnrollmentStatus extends localize(i18next)(PageView) {\n static styles = [WorkSurfacePageStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles]\n\n /* A manager reads everyone; anyone else reads their departments - known once asked. */\n @state() private manager?: boolean\n @state() private gristConfig: any\n @query('ox-grist') private grist!: DataGrist\n\n /* The courses the course filter offers - a manager's only. */\n private courses = new QueryController(this, gql`query { courses(pagination: { page: 1, limit: 500 }) { items { id name } } }`, {\n variables: () => (this.manager ? {} : undefined)\n })\n private reminder = new MutationController(this, gql`mutation ($ids: [String!]!) { remindEnrollments(ids: $ids) }`)\n private canceller = new MutationController(this, gql`mutation ($id: String!, $reason: String!) { cancelEnrollment(id: $id, reason: $reason) { id } }`)\n\n /** The course the grid's `course` filter names - the completion register is of it. */\n private get filteredCourse(): { id: string; name: string } | undefined {\n const id = this.grist?.filters?.find((one: any) => one.name === 'course')?.value\n return id ? (this.courses.data?.courses?.items ?? []).find((one: any) => one.id === id) : undefined\n }\n\n get context() {\n const course = this.filteredCourse\n return {\n title: t('title.learning-enrollment-status'),\n search: { handler: (search: string) => (this.grist.searchText = search), value: this.grist?.searchText || '' },\n filter: { handler: () => this.grist.toggleHeadroom() },\n ...(this.manager\n ? {\n actions: [\n { title: t('button.learning-remind'), icon: 'notifications_active', action: () => this.remind() },\n { title: t('button.learning-cancel-enrollment'), icon: 'block', action: () => this.cancel() },\n ...(course ? [{ title: t('button.learning-register-pdf'), icon: 'picture_as_pdf', action: () => issueDocument({ kind: 'learning.completion-register', refId: course.id, withAsOf: true }) }] : [])\n ],\n exportable: { name: course ? t('text.learning-register-name', { course: course.name }) : t('title.learning-enrollment-status'), data: () => this.grist.exportRecords() }\n }\n : {})\n }\n }\n\n render() {\n return this.gristConfig\n ? html`<ox-grist\n class=\"page-fill\"\n search-mode=\"term\"\n .mode=${isMobileDevice() ? 'CARD' : 'GRID'}\n .config=${this.gristConfig}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .countHandler=${this.countHandler.bind(this)}\n ?url-params-sensitive=${this.active}\n >\n <div slot=\"headroom\" class=\"header\">\n <div class=\"filters\"><ox-filters-form without-search></ox-filters-form></div>\n </div>\n </ox-grist>`\n : html``\n }\n\n async pageInitialized() {\n this.manager = !!(await hasPrivilege({ category: 'course', privilege: 'query', domainOwnerGranted: true }))\n this.gristConfig = this.configOf(this.manager)\n this.updateContext()\n }\n\n /** The courses for the course filter, once the read of them has answered. */\n private async courseOptions(): Promise<{ value: string; display: string }[]> {\n if (this.courses.loading) await new Promise(resolve => this.addEventListener(QUERY_ANSWERED, resolve, { once: true }))\n return [{ value: '', display: t('text.learning-course-all') }, ...(this.courses.data?.courses?.items ?? []).map((one: any) => ({ value: one.id, display: one.name }))]\n }\n\n private recordOf(row: any) {\n const e = row.enrollment ?? {}\n return {\n id: row.id,\n name: row.user?.name ?? '',\n department: row.departmentName ?? '',\n course: row.course?.name ?? '',\n state: row.state,\n dueOn: e.dueOn ?? '',\n units: `${row.unitsDone}/${row.units}`,\n minutes: e.learnedSeconds != null ? minutesOf(e.learnedSeconds) : null,\n score: e.score ?? null,\n completedOn: dayOf(e.completedAt),\n certificateNo: e.certificateNo ?? '',\n open: e.status === 'ASSIGNED' || e.status === 'IN_PROGRESS'\n }\n }\n\n private configOf(manager: boolean) {\n const column = (name: string, header: string, width: any, extra: any = {}) => ({ type: 'string', name, header, width, sortable: false, ...extra, record: { editable: false, ...extra.record } })\n return {\n rows: { appendable: false, selectable: manager ? { multiple: true } : false },\n pagination: { limit: 100, pages: [50, 100, 500] },\n emptyNote: { title: t('text.learning-no-enrollment') },\n list: { fields: ['name', 'state'], details: ['course', 'dueOn'] },\n columns: [\n ...(manager ? [{ type: 'gutter', gutterName: 'row-selector', multiple: true }] : []),\n column('name', t('field.learning-learner'), 120),\n column('department', t('field.learning-department'), 120),\n column('course', t('field.learning-course-name'), 220, manager ? { filter: { type: 'select', operator: 'eq', options: () => this.courseOptions() } } : {}),\n column('state', t('field.learning-state'), 100, {\n record: { renderer: (value: string) => stateWord(value) },\n /* Chips with their counts; none pressed is every state. */\n filter: { type: 'select-buttons', operator: 'in', options: STATES.map(value => ({ value, display: stateWord(value) })) }\n }),\n column('dueOn', t('field.learning-due-on'), 110, { sortable: true }),\n column('units', t('field.learning-units-done'), 90, { record: { align: 'right' } }),\n ...(manager ? [column('minutes', t('field.learning-minutes'), 120, { record: { align: 'right' } }), column('score', t('field.learning-score'), 70, { record: { align: 'right' } })] : []),\n column('completedOn', t('field.learning-completed-on'), 110),\n ...(manager ? [column('certificateNo', t('field.learning-certificate-no'), 180)] : [])\n ],\n sorters: [{ name: 'dueOn' }]\n }\n }\n\n async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [], search, signal }: FetchOption) {\n const door = this.manager ? 'enrollmentStatus' : 'teamEnrollmentStatus'\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $search: String) {\n responses: ${door}(filters: $filters, pagination: $pagination, sortings: $sortings, search: $search) {\n items { ${this.manager ? STATUS_ROW_FIELDS : TEAM_ROW_FIELDS} }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings, search: search?.trim() || undefined },\n fetchPolicy: 'no-cache',\n context: { fetchOptions: { signal }, queryDeduplication: false }\n })\n if (response.errors?.length) throw response.errors[0]\n /* The register action names the course filtered - said again with each read. */\n this.updateContext()\n return { total: response.data.responses.total, records: response.data.responses.items.map((row: any) => this.recordOf(row)) }\n }\n\n /* The state chips' numbers, under the list's other conditions - the chips' own left out, counted by the server. */\n async countHandler({ filters = [], search, signal }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $search: String) {\n enrollmentStateCounts(filters: $filters, search: $search) { value count }\n }\n `,\n variables: { filters: filters.filter((one: any) => one.name !== 'state'), search: search?.trim() || undefined },\n fetchPolicy: 'no-cache',\n context: { fetchOptions: { signal }, queryDeduplication: false }\n })\n if (response.errors?.length) throw response.errors[0]\n return { state: Object.fromEntries(response.data.enrollmentStateCounts.map((row: { value: string; count: number }) => [row.value, row.count])) }\n }\n\n private selectedOpen(): any[] {\n return (this.grist?.selected ?? []).filter((one: any) => one.open)\n }\n\n private async remind() {\n const ids = this.selectedOpen().map(one => one.id)\n if (!ids.length) return notify({ level: 'warn', message: t('text.learning-pick-open') })\n const done = await this.reminder.run({ ids })\n if (done) notify({ message: t('text.learning-reminded', { count: done.remindEnrollments }) })\n }\n\n private async cancel() {\n const [one] = this.grist?.selected ?? []\n if (!one) return notify({ level: 'warn', message: t('text.learning-pick-one') })\n const reason = await OxPrompt.prompt({ title: t('button.learning-cancel-enrollment'), text: t('field.learning-cancel-reason'), multiline: true })\n if (reason === null || !String(reason).trim()) return\n if (await this.canceller.run({ id: one.id, reason: String(reason).trim() })) {\n notify({ message: t('text.learning-cancelled') })\n this.grist.fetch()\n }\n }\n}\n\n"]}
@@ -20,12 +20,16 @@ export declare class LearningView extends LearningView_base {
20
20
  private result?;
21
21
  private agreed;
22
22
  private tickError;
23
+ private switching;
23
24
  private read;
25
+ private savedAt?;
24
26
  private tickSave;
25
27
  private quizStart;
26
28
  private quizSubmit;
27
29
  private acknowledgement;
28
30
  private ticks;
31
+ /** Where each lesson starts - the server's last answer for its unit, else what the page read (LessonStarts). */
32
+ private starts;
29
33
  get enrollment(): any;
30
34
  get context(): {
31
35
  subject?: {
@@ -43,8 +47,15 @@ export declare class LearningView extends LearningView_base {
43
47
  private renderLesson;
44
48
  private renderQuiz;
45
49
  private renderAcknowledgement;
50
+ /**
51
+ * Opens a unit. The lesson shown leaves first and its last tick is answered, so the unit opened - the same one coming
52
+ * back too - starts from what the server counted, not from what the page read when it opened (2026-11-08).
53
+ */
46
54
  private open;
55
+ /** Keeps the server's answer for a unit - the next time it opens, it starts there. */
56
+ private keep;
47
57
  private tick;
58
+ private tickLeaving;
48
59
  private startQuiz;
49
60
  private submit;
50
61
  private errorText;
@@ -15,8 +15,14 @@ import { i18next, localize } from '@operato/i18n';
15
15
  import { PageView } from '@operato/shell';
16
16
  import { DocumentPageStyles } from '@operato/styles';
17
17
  import { issueDocument } from '@things-factory/pdf/dist-client';
18
- import { LEARNING_FIELDS, unitsDoneOf } from '../utils/learning-api.js';
18
+ import { LEARNING_FIELDS, LessonStarts, pageLookedOf, PROGRESS_FIELDS, resumeUnitId, unitsDoneOf } from '../utils/learning-api.js';
19
19
  import { dayOf, minutesOf, missingWord, stateWord, t, unitKindWord } from '../utils/learning-words.js';
20
+ const TICK_MUTATION = `mutation ($enrollmentId: String!, $unitId: String!, $seconds: Int!, $position: Int!) {
21
+ recordLearningTick(enrollmentId: $enrollmentId, unitId: $unitId, seconds: $seconds, position: $position) {
22
+ enrollment { id status }
23
+ progress { ${PROGRESS_FIELDS} }
24
+ }
25
+ }`;
20
26
  /**
21
27
  * 과정 화면 (ADR-0194 decisions 3-9) - one enrolment: the units in order with the one open played by `ox-lesson`, whose
22
28
  * ticks go to the server as they come (the server counts the time); the quiz drawn by the server and scored there; the
@@ -29,16 +35,13 @@ let LearningView = class LearningView extends localize(i18next)(PageView) {
29
35
  this.unitId = '';
30
36
  this.agreed = false;
31
37
  this.tickError = '';
38
+ /* While one lesson leaves for another, nothing is shown - the leaving one's last tick is answered first. */
39
+ this.switching = false;
32
40
  this.read = new QueryController(this, gql `query ($id: String!) { myEnrollment(id: $id) { ${LEARNING_FIELDS} } }`, {
33
41
  variables: () => (this.enrollmentId ? { id: this.enrollmentId } : undefined),
34
42
  keepPrevious: true
35
43
  });
36
- this.tickSave = new MutationController(this, gql `mutation ($enrollmentId: String!, $unitId: String!, $seconds: Int!, $position: Int!) {
37
- recordLearningTick(enrollmentId: $enrollmentId, unitId: $unitId, seconds: $seconds, position: $position) {
38
- enrollment { id status }
39
- progress { learnedSeconds watchedSeconds positionSeconds doneAt }
40
- }
41
- }`, { showsError: true });
44
+ this.tickSave = new MutationController(this, gql(TICK_MUTATION), { showsError: true });
42
45
  this.quizStart = new MutationController(this, gql `mutation ($enrollmentId: String!) { startQuizAttempt(enrollmentId: $enrollmentId) { attemptId questions { id kind text choices { id text } } } }`, {
43
46
  showsError: true
44
47
  });
@@ -47,8 +50,13 @@ let LearningView = class LearningView extends localize(i18next)(PageView) {
47
50
  showsError: true
48
51
  });
49
52
  this.acknowledgement = new MutationController(this, gql `mutation ($id: String!) { acknowledgeEnrollment(id: $id) { id status } }`, { refetch: () => [this.read], showsError: true });
50
- /* Ticks go one after another - a stop right after a start is not dropped while the start is out. */
53
+ /*
54
+ * Ticks go one after another - the leaving one too - so the server takes them in the order they were looked at: a
55
+ * stop right after a start is not dropped while the start is out, and a page left on is never overtaken by an older one.
56
+ */
51
57
  this.ticks = Promise.resolve();
58
+ /** Where each lesson starts - the server's last answer for its unit, else what the page read (LessonStarts). */
59
+ this.starts = new LessonStarts();
52
60
  }
53
61
  static { this.styles = [
54
62
  DocumentPageStyles,
@@ -154,6 +162,12 @@ let LearningView = class LearningView extends localize(i18next)(PageView) {
154
162
  margin: 0;
155
163
  line-height: 1.6;
156
164
  }
165
+ .saved {
166
+ margin: 0;
167
+ min-height: 1.4em;
168
+ color: var(--md-sys-color-on-surface-variant);
169
+ font-size: var(--ui-t-small, 12px);
170
+ }
157
171
  .result {
158
172
  margin: 0;
159
173
  font-weight: 600;
@@ -181,9 +195,8 @@ let LearningView = class LearningView extends localize(i18next)(PageView) {
181
195
  updated() {
182
196
  const one = this.enrollment;
183
197
  if (one && !this.unitId && one.units?.length) {
184
- /* Where the learner left off: the first unit that counts and is not done, else the first. */
185
- const next = one.units.find((row) => row.counts && !row.progress?.doneAt) ?? one.units[0];
186
- this.unitId = next.unit.id;
198
+ /* Where the learner was - the unit learned last (resumeUnitId). */
199
+ this.unitId = resumeUnitId(one);
187
200
  }
188
201
  }
189
202
  render() {
@@ -242,7 +255,9 @@ let LearningView = class LearningView extends localize(i18next)(PageView) {
242
255
  }
243
256
  renderLesson(one, row) {
244
257
  const unit = row.unit;
245
- const progress = row.progress ?? {};
258
+ if (this.switching)
259
+ return html `<h3>${unit.name}</h3>`;
260
+ const progress = unit.kind === 'LINK' ? {} : this.starts.startOf(one.id, unit.id, row.progress);
246
261
  if (unit.kind === 'LINK')
247
262
  return html `<h3>${unit.name}</h3>
248
263
  <p class="page-section-note">${t('text.learning-link-not-counted')}</p>
@@ -258,14 +273,19 @@ let LearningView = class LearningView extends localize(i18next)(PageView) {
258
273
  .src=${unit.file?.fullpath}
259
274
  .body=${unit.body ?? ''}
260
275
  .label=${unit.name}
261
- .watched=${progress.watchedSeconds ?? 0}
262
- .position=${progress.positionSeconds ?? 0}
276
+ .watched=${unit.kind === 'PDF' ? (progress.pagesRead ?? 0) : (progress.watchedSeconds ?? 0)}
277
+ .position=${unit.kind === 'PDF' ? (progress.pageAt ?? 0) : (progress.positionSeconds ?? 0)}
278
+ .pages=${unit.pageCount ?? 0}
279
+ .pageSeconds=${unit.secondsPerPage ?? undefined}
280
+ .pageLooked=${unit.kind === 'PDF' ? pageLookedOf(progress) : 0}
263
281
  .requiredSeconds=${unit.minSeconds ?? 60}
264
282
  .learnedSeconds=${progress.learnedSeconds ?? 0}
265
- .done=${!!progress.doneAt || one.status === 'COMPLETED'}
283
+ .done=${!!row.progress?.doneAt || !!progress.doneAt || one.status === 'COMPLETED'}
266
284
  @lesson-tick=${(e) => this.tick(one.id, unit.id, e.detail)}
267
285
  ></ox-lesson>`)}
268
- ${this.tickError ? html `<div class="error" role="alert">${this.tickError}</div>` : nothing}`;
286
+ ${this.tickError
287
+ ? html `<div class="error" role="alert">${this.tickError}</div>`
288
+ : html `<p class="saved" role="status">${this.savedAt ? t('text.learning-saved-at', { time: this.savedAt.toLocaleTimeString(i18next.language) }) : ''}</p>`}`;
269
289
  }
270
290
  renderQuiz(one) {
271
291
  const completed = one.status === 'COMPLETED';
@@ -301,26 +321,73 @@ let LearningView = class LearningView extends localize(i18next)(PageView) {
301
321
  ${this.acknowledgement.error ? html `<div class="error" role="alert">${this.errorText(this.acknowledgement.error)}</div>` : nothing}`}
302
322
  </section>`;
303
323
  }
304
- open(unitId) {
305
- this.unitId = unitId;
324
+ /**
325
+ * Opens a unit. The lesson shown leaves first and its last tick is answered, so the unit opened - the same one coming
326
+ * back too - starts from what the server counted, not from what the page read when it opened (2026-11-08).
327
+ */
328
+ async open(unitId) {
329
+ if (unitId === this.unitId || this.switching)
330
+ return;
306
331
  this.tickError = '';
332
+ this.switching = true;
333
+ await this.updateComplete;
334
+ await this.ticks;
335
+ this.starts.reopen();
336
+ this.unitId = unitId;
337
+ this.switching = false;
338
+ }
339
+ /** Keeps the server's answer for a unit - the next time it opens, it starts there. */
340
+ keep(enrollmentId, unitId, answer) {
341
+ this.starts.keep(enrollmentId, unitId, answer?.progress);
342
+ this.savedAt = new Date();
307
343
  }
308
344
  tick(enrollmentId, unitId, detail) {
345
+ const variables = { enrollmentId, unitId, seconds: Math.round(detail.seconds), position: Math.floor(detail.position) };
346
+ /*
347
+ * Leaving - the tab hidden, closed or reloaded - the last piece goes out on a request the browser finishes after the
348
+ * page is gone (keepalive). Through the usual queue it was dropped with the page, and up to a tick was lost.
349
+ */
350
+ if (document.visibilityState === 'hidden') {
351
+ this.ticks = this.ticks.then(() => this.tickLeaving(variables));
352
+ return;
353
+ }
309
354
  this.ticks = this.ticks.then(async () => {
310
355
  const before = this.enrollment;
311
- const answer = await this.tickSave.run({ enrollmentId, unitId, seconds: Math.round(detail.seconds), position: Math.floor(detail.position) });
356
+ const answer = await this.tickSave.run(variables);
312
357
  if (!answer) {
313
358
  this.tickError = this.errorText(this.tickSave.error);
314
359
  await this.read.refetch();
315
360
  return;
316
361
  }
317
362
  this.tickError = '';
363
+ this.keep(enrollmentId, unitId, answer.recordLearningTick);
318
364
  const row = before?.units?.find((one) => one.unit.id === unitId);
319
365
  const newlyDone = !!answer.recordLearningTick.progress.doneAt && !row?.progress?.doneAt;
320
366
  if (newlyDone || answer.recordLearningTick.enrollment.status !== before?.status)
321
367
  await this.read.refetch();
322
368
  });
323
369
  }
370
+ /*
371
+ * The leaving tick goes after the ticks before it, on a request the browser finishes after the page is gone
372
+ * (keepalive). Only when the page is closed while an earlier tick is still out is it not sent - the order is kept.
373
+ */
374
+ async tickLeaving(variables) {
375
+ await fetch('/graphql', {
376
+ method: 'POST',
377
+ keepalive: true,
378
+ credentials: 'include',
379
+ headers: { 'Content-Type': 'application/json', 'Apollo-Require-Preflight': 'true' },
380
+ body: JSON.stringify({ query: TICK_MUTATION, variables })
381
+ })
382
+ .then(response => response.json())
383
+ .then(answer => {
384
+ if (answer?.data?.recordLearningTick)
385
+ this.keep(variables.enrollmentId, variables.unitId, answer.data.recordLearningTick);
386
+ else
387
+ this.tickError = answer?.errors?.[0]?.message ?? '';
388
+ })
389
+ .catch(error => (this.tickError = error?.message ?? String(error)));
390
+ }
324
391
  async startQuiz(enrollmentId) {
325
392
  this.result = undefined;
326
393
  const answer = await this.quizStart.run({ enrollmentId });
@@ -364,6 +431,14 @@ __decorate([
364
431
  state(),
365
432
  __metadata("design:type", Object)
366
433
  ], LearningView.prototype, "tickError", void 0);
434
+ __decorate([
435
+ state(),
436
+ __metadata("design:type", Object)
437
+ ], LearningView.prototype, "switching", void 0);
438
+ __decorate([
439
+ state(),
440
+ __metadata("design:type", Date)
441
+ ], LearningView.prototype, "savedAt", void 0);
367
442
  LearningView = __decorate([
368
443
  customElement('learning-view')
369
444
  ], LearningView);