@things-factory/work-shift 10.1.248 → 10.1.249

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 (87) hide show
  1. package/dist-client/bootstrap.d.ts +1 -0
  2. package/dist-client/bootstrap.js +6 -0
  3. package/dist-client/bootstrap.js.map +1 -0
  4. package/dist-client/index.d.ts +0 -0
  5. package/dist-client/index.js +2 -0
  6. package/dist-client/index.js.map +1 -0
  7. package/dist-client/pages/work-shift.d.ts +105 -0
  8. package/dist-client/pages/work-shift.js +1144 -0
  9. package/dist-client/pages/work-shift.js.map +1 -0
  10. package/dist-client/route.d.ts +1 -0
  11. package/dist-client/route.js +8 -0
  12. package/dist-client/route.js.map +1 -0
  13. package/dist-client/shift-coverage.d.ts +102 -0
  14. package/dist-client/shift-coverage.js +196 -0
  15. package/dist-client/shift-coverage.js.map +1 -0
  16. package/dist-client/shift-editing.d.ts +80 -0
  17. package/dist-client/shift-editing.js +212 -0
  18. package/dist-client/shift-editing.js.map +1 -0
  19. package/dist-client/shift-revisions.d.ts +73 -0
  20. package/dist-client/shift-revisions.js +64 -0
  21. package/dist-client/shift-revisions.js.map +1 -0
  22. package/dist-client/tsconfig.tsbuildinfo +1 -0
  23. package/dist-server/controllers/index.d.ts +2 -0
  24. package/dist-server/controllers/index.js +6 -0
  25. package/dist-server/controllers/index.js.map +1 -0
  26. package/dist-server/controllers/work-shift-range.d.ts +28 -0
  27. package/dist-server/controllers/work-shift-range.js +65 -0
  28. package/dist-server/controllers/work-shift-range.js.map +1 -0
  29. package/dist-server/controllers/work-shift-schedule.d.ts +5 -0
  30. package/dist-server/controllers/work-shift-schedule.js +60 -0
  31. package/dist-server/controllers/work-shift-schedule.js.map +1 -0
  32. package/dist-server/index.d.ts +2 -0
  33. package/dist-server/index.js +19 -0
  34. package/dist-server/index.js.map +1 -0
  35. package/dist-server/service/index.d.ts +13 -0
  36. package/dist-server/service/index.js +33 -0
  37. package/dist-server/service/index.js.map +1 -0
  38. package/dist-server/service/work-shift/days-of-week.d.ts +30 -0
  39. package/dist-server/service/work-shift/days-of-week.js +74 -0
  40. package/dist-server/service/work-shift/days-of-week.js.map +1 -0
  41. package/dist-server/service/work-shift/index.d.ts +9 -0
  42. package/dist-server/service/work-shift/index.js +20 -0
  43. package/dist-server/service/work-shift/index.js.map +1 -0
  44. package/dist-server/service/work-shift/shift-occurrence.d.ts +19 -0
  45. package/dist-server/service/work-shift/shift-occurrence.js +34 -0
  46. package/dist-server/service/work-shift/shift-occurrence.js.map +1 -0
  47. package/dist-server/service/work-shift/work-shift-arrangement.d.ts +10 -0
  48. package/dist-server/service/work-shift/work-shift-arrangement.js +40 -0
  49. package/dist-server/service/work-shift/work-shift-arrangement.js.map +1 -0
  50. package/dist-server/service/work-shift/work-shift-baseline.d.ts +23 -0
  51. package/dist-server/service/work-shift/work-shift-baseline.js +55 -0
  52. package/dist-server/service/work-shift/work-shift-baseline.js.map +1 -0
  53. package/dist-server/service/work-shift/work-shift-change-approval.d.ts +14 -0
  54. package/dist-server/service/work-shift/work-shift-change-approval.js +64 -0
  55. package/dist-server/service/work-shift/work-shift-change-approval.js.map +1 -0
  56. package/dist-server/service/work-shift/work-shift-content.d.ts +18 -0
  57. package/dist-server/service/work-shift/work-shift-content.js +62 -0
  58. package/dist-server/service/work-shift/work-shift-content.js.map +1 -0
  59. package/dist-server/service/work-shift/work-shift-lifecycle-resolver.d.ts +68 -0
  60. package/dist-server/service/work-shift/work-shift-lifecycle-resolver.js +89 -0
  61. package/dist-server/service/work-shift/work-shift-lifecycle-resolver.js.map +1 -0
  62. package/dist-server/service/work-shift/work-shift-lifecycle.d.ts +229 -0
  63. package/dist-server/service/work-shift/work-shift-lifecycle.js +672 -0
  64. package/dist-server/service/work-shift/work-shift-lifecycle.js.map +1 -0
  65. package/dist-server/service/work-shift/work-shift-mutation.d.ts +16 -0
  66. package/dist-server/service/work-shift/work-shift-mutation.js +77 -0
  67. package/dist-server/service/work-shift/work-shift-mutation.js.map +1 -0
  68. package/dist-server/service/work-shift/work-shift-query.d.ts +14 -0
  69. package/dist-server/service/work-shift/work-shift-query.js +116 -0
  70. package/dist-server/service/work-shift/work-shift-query.js.map +1 -0
  71. package/dist-server/service/work-shift/work-shift-revision-service.d.ts +32 -0
  72. package/dist-server/service/work-shift/work-shift-revision-service.js +89 -0
  73. package/dist-server/service/work-shift/work-shift-revision-service.js.map +1 -0
  74. package/dist-server/service/work-shift/work-shift-revision.d.ts +85 -0
  75. package/dist-server/service/work-shift/work-shift-revision.js +337 -0
  76. package/dist-server/service/work-shift/work-shift-revision.js.map +1 -0
  77. package/dist-server/service/work-shift/work-shift-selection.d.ts +50 -0
  78. package/dist-server/service/work-shift/work-shift-selection.js +65 -0
  79. package/dist-server/service/work-shift/work-shift-selection.js.map +1 -0
  80. package/dist-server/service/work-shift/work-shift-type.d.ts +26 -0
  81. package/dist-server/service/work-shift/work-shift-type.js +77 -0
  82. package/dist-server/service/work-shift/work-shift-type.js.map +1 -0
  83. package/dist-server/service/work-shift/work-shift.d.ts +56 -0
  84. package/dist-server/service/work-shift/work-shift.js +109 -0
  85. package/dist-server/service/work-shift/work-shift.js.map +1 -0
  86. package/dist-server/tsconfig.tsbuildinfo +1 -0
  87. package/package.json +3 -3
@@ -0,0 +1,1144 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@operato/input/ox-input-select-buttons.js';
3
+ import '@operato/list-state/ox-list-state.js';
4
+ import '@operato/list-state/ox-page-state.js';
5
+ import gql from 'graphql-tag';
6
+ import { css, html, nothing } from 'lit';
7
+ import { customElement, state } from 'lit/decorators.js';
8
+ import { client } from '@operato/graphql';
9
+ import { i18next, localize } from '@operato/i18n';
10
+ import { notify } from '@operato/layout';
11
+ import { OxPrompt } from '@operato/popup';
12
+ import { PageView } from '@operato/shell';
13
+ import { ButtonStyles, DocumentPageStyles, FormFieldStyles, ScrollbarStyles } from '@operato/styles';
14
+ import { revisionText as rt, revisionDate } from '@things-factory/ops-master/dist-client/pages/master/revision-comparison.js';
15
+ import { localDateTime, isoFromLocal } from '@things-factory/ops-master/dist-client/pages/master/revision-view.js';
16
+ import { clockOf, coverageProblems, timelineOf } from '../shift-coverage';
17
+ import { daysLabel, daysOf, daysValue, draftIssues, groupOverlaps, lengthOf, nextShift, settle, shiftsContent, weekGroups, WEEK } from '../shift-editing';
18
+ import { isBaseline, placeRevisions, reasonOf, timeRange } from '../shift-revisions';
19
+ const DAY_MINUTES = 24 * 60;
20
+ const t = (key, params = {}) => String(i18next.t(key, params));
21
+ /**
22
+ * 근무 교대 — the shift arrangement in force, what is about to change, and what it was before.
23
+ *
24
+ * One arrangement is in force at a time. Changing it means writing a whole new arrangement with a reason and
25
+ * the moment it takes effect; where the site requires approval, the change goes through ops-master's approval
26
+ * and takes effect once approved. Work already recorded keeps the shift it was recorded under.
27
+ *
28
+ * The rules this screen applies - weekday groups, the end day of a shift, what blocks a save, where each
29
+ * revision belongs - live in shift-editing.ts, shift-revisions.ts and shift-coverage.ts, with tests.
30
+ */
31
+ let WorkShift = class WorkShift extends localize(i18next)(PageView) {
32
+ constructor() {
33
+ super(...arguments);
34
+ this.loading = false;
35
+ this.error = null;
36
+ this.approvals = {};
37
+ this.dirty = false;
38
+ this.busy = false;
39
+ this.saveError = '';
40
+ /** A save was tried: from then on every blocking problem is shown, not only the ones a person touched. */
41
+ this.tried = false;
42
+ /* ── pieces ────────────────────────────────────────────────────────── */
43
+ this.dayWord = (offset) => rt('day-offset-' + offset);
44
+ }
45
+ static { this.styles = [
46
+ FormFieldStyles,
47
+ ButtonStyles,
48
+ DocumentPageStyles,
49
+ ScrollbarStyles,
50
+ css `
51
+ .arrangement {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--spacing-large);
55
+ }
56
+
57
+ /* ── the day bar ─────────────────────────────────────────────── */
58
+ .day-groups {
59
+ display: grid;
60
+ grid-template-columns: max-content minmax(0, 1fr);
61
+ gap: var(--spacing-large) var(--spacing-large);
62
+ align-items: start;
63
+ }
64
+ .day-label {
65
+ padding-top: 7px;
66
+ color: var(--ui-muted);
67
+ font-size: var(--ui-t-small);
68
+ font-weight: 600;
69
+ white-space: nowrap;
70
+ }
71
+ .day-none {
72
+ padding-top: 7px;
73
+ color: var(--ui-muted);
74
+ }
75
+ .bar {
76
+ position: relative;
77
+ height: 32px;
78
+ border-radius: var(--md-sys-shape-corner-small, 4px);
79
+ background: var(--md-sys-color-surface-container-low);
80
+ overflow: hidden;
81
+ }
82
+ .piece {
83
+ position: absolute;
84
+ inset-block: 0;
85
+ box-sizing: border-box;
86
+ display: flex;
87
+ align-items: center;
88
+ gap: var(--spacing-small);
89
+ min-width: 0;
90
+ padding-inline: var(--spacing-medium);
91
+ overflow: hidden;
92
+ white-space: nowrap;
93
+ font-size: var(--ui-t-small);
94
+ }
95
+ .piece[data-kind='shift'] {
96
+ border-inline-start: 3px solid var(--ui-accent, var(--md-sys-color-primary));
97
+ border-inline-end: 1px solid var(--md-sys-color-surface-container-lowest);
98
+ background: color-mix(in srgb, var(--ui-accent, var(--md-sys-color-primary)) 18%, var(--md-sys-color-surface-container-lowest));
99
+ color: var(--ui-ink);
100
+ }
101
+ .piece[data-kind='shift'] strong {
102
+ font-weight: 600;
103
+ overflow: hidden;
104
+ text-overflow: ellipsis;
105
+ }
106
+ .piece .time {
107
+ color: var(--ui-muted);
108
+ font-variant-numeric: tabular-nums;
109
+ }
110
+ .piece[data-kind='gap'] {
111
+ background: repeating-linear-gradient(
112
+ -45deg,
113
+ transparent 0 5px,
114
+ color-mix(in srgb, var(--ui-warn) 35%, transparent) 5px 6px
115
+ );
116
+ }
117
+ .piece[data-kind='overlap'] {
118
+ background: repeating-linear-gradient(
119
+ -45deg,
120
+ color-mix(in srgb, var(--ui-crit) 30%, transparent) 0 4px,
121
+ transparent 4px 7px
122
+ );
123
+ }
124
+ .midnight {
125
+ position: absolute;
126
+ inset-block: 0;
127
+ border-inline-start: 1px dashed var(--ui-muted);
128
+ }
129
+ .axis {
130
+ position: relative;
131
+ height: 16px;
132
+ margin-top: 2px;
133
+ color: var(--ui-muted);
134
+ font-size: var(--ui-t-micro);
135
+ font-variant-numeric: tabular-nums;
136
+ }
137
+ .axis span {
138
+ position: absolute;
139
+ white-space: nowrap;
140
+ transform: translateX(-50%);
141
+ }
142
+ .axis span:first-child {
143
+ transform: none;
144
+ }
145
+ .axis span:last-child {
146
+ transform: translateX(-100%);
147
+ }
148
+
149
+ .findings {
150
+ display: grid;
151
+ gap: var(--spacing-small);
152
+ margin: 0;
153
+ padding: 0;
154
+ list-style: none;
155
+ }
156
+ .findings li {
157
+ display: flex;
158
+ gap: var(--spacing-medium);
159
+ font-variant-numeric: tabular-nums;
160
+ }
161
+ .findings li::before {
162
+ content: '';
163
+ flex: none;
164
+ width: 3px;
165
+ border-radius: 2px;
166
+ background: var(--ui-warn);
167
+ }
168
+ .findings li[data-kind='overlap']::before,
169
+ .findings li[data-kind='error']::before {
170
+ background: var(--ui-crit);
171
+ }
172
+ .findings .why {
173
+ display: block;
174
+ color: var(--ui-muted);
175
+ font-size: var(--ui-t-small);
176
+ }
177
+
178
+ /* ── tables: the grid's look (page-standard §2-1) ───────────────── */
179
+ .table-scroll {
180
+ overflow-x: auto;
181
+ }
182
+ table {
183
+ width: 100%;
184
+ border-collapse: collapse;
185
+ font-size: var(--md-sys-typescale-label-large-size, 14px);
186
+ }
187
+ th {
188
+ padding: var(--spacing-small) var(--spacing-medium);
189
+ border-top: 2px solid var(--md-sys-color-primary);
190
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
191
+ background: var(--md-sys-color-surface-container-low);
192
+ color: var(--md-sys-color-primary);
193
+ font-weight: 500;
194
+ text-align: start;
195
+ white-space: nowrap;
196
+ }
197
+ td {
198
+ padding: var(--spacing-medium);
199
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
200
+ vertical-align: top;
201
+ }
202
+ td.figure,
203
+ .figure {
204
+ font-variant-numeric: tabular-nums;
205
+ white-space: nowrap;
206
+ }
207
+ td.reason {
208
+ min-width: 16em;
209
+ }
210
+ td.actions {
211
+ width: 1%;
212
+ white-space: nowrap;
213
+ }
214
+ td.actions > div {
215
+ display: flex;
216
+ justify-content: flex-end;
217
+ gap: var(--spacing-small);
218
+ }
219
+ .muted {
220
+ color: var(--ui-muted);
221
+ }
222
+ .shift-lines {
223
+ display: grid;
224
+ gap: 2px;
225
+ }
226
+ .shift-lines > div {
227
+ white-space: nowrap;
228
+ }
229
+ .shift-lines .figure {
230
+ color: var(--ui-muted);
231
+ }
232
+ .state {
233
+ display: inline-block;
234
+ padding: 1px var(--spacing-medium);
235
+ border-radius: var(--md-sys-shape-corner-small, 4px);
236
+ background: var(--md-sys-color-surface-container-high);
237
+ font-size: var(--ui-t-micro);
238
+ font-weight: 600;
239
+ white-space: nowrap;
240
+ }
241
+ .state[data-state='current'] {
242
+ background: color-mix(in srgb, var(--ui-good) 18%, transparent);
243
+ }
244
+ .state[data-state='scheduled'],
245
+ .state[data-state='pending'],
246
+ .state[data-state='approved'] {
247
+ background: color-mix(in srgb, var(--ui-info, var(--md-sys-color-primary)) 18%, transparent);
248
+ }
249
+ .state[data-state='rejected'] {
250
+ background: color-mix(in srgb, var(--ui-crit) 16%, transparent);
251
+ }
252
+ tr.detail > td {
253
+ background: var(--md-sys-color-surface-container-lowest);
254
+ }
255
+
256
+ /* ── the editor ──────────────────────────────────────────────── */
257
+ .shift-rows {
258
+ container-type: inline-size;
259
+ display: grid;
260
+ margin-bottom: var(--spacing-large);
261
+ }
262
+ .shift-row {
263
+ display: grid;
264
+ grid-template-columns:
265
+ minmax(110px, 1fr) minmax(184px, 1.1fr) minmax(190px, 1.1fr) minmax(392px, max-content)
266
+ minmax(110px, 1fr) max-content;
267
+ gap: var(--spacing-medium) var(--spacing-large);
268
+ align-items: start;
269
+ padding: var(--spacing-medium) 0;
270
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
271
+ }
272
+ .shift-row.head {
273
+ padding-block: var(--spacing-small);
274
+ border-top: 2px solid var(--md-sys-color-primary);
275
+ color: var(--md-sys-color-primary);
276
+ font-weight: 500;
277
+ }
278
+ .shift-row input:not([type='checkbox']),
279
+ .shift-row select {
280
+ width: 100%;
281
+ min-width: 0;
282
+ }
283
+ .cell {
284
+ display: grid;
285
+ gap: var(--spacing-small);
286
+ min-width: 0;
287
+ }
288
+ .cell-label {
289
+ display: none;
290
+ color: var(--ui-muted);
291
+ font-size: var(--ui-t-micro);
292
+ }
293
+ .clock {
294
+ display: flex;
295
+ align-items: center;
296
+ gap: var(--spacing-small);
297
+ }
298
+ .clock select {
299
+ flex: none;
300
+ width: 72px !important;
301
+ }
302
+ .clock input {
303
+ flex: 1;
304
+ }
305
+ .derived {
306
+ color: var(--ui-muted);
307
+ font-size: var(--ui-t-small);
308
+ font-variant-numeric: tabular-nums;
309
+ white-space: nowrap;
310
+ }
311
+ .row-issues {
312
+ grid-column: 1 / -1;
313
+ margin: 0;
314
+ color: var(--ui-crit);
315
+ font-size: var(--ui-t-small);
316
+ }
317
+ .shift-row input[aria-invalid='true'] {
318
+ border-color: var(--ui-crit);
319
+ }
320
+ ox-input-select-buttons {
321
+ --md-chip-set-gap: 4px;
322
+ }
323
+ @container (max-width: 1080px) {
324
+ .shift-row {
325
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
326
+ }
327
+ .shift-row.head {
328
+ display: none;
329
+ }
330
+ .cell-label {
331
+ display: block;
332
+ }
333
+ .cell.name,
334
+ .cell.days,
335
+ .cell.note {
336
+ grid-column: 1 / -1;
337
+ }
338
+ .cell.remove {
339
+ grid-column: 1 / -1;
340
+ justify-items: start;
341
+ }
342
+ }
343
+ @container (max-width: 480px) {
344
+ .shift-row {
345
+ grid-template-columns: minmax(0, 1fr);
346
+ }
347
+ }
348
+
349
+ .form-grid {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
352
+ gap: var(--spacing-large);
353
+ }
354
+ .field {
355
+ display: grid;
356
+ align-content: start;
357
+ gap: var(--spacing-small);
358
+ min-width: 0;
359
+ }
360
+ .field > span:first-child {
361
+ color: var(--ui-muted);
362
+ font-size: var(--ui-t-micro);
363
+ }
364
+ .field input,
365
+ .field select {
366
+ width: 100%;
367
+ }
368
+ .field .error {
369
+ color: var(--ui-crit);
370
+ font-size: var(--ui-t-small);
371
+ }
372
+ .when {
373
+ display: flex;
374
+ flex-wrap: wrap;
375
+ align-items: center;
376
+ gap: var(--spacing-medium);
377
+ }
378
+ .when input {
379
+ flex: 1;
380
+ min-width: 200px;
381
+ }
382
+ .alert {
383
+ margin: 0;
384
+ padding: var(--spacing-medium) var(--spacing-large);
385
+ border-inline-start: 3px solid var(--ui-crit);
386
+ background: color-mix(in srgb, var(--ui-crit) 8%, transparent);
387
+ }
388
+
389
+ @media (max-width: 600px) {
390
+ .day-groups {
391
+ grid-template-columns: minmax(0, 1fr);
392
+ gap: var(--spacing-small);
393
+ }
394
+ .piece .time {
395
+ display: none;
396
+ }
397
+ }
398
+ `
399
+ ]; }
400
+ get context() {
401
+ return {
402
+ title: t('title.work-shift'),
403
+ frame: 'document',
404
+ help: 'page/work-shift',
405
+ actions: this.actions()
406
+ };
407
+ }
408
+ actions() {
409
+ if (!this.overview || this.error)
410
+ return [];
411
+ if (!this.draft) {
412
+ return [{ title: t('button.change-shifts'), icon: 'edit', emphasis: { raised: true }, action: () => this.begin() }];
413
+ }
414
+ const approval = this.overview.approvalRequired;
415
+ return [
416
+ { title: t('button.cancel'), action: () => this.close() },
417
+ ...(approval ? [{ title: rt('save-draft'), action: () => this.saveDraft() }] : []),
418
+ {
419
+ title: approval ? rt('request-approval') : t('button.save'),
420
+ icon: approval ? 'send' : 'save',
421
+ emphasis: { raised: true },
422
+ action: () => this.commit()
423
+ }
424
+ ];
425
+ }
426
+ async pageUpdated(changes) {
427
+ if (this.active && changes.active && !this.draft)
428
+ await this.load();
429
+ }
430
+ /* ── data ──────────────────────────────────────────────────────────── */
431
+ async load() {
432
+ this.loading = true;
433
+ this.error = null;
434
+ try {
435
+ const result = await client.query({
436
+ query: gql `
437
+ query {
438
+ workShiftRevisionOverview
439
+ }
440
+ `,
441
+ fetchPolicy: 'no-cache'
442
+ });
443
+ if (result.errors?.length)
444
+ throw result.errors[0];
445
+ this.overview = result.data.workShiftRevisionOverview;
446
+ this.approvals = await this.readApprovals(this.overview.revisions);
447
+ }
448
+ catch (e) {
449
+ this.error = e;
450
+ }
451
+ finally {
452
+ this.loading = false;
453
+ this.updateContext();
454
+ }
455
+ }
456
+ /** Approval states come from ops-master's approval flow, one revision at a time. */
457
+ async readApprovals(revisions) {
458
+ const approvals = {};
459
+ for (const row of revisions.filter(r => r.activityInstanceId)) {
460
+ let promise;
461
+ document.dispatchEvent(new CustomEvent('master-change-status', {
462
+ detail: { activityInstanceId: row.activityInstanceId, respondWith: (p) => (promise = p) }
463
+ }));
464
+ if (promise)
465
+ approvals[row.id] = await promise.catch(() => ({ state: 'unknown' }));
466
+ }
467
+ return approvals;
468
+ }
469
+ get placed() {
470
+ return placeRevisions(this.overview ?? { revisions: [], schedule: [] }, this.approvals);
471
+ }
472
+ /* ── editing ───────────────────────────────────────────────────────── */
473
+ begin(from) {
474
+ const base = from ?? this.placed.current?.row;
475
+ const shifts = base?.shifts ?? this.overview.legacyShifts ?? [];
476
+ this.draft = {
477
+ description: base?.description ?? '',
478
+ timezone: base?.timezone ?? this.overview.timezone,
479
+ shifts: shifts.length ? shifts.map(shift => settle({ ...shift })) : [nextShift([])],
480
+ effectiveMode: 'now',
481
+ effectiveFrom: new Date().toISOString(),
482
+ reason: ''
483
+ };
484
+ this.enterEditor();
485
+ }
486
+ resume(row) {
487
+ this.draft = {
488
+ id: row.id,
489
+ editGeneration: row.editGeneration,
490
+ revisionNumber: row.revisionNumber,
491
+ description: row.description ?? '',
492
+ timezone: row.timezone ?? this.overview.timezone,
493
+ shifts: (row.shifts ?? []).map(shift => settle({ ...shift })),
494
+ effectiveMode: 'at',
495
+ effectiveFrom: row.effectiveFrom ?? new Date().toISOString(),
496
+ reason: row.reason ?? ''
497
+ };
498
+ this.enterEditor();
499
+ }
500
+ enterEditor() {
501
+ this.dirty = false;
502
+ this.tried = false;
503
+ this.saveError = '';
504
+ this.updateContext();
505
+ /* The document frame scrolls the page element itself. */
506
+ this.scrollTo({ top: 0 });
507
+ }
508
+ async close() {
509
+ if (this.busy)
510
+ return;
511
+ if (this.dirty &&
512
+ !(await OxPrompt.open({
513
+ type: 'warning',
514
+ title: rt('discard-question'),
515
+ confirmButton: { text: rt('discard'), destructive: true },
516
+ cancelButton: { text: rt('keep-editing') }
517
+ })))
518
+ return;
519
+ this.draft = undefined;
520
+ this.dirty = false;
521
+ this.updateContext();
522
+ await this.load();
523
+ }
524
+ change(patch) {
525
+ this.draft = { ...this.draft, ...patch };
526
+ this.dirty = true;
527
+ }
528
+ changeShift(index, patch) {
529
+ this.change({ shifts: this.draft.shifts.map((s, i) => (i === index ? settle({ ...s, ...patch }) : s)) });
530
+ }
531
+ content() {
532
+ const { description, timezone, shifts, effectiveMode, effectiveFrom, reason } = this.draft;
533
+ return {
534
+ description,
535
+ timezone,
536
+ shifts: shiftsContent(shifts),
537
+ effectiveFrom: effectiveMode === 'now' ? new Date().toISOString() : effectiveFrom,
538
+ reason: reason.trim()
539
+ };
540
+ }
541
+ async run(work) {
542
+ if (this.busy)
543
+ return undefined;
544
+ this.busy = true;
545
+ this.saveError = '';
546
+ try {
547
+ return await work();
548
+ }
549
+ catch (e) {
550
+ /* The GraphQL client already announces server refusals and network failures; say only what it does not. */
551
+ if (!(e?.reported || e?.graphQLErrors?.length || e?.networkError))
552
+ this.saveError = e?.message ?? String(e);
553
+ return undefined;
554
+ }
555
+ finally {
556
+ this.busy = false;
557
+ }
558
+ }
559
+ async mutate(mutation, variables, field) {
560
+ const response = await client.mutate({ mutation, variables });
561
+ if (response.errors?.length)
562
+ throw Object.assign(new Error(response.errors[0].message), { reported: true });
563
+ return response.data[field];
564
+ }
565
+ /** Saves the editor as a draft and keeps it open. Returns false when it did not save. */
566
+ async saveDraft(quiet = false) {
567
+ this.tried = true;
568
+ if (draftIssues(this.draft).blocking)
569
+ return false;
570
+ const saved = await this.run(async () => {
571
+ const draft = this.draft;
572
+ const row = draft.id
573
+ ? await this.mutate(gql `
574
+ mutation ($revisionId: String!, $editGeneration: Int!, $content: Object!) {
575
+ editWorkShiftDraft(revisionId: $revisionId, editGeneration: $editGeneration, content: $content)
576
+ }
577
+ `, { revisionId: draft.id, editGeneration: draft.editGeneration, content: this.content() }, 'editWorkShiftDraft')
578
+ : await this.mutate(gql `
579
+ mutation ($content: Object!) {
580
+ createWorkShiftDraft(content: $content)
581
+ }
582
+ `, { content: this.content() }, 'createWorkShiftDraft');
583
+ this.draft = { ...draft, id: row.id, editGeneration: row.editGeneration, revisionNumber: row.revisionNumber };
584
+ this.dirty = false;
585
+ return true;
586
+ });
587
+ if (saved && !quiet)
588
+ notify({ message: rt('saved') });
589
+ return !!saved;
590
+ }
591
+ /** Puts the change into effect, or asks for approval where the site requires it. */
592
+ async commit() {
593
+ this.tried = true;
594
+ if (draftIssues(this.draft).blocking)
595
+ return;
596
+ const approval = this.overview.approvalRequired;
597
+ if (approval && (this.dirty || !this.draft.id) && !(await this.saveDraft(true)))
598
+ return;
599
+ const done = await this.run(async () => {
600
+ const draft = this.draft;
601
+ if (approval) {
602
+ let promise;
603
+ document.dispatchEvent(new CustomEvent('master-change-approval', {
604
+ detail: {
605
+ axis: 'workShift',
606
+ revisionId: draft.id,
607
+ editGeneration: draft.editGeneration,
608
+ respondWith: (p) => (promise = p)
609
+ }
610
+ }));
611
+ if (!promise)
612
+ throw new Error(rt('approval-unavailable'));
613
+ await promise;
614
+ }
615
+ else {
616
+ await this.mutate(gql `
617
+ mutation ($revisionId: String, $editGeneration: Int, $content: Object!) {
618
+ saveAndPublishWorkShift(revisionId: $revisionId, editGeneration: $editGeneration, content: $content)
619
+ }
620
+ `, { revisionId: draft.id, editGeneration: draft.editGeneration, content: this.content() }, 'saveAndPublishWorkShift');
621
+ }
622
+ return true;
623
+ });
624
+ if (!done)
625
+ return;
626
+ notify({ message: rt(approval ? 'submitted' : 'published') });
627
+ this.draft = undefined;
628
+ this.dirty = false;
629
+ this.updateContext();
630
+ await this.load();
631
+ }
632
+ clock(minute) {
633
+ return minute >= DAY_MINUTES
634
+ ? `${this.dayWord(1)} ${clockOf(minute - DAY_MINUTES)}`
635
+ : minute < 0
636
+ ? `${this.dayWord(-1)} ${clockOf(minute + DAY_MINUTES)}`
637
+ : clockOf(minute);
638
+ }
639
+ span(from, to) {
640
+ return `${this.clock(from)} – ${this.clock(to)}`;
641
+ }
642
+ hours(minutes) {
643
+ if (minutes === undefined)
644
+ return '';
645
+ const h = Math.floor(minutes / 60);
646
+ const m = minutes % 60;
647
+ return m ? t('text.hours-minutes', { h, m }) : t('text.hours', { h });
648
+ }
649
+ daysText(days) {
650
+ return daysLabel(days, day => rt('weekday-' + day), rt('every-day'));
651
+ }
652
+ bar(shifts) {
653
+ const line = timelineOf(shifts);
654
+ if (!line)
655
+ return html `<div class="day-none">${t('text.no-shift-on-these-days')}</div>`;
656
+ const label = (piece) => piece.kind === 'shift'
657
+ ? `${piece.names[0]} ${this.span(piece.fromMinute, piece.toMinute)}`
658
+ : piece.kind === 'gap'
659
+ ? t('text.shift-gap-span', { span: this.span(piece.fromMinute, piece.toMinute) })
660
+ : t('text.shift-overlap-span', { span: this.span(piece.fromMinute, piece.toMinute), first: piece.names[0], second: piece.names[1] });
661
+ return html `<div>
662
+ <div class="bar" role="img" aria-label=${line.pieces.map(label).join(', ')}>
663
+ ${line.midnights.map(left => html `<div class="midnight" style="left:${left}%"></div>`)}
664
+ ${line.pieces.map(piece => html `<div class="piece" data-kind=${piece.kind} style="left:${piece.left}%;width:${piece.width}%" title=${label(piece)}>
665
+ ${piece.kind === 'shift'
666
+ ? html `<strong>${piece.names[0]}</strong
667
+ ><span class="time">${clockOf(((piece.fromMinute % DAY_MINUTES) + DAY_MINUTES) % DAY_MINUTES)}–${clockOf(((piece.toMinute % DAY_MINUTES) + DAY_MINUTES) % DAY_MINUTES)}</span>`
668
+ : nothing}
669
+ </div>`)}
670
+ </div>
671
+ <div class="axis" aria-hidden="true">
672
+ ${line.ticks.map((minute, i) => html `<span style="left:${i * 25}%">${clockOf(((minute % DAY_MINUTES) + DAY_MINUTES) % DAY_MINUTES)}</span>`)}
673
+ </div>
674
+ </div>`;
675
+ }
676
+ /**
677
+ * The day bars of an arrangement, one per group of weekdays that run the same shifts, and what is wrong with them.
678
+ * The editor passes the week's overlaps: they include a Friday night running into Saturday, which no single
679
+ * day's bar shows, and the server refuses every one of them.
680
+ */
681
+ timeline(shifts, weekOverlaps) {
682
+ const groups = weekGroups(shifts);
683
+ const findings = groups.flatMap(group => coverageProblems(group.shifts)
684
+ .filter(problem => problem.kind === 'gap' || (problem.kind === 'overlap' && !weekOverlaps))
685
+ .map(problem => ({ group, problem })));
686
+ const firstGap = findings.findIndex(({ problem }) => problem.kind === 'gap');
687
+ const prefix = (days) => (groups.length > 1 ? `${this.daysText(days)} · ` : '');
688
+ return html `
689
+ <div class="day-groups">
690
+ ${groups.map(group => html `<div class="day-label">${this.daysText(group.days)}</div>
691
+ ${group.shifts.length ? this.bar(group.shifts) : html `<div class="day-none">${t('text.no-shift-on-these-days')}</div>`}`)}
692
+ </div>
693
+ ${findings.length || weekOverlaps?.length
694
+ ? html `<ul class="findings">
695
+ ${findings.map(({ group, problem }, index) => problem.kind === 'gap'
696
+ ? html `<li data-kind="gap">
697
+ <span
698
+ >${prefix(group.days)}${t('text.shift-gap-span', { span: this.span(problem.fromMinute, problem.toMinute) })}
699
+ ${index === firstGap ? html `<span class="why">${t('text.shift-gap-why')}</span>` : nothing}</span
700
+ >
701
+ </li>`
702
+ : html `<li data-kind="overlap">
703
+ <span
704
+ >${prefix(group.days)}${t('text.shift-overlap-span', {
705
+ span: this.span(problem.fromMinute, problem.toMinute),
706
+ first: problem.names[0],
707
+ second: problem.names[1]
708
+ })}</span
709
+ >
710
+ </li>`)}
711
+ ${(weekOverlaps ?? []).map(overlap => html `<li data-kind="error">
712
+ <span
713
+ >${t('text.shift-overlap-on-days', {
714
+ days: this.daysText(overlap.days),
715
+ span: this.span(overlap.fromMinute, overlap.toMinute),
716
+ first: overlap.names[0],
717
+ second: overlap.names[1]
718
+ })}</span
719
+ >
720
+ </li>`)}
721
+ </ul>`
722
+ : nothing}
723
+ `;
724
+ }
725
+ shiftTable(shifts) {
726
+ return html `<div class="table-scroll">
727
+ <table>
728
+ <thead>
729
+ <tr>
730
+ <th>${t('field.shift')}</th>
731
+ <th>${t('field.shift-hours')}</th>
732
+ <th>${rt('daysOfWeek')}</th>
733
+ </tr>
734
+ </thead>
735
+ <tbody>
736
+ ${shifts.map(shift => html `<tr>
737
+ <td>${shift.name}${shift.description ? html `<div class="muted">${shift.description}</div>` : nothing}</td>
738
+ <td class="figure">${timeRange(shift, this.dayWord)} <span class="muted">· ${this.hours(lengthOf(shift))}</span></td>
739
+ <td>${this.daysText(daysOf(shift.daysOfWeek))}</td>
740
+ </tr>`)}
741
+ </tbody>
742
+ </table>
743
+ </div>`;
744
+ }
745
+ shiftLines(shifts = []) {
746
+ return html `<div class="shift-lines">
747
+ ${shifts.map(shift => html `<div>
748
+ ${shift.name} <span class="figure">${timeRange(shift, this.dayWord)}</span>
749
+ ${daysOf(shift.daysOfWeek).length < WEEK.length ? html `<span class="muted">${this.daysText(daysOf(shift.daysOfWeek))}</span>` : nothing}
750
+ </div>`)}
751
+ </div>`;
752
+ }
753
+ stateTag(state) {
754
+ return html `<span class="state" data-state=${state}>${rt(state)}</span>`;
755
+ }
756
+ period(item) {
757
+ if (!item.period)
758
+ return html `<span class="muted">${t('text.from-time', { time: revisionDate(item.row.effectiveFrom) })}</span>`;
759
+ return item.period.to
760
+ ? `${revisionDate(item.period.from)} – ${revisionDate(item.period.to)}`
761
+ : t('text.from-time', { time: revisionDate(item.period.from) });
762
+ }
763
+ /* ── view ──────────────────────────────────────────────────────────── */
764
+ viewCurrent(placed) {
765
+ const current = placed.current;
766
+ const legacy = !current ? this.overview.legacyShifts ?? [] : [];
767
+ return html `<section class="page-section">
768
+ <header class="page-section-header"><h3>${t('title.shift-current')}</h3></header>
769
+ ${current
770
+ ? html `<p class="page-section-note">
771
+ ${t('text.from-time', { time: revisionDate(current.period.from) })} · ${current.row.timezone}
772
+ </p>`
773
+ : nothing}
774
+ ${current?.row.shifts?.length || legacy.length
775
+ ? html `<div class="arrangement">
776
+ ${this.timeline(current?.row.shifts ?? legacy)} ${this.shiftTable(current?.row.shifts ?? legacy)}
777
+ </div>`
778
+ : html `<ox-list-state
779
+ .scale=${{ shown: 0, loaded: 0 }}
780
+ .emptyNote=${{ icon: 'schedule', title: t('text.no-shifts-declared') }}
781
+ ></ox-list-state>`}
782
+ </section>`;
783
+ }
784
+ viewScheduled(placed) {
785
+ if (!placed.scheduled.length)
786
+ return nothing;
787
+ return html `<section class="page-section">
788
+ <header class="page-section-header"><h3>${t('title.shift-announcements')}</h3></header>
789
+ ${placed.scheduled.map(
790
+ /* The bar only: the shifts themselves are listed in the history row right below. */
791
+ item => html `<p class="page-section-note">
792
+ ${t('text.from-time', { time: revisionDate(item.period.from) })}${item.row.reason ? ` · ${item.row.reason}` : ''}
793
+ </p>
794
+ <div class="arrangement">${this.timeline(item.row.shifts ?? [])}</div>`)}
795
+ </section>`;
796
+ }
797
+ /** A person's reason as written; the recorded first arrangement in plain words, with where it came from. */
798
+ reasonCell(row, approval) {
799
+ return html `<td class="reason">
800
+ ${reasonOf(row, t('text.shift-baseline'))}
801
+ ${isBaseline(row) ? html `<div class="muted">${t('text.shift-baseline-why')}</div>` : nothing}
802
+ ${approval?.comment ? html `<div class="muted">${approval.comment}</div>` : nothing}
803
+ </td>`;
804
+ }
805
+ viewOpen(placed) {
806
+ if (!placed.open.length)
807
+ return nothing;
808
+ return html `<section class="page-section">
809
+ <header class="page-section-header"><h3>${t('title.shift-open-changes')}</h3></header>
810
+ <div class="table-scroll">
811
+ <table>
812
+ <thead>
813
+ <tr>
814
+ <th>${rt('state')}</th>
815
+ <th>${rt('effectiveFrom')}</th>
816
+ <th>${t('field.shift')}</th>
817
+ <th>${rt('reason')}</th>
818
+ <th></th>
819
+ </tr>
820
+ </thead>
821
+ <tbody>
822
+ ${placed.open.map(item => {
823
+ const approval = this.approvals[item.row.id];
824
+ const open = this.opened === item.row.id;
825
+ return html `<tr>
826
+ <td>${this.stateTag(item.state)}</td>
827
+ <td class="figure">${revisionDate(item.row.effectiveFrom)}</td>
828
+ <td>${this.shiftLines(item.row.shifts)}</td>
829
+ ${this.reasonCell(item.row, approval)}
830
+ <td class="actions">
831
+ <div>
832
+ ${item.state === 'draft'
833
+ ? html `<button small @click=${() => this.resume(item.row)}>${t('button.continue-editing')}</button>`
834
+ : html `<button small quiet @click=${() => (this.opened = open ? undefined : item.row.id)}>
835
+ ${open ? t('button.close') : rt('changes')}
836
+ </button>`}
837
+ ${approval?.open ? html `<button small @click=${() => approval.open()}>${rt('approval-detail')}</button>` : nothing}
838
+ </div>
839
+ </td>
840
+ </tr>
841
+ ${open
842
+ ? html `<tr class="detail">
843
+ <td colspan="5">
844
+ <ops-master-revision-comparison
845
+ axis="workShift"
846
+ .before=${this.overview.revisions.find((r) => r.id === item.row.baseRevisionId)}
847
+ .after=${item.row}
848
+ ></ops-master-revision-comparison>
849
+ </td>
850
+ </tr>`
851
+ : nothing}`;
852
+ })}
853
+ </tbody>
854
+ </table>
855
+ </div>
856
+ </section>`;
857
+ }
858
+ viewHistory(placed) {
859
+ return html `<section class="page-section">
860
+ <header class="page-section-header"><h3>${t('title.shift-history')}</h3></header>
861
+ ${placed.history.length
862
+ ? html `<div class="table-scroll">
863
+ <table>
864
+ <thead>
865
+ <tr>
866
+ <th>${t('field.revision-number')}</th>
867
+ <th>${rt('state')}</th>
868
+ <th>${t('field.shift-period')}</th>
869
+ <th>${t('field.shift')}</th>
870
+ <th>${rt('reason')}</th>
871
+ <th></th>
872
+ </tr>
873
+ </thead>
874
+ <tbody>
875
+ ${placed.history.map(item => {
876
+ const approval = this.approvals[item.row.id];
877
+ return html `<tr>
878
+ <td class="figure">${item.row.revisionNumber}</td>
879
+ <td>${this.stateTag(item.state)}</td>
880
+ <td class="figure">${this.period(item)}</td>
881
+ <td>${this.shiftLines(item.row.shifts)}</td>
882
+ ${this.reasonCell(item.row, approval)}
883
+ <td class="actions">
884
+ <div>
885
+ ${approval?.open ? html `<button small quiet @click=${() => approval.open()}>${rt('approval-detail')}</button>` : nothing}
886
+ <button small @click=${() => this.begin(item.row)}>${t('button.change-from-this')}</button>
887
+ </div>
888
+ </td>
889
+ </tr>`;
890
+ })}
891
+ </tbody>
892
+ </table>
893
+ </div>`
894
+ : html `<ox-list-state .scale=${{ shown: 0, loaded: 0 }} .emptyNote=${{ title: t('text.no-shift-history') }}></ox-list-state>`}
895
+ </section>`;
896
+ }
897
+ /* ── editor ────────────────────────────────────────────────────────── */
898
+ issueText(issue) {
899
+ return t('text.shift-issue-' + issue);
900
+ }
901
+ shiftRow(shift, index, issues) {
902
+ const draft = this.draft;
903
+ const nameBad = issues.includes('name-missing') || issues.includes('name-duplicate');
904
+ const timeBad = issues.includes('time-missing') || issues.includes('ends-too-late');
905
+ const length = lengthOf(shift);
906
+ return html `<div class="shift-row">
907
+ <label class="cell name">
908
+ <span class="cell-label">${rt('name')}</span>
909
+ <input
910
+ .value=${shift.name ?? ''}
911
+ aria-label=${rt('name')}
912
+ aria-invalid=${nameBad && (this.tried || !!shift.name) ? 'true' : 'false'}
913
+ ?disabled=${this.busy}
914
+ @input=${(e) => this.changeShift(index, { name: e.target.value })}
915
+ />
916
+ </label>
917
+ <div class="cell">
918
+ <span class="cell-label">${t('field.shift-start')}</span>
919
+ <div class="clock">
920
+ <select
921
+ aria-label=${rt('fromDate')}
922
+ ?disabled=${this.busy}
923
+ @change=${(e) => this.changeShift(index, { fromDate: Number(e.target.value) })}
924
+ >
925
+ ${[-1, 0, 1].map(day => html `<option value=${day} ?selected=${Number(shift.fromDate ?? 0) === day}>${this.dayWord(day)}</option>`)}
926
+ </select>
927
+ <input
928
+ type="time"
929
+ .value=${String(shift.fromTime ?? '').slice(0, 5)}
930
+ aria-label=${rt('fromTime')}
931
+ aria-invalid=${timeBad && this.tried ? 'true' : 'false'}
932
+ ?disabled=${this.busy}
933
+ @input=${(e) => this.changeShift(index, { fromTime: e.target.value })}
934
+ />
935
+ </div>
936
+ </div>
937
+ <div class="cell">
938
+ <span class="cell-label">${t('field.shift-end')}</span>
939
+ <div class="clock">
940
+ <input
941
+ type="time"
942
+ .value=${String(shift.toTime ?? '').slice(0, 5)}
943
+ aria-label=${rt('toTime')}
944
+ aria-invalid=${timeBad && this.tried ? 'true' : 'false'}
945
+ ?disabled=${this.busy}
946
+ @input=${(e) => this.changeShift(index, { toTime: e.target.value })}
947
+ />
948
+ <span class="derived"
949
+ >${shift.toDate ? `${this.dayWord(shift.toDate)} · ` : ''}${this.hours(length)}</span
950
+ >
951
+ </div>
952
+ </div>
953
+ <div class="cell days">
954
+ <span class="cell-label">${rt('daysOfWeek')}</span>
955
+ <ox-input-select-buttons
956
+ multiple
957
+ aria-label=${rt('daysOfWeek')}
958
+ .options=${WEEK.map(day => ({ display: rt('weekday-' + day), value: day }))}
959
+ .value=${daysOf(shift.daysOfWeek)}
960
+ ?disabled=${this.busy}
961
+ @change=${(e) => {
962
+ const value = daysValue(e.detail ?? []);
963
+ /* A shift on no day is not a shift: keep the last day chosen. */
964
+ if (value === undefined)
965
+ e.target.value = daysOf(shift.daysOfWeek);
966
+ else
967
+ this.changeShift(index, { daysOfWeek: value });
968
+ }}
969
+ ></ox-input-select-buttons>
970
+ </div>
971
+ <label class="cell note">
972
+ <span class="cell-label">${rt('description')}</span>
973
+ <input
974
+ .value=${shift.description ?? ''}
975
+ aria-label=${rt('description')}
976
+ ?disabled=${this.busy}
977
+ @input=${(e) => this.changeShift(index, { description: e.target.value })}
978
+ />
979
+ </label>
980
+ <div class="cell remove">
981
+ <button
982
+ small
983
+ quiet
984
+ danger
985
+ ?disabled=${this.busy || draft.shifts.length === 1}
986
+ aria-label=${`${rt('remove')} ${shift.name || index + 1}`}
987
+ @click=${() => this.change({ shifts: draft.shifts.filter((_, i) => i !== index) })}
988
+ >
989
+ ${rt('remove')}
990
+ </button>
991
+ </div>
992
+ ${issues.length && (this.tried || issues.some(issue => issue === 'name-duplicate' || issue === 'ends-too-late'))
993
+ ? html `<p class="row-issues">${issues.map(issue => this.issueText(issue)).join(' · ')}</p>`
994
+ : nothing}
995
+ </div>`;
996
+ }
997
+ editor() {
998
+ const draft = this.draft;
999
+ const issues = draftIssues(draft);
1000
+ const timezones = Intl.supportedValuesOf?.('timeZone') ?? [];
1001
+ const zones = timezones.includes(draft.timezone) ? timezones : [draft.timezone, ...timezones];
1002
+ return html `
1003
+ ${this.saveError ? html `<p class="alert" role="alert">${this.saveError}</p>` : nothing}
1004
+ <section class="page-section">
1005
+ <header class="page-section-header">
1006
+ <h3>${draft.revisionNumber ? rt('revision-value', { n: draft.revisionNumber }) : t('title.shift-new-arrangement')}</h3>
1007
+ <div class="page-section-actions">
1008
+ <button ?disabled=${this.busy} @click=${() => this.change({ shifts: [...draft.shifts, nextShift(draft.shifts)] })}>
1009
+ ${rt('add-shift')}
1010
+ </button>
1011
+ </div>
1012
+ </header>
1013
+ <div class="shift-rows">
1014
+ <div class="shift-row head" aria-hidden="true">
1015
+ <span>${rt('name')}</span><span>${t('field.shift-start')}</span><span>${t('field.shift-end')}</span
1016
+ ><span>${rt('daysOfWeek')}</span><span>${rt('description')}</span><span></span>
1017
+ </div>
1018
+ ${draft.shifts.map((shift, index) => this.shiftRow(shift, index, issues.rows.get(index) ?? []))}
1019
+ </div>
1020
+ <div class="arrangement">
1021
+ ${this.timeline(draft.shifts, groupOverlaps(issues.overlaps))}
1022
+ </div>
1023
+ </section>
1024
+
1025
+ <section class="page-section">
1026
+ <header class="page-section-header"><h3>${t('title.shift-apply')}</h3></header>
1027
+ <div class="form-grid">
1028
+ <div class="field">
1029
+ <span>${rt('effectiveFrom')}</span>
1030
+ <div class="when">
1031
+ <ox-input-select-buttons
1032
+ .options=${[
1033
+ { display: t('text.apply-now'), value: 'now' },
1034
+ { display: t('text.apply-later'), value: 'at' }
1035
+ ]}
1036
+ .value=${draft.effectiveMode}
1037
+ ?disabled=${this.busy}
1038
+ @change=${(e) => {
1039
+ if (!e.detail)
1040
+ e.target.value = draft.effectiveMode;
1041
+ else
1042
+ this.change({ effectiveMode: e.detail });
1043
+ }}
1044
+ ></ox-input-select-buttons>
1045
+ ${draft.effectiveMode === 'at'
1046
+ ? html `<input
1047
+ type="datetime-local"
1048
+ aria-label=${rt('effectiveFrom')}
1049
+ .value=${localDateTime(draft.effectiveFrom)}
1050
+ ?disabled=${this.busy}
1051
+ @change=${(e) => {
1052
+ try {
1053
+ this.change({ effectiveFrom: isoFromLocal(e.target.value) });
1054
+ }
1055
+ catch {
1056
+ e.target.value = localDateTime(draft.effectiveFrom);
1057
+ }
1058
+ }}
1059
+ />`
1060
+ : nothing}
1061
+ </div>
1062
+ </div>
1063
+ <label class="field">
1064
+ <span>${rt('reason')}</span>
1065
+ <input
1066
+ .value=${draft.reason}
1067
+ aria-invalid=${issues.reasonMissing && this.tried ? 'true' : 'false'}
1068
+ ?disabled=${this.busy}
1069
+ @input=${(e) => this.change({ reason: e.target.value })}
1070
+ />
1071
+ ${issues.reasonMissing && this.tried ? html `<span class="error">${t('text.shift-issue-reason-missing')}</span>` : nothing}
1072
+ </label>
1073
+ <label class="field">
1074
+ <span>${rt('timezone')}</span>
1075
+ <select ?disabled=${this.busy} @change=${(e) => this.change({ timezone: e.target.value })}>
1076
+ ${zones.map(zone => html `<option value=${zone} ?selected=${zone === draft.timezone}>${zone}</option>`)}
1077
+ </select>
1078
+ </label>
1079
+ <label class="field">
1080
+ <span>${rt('description')}</span>
1081
+ <input .value=${draft.description ?? ''} ?disabled=${this.busy} @input=${(e) => this.change({ description: e.target.value })} />
1082
+ </label>
1083
+ </div>
1084
+ </section>
1085
+ `;
1086
+ }
1087
+ render() {
1088
+ const placed = this.overview ? this.placed : undefined;
1089
+ return html `<ox-page-state .loading=${this.loading && !this.overview} .error=${this.error} @retry=${() => this.load()}>
1090
+ <div class="page">
1091
+ ${placed
1092
+ ? this.draft
1093
+ ? this.editor()
1094
+ : html `${this.viewCurrent(placed)} ${this.viewScheduled(placed)} ${this.viewOpen(placed)} ${this.viewHistory(placed)}`
1095
+ : nothing}
1096
+ </div>
1097
+ </ox-page-state>`;
1098
+ }
1099
+ };
1100
+ __decorate([
1101
+ state(),
1102
+ __metadata("design:type", Object)
1103
+ ], WorkShift.prototype, "overview", void 0);
1104
+ __decorate([
1105
+ state(),
1106
+ __metadata("design:type", Object)
1107
+ ], WorkShift.prototype, "loading", void 0);
1108
+ __decorate([
1109
+ state(),
1110
+ __metadata("design:type", Object)
1111
+ ], WorkShift.prototype, "error", void 0);
1112
+ __decorate([
1113
+ state(),
1114
+ __metadata("design:type", Object)
1115
+ ], WorkShift.prototype, "approvals", void 0);
1116
+ __decorate([
1117
+ state(),
1118
+ __metadata("design:type", Object)
1119
+ ], WorkShift.prototype, "draft", void 0);
1120
+ __decorate([
1121
+ state(),
1122
+ __metadata("design:type", Object)
1123
+ ], WorkShift.prototype, "dirty", void 0);
1124
+ __decorate([
1125
+ state(),
1126
+ __metadata("design:type", Object)
1127
+ ], WorkShift.prototype, "busy", void 0);
1128
+ __decorate([
1129
+ state(),
1130
+ __metadata("design:type", Object)
1131
+ ], WorkShift.prototype, "saveError", void 0);
1132
+ __decorate([
1133
+ state(),
1134
+ __metadata("design:type", Object)
1135
+ ], WorkShift.prototype, "tried", void 0);
1136
+ __decorate([
1137
+ state(),
1138
+ __metadata("design:type", String)
1139
+ ], WorkShift.prototype, "opened", void 0);
1140
+ WorkShift = __decorate([
1141
+ customElement('work-shift')
1142
+ ], WorkShift);
1143
+ export { WorkShift };
1144
+ //# sourceMappingURL=work-shift.js.map