@kernhq/module-hr 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +30 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +276 -61
- package/dist/server/router.js.map +1 -1
- package/package.json +1 -1
- package/src/client/messages.ts +114 -60
- package/src/client/pages/AttendancePage.svelte +24 -1
- package/src/client/pages/DirectoryPage.svelte +240 -10
- package/src/client/pages/OfficesPage.svelte +6 -0
- package/src/client/pages/OrgPage.svelte +7 -0
- package/src/client/settings/AccrualSettings.svelte +119 -38
- package/src/client/settings/SchedulesSettings.svelte +31 -78
|
@@ -43,6 +43,19 @@ import { formatDuration, hrKeys, isoDate } from '../query.js'
|
|
|
43
43
|
* the week runs down the dialog rather than across it, and the label column lands on whichever side
|
|
44
44
|
* the reader's language starts from without a single `left` or `right` in the stylesheet.
|
|
45
45
|
*
|
|
46
|
+
* **A schedule sets the hours, not the zone, and this screen used to say otherwise.** There was a
|
|
47
|
+
* "Time zone" field here offering the person's office, the person's own zone, or one fixed zone,
|
|
48
|
+
* and `schedules.tz_mode` / `schedules.tz` do store whichever was picked. Nothing ever reads them:
|
|
49
|
+
* `attendance.scheduleFor` builds a `ResolvedSchedule` of shifts, rounding and the automatic close
|
|
50
|
+
* and carries neither, and the zone a day is attributed in comes from `resolve.forPerson` — the
|
|
51
|
+
* person's own override if their profile sets one, otherwise their primary office. That ladder is
|
|
52
|
+
* the module's stated rule rather than an accident (`OfficeAssignment` in the contract: only the
|
|
53
|
+
* primary decides "which timezone the person's day is attributed in"), so the control was offering
|
|
54
|
+
* to change something it could not reach, and `fixed` was the option with no rung at all. The
|
|
55
|
+
* control is gone; the two columns stay. They hold what somebody once chose, dropping them would
|
|
56
|
+
* destroy that, and switching a setting off must never destroy data — so the screen states where
|
|
57
|
+
* the zone comes from instead of asking.
|
|
58
|
+
*
|
|
46
59
|
* No stat tiles. Every number worth one here — how many schedules, how long each week is — is
|
|
47
60
|
* already a column of the table below, and OfficesPage learned the hard way that a tile restating
|
|
48
61
|
* the list is noise. The one figure that would earn a tile, how many people are on no schedule at
|
|
@@ -199,8 +212,6 @@ let editing = $state<{ schedule: Schedule | null } | null>(null)
|
|
|
199
212
|
let name = $state('')
|
|
200
213
|
let kind = $state('fixed')
|
|
201
214
|
let week = $state<Week>(defaultWeek())
|
|
202
|
-
let tzMode = $state('office')
|
|
203
|
-
let tz = $state('')
|
|
204
215
|
let graceIn = $state('0')
|
|
205
216
|
let graceOut = $state('0')
|
|
206
217
|
let roundingStep = $state('0')
|
|
@@ -215,8 +226,6 @@ function openEditor(schedule: Schedule | null) {
|
|
|
215
226
|
name = schedule?.name ?? ''
|
|
216
227
|
kind = schedule?.kind ?? 'fixed'
|
|
217
228
|
week = schedule ? readWeek(schedule.week) : defaultWeek()
|
|
218
|
-
tzMode = schedule?.tzMode ?? 'office'
|
|
219
|
-
tz = schedule?.tz ?? ''
|
|
220
229
|
graceIn = String(schedule?.graceInMinutes ?? 0)
|
|
221
230
|
graceOut = String(schedule?.graceOutMinutes ?? 0)
|
|
222
231
|
roundingStep = String(schedule?.roundingStepMinutes ?? 0)
|
|
@@ -261,22 +270,15 @@ const timesMissing = $derived(
|
|
|
261
270
|
}),
|
|
262
271
|
)
|
|
263
272
|
const noWorkingDays = $derived(workingDays.length === 0)
|
|
264
|
-
const zoneMissing = $derived(tzMode === 'fixed' && !tz)
|
|
265
273
|
|
|
266
274
|
const weekError = $derived(
|
|
267
275
|
noWorkingDays ? t('schedule_week_empty') : timesMissing ? t('schedule_time_missing') : null,
|
|
268
276
|
)
|
|
269
|
-
const canSave = $derived(manage && name.trim().length > 0 && !weekError
|
|
277
|
+
const canSave = $derived(manage && name.trim().length > 0 && !weekError)
|
|
270
278
|
|
|
271
279
|
/** Why Save is off, in the order somebody would fix them. Null when nothing is blocking. */
|
|
272
280
|
const blockedReason = $derived(
|
|
273
|
-
canSave
|
|
274
|
-
? null
|
|
275
|
-
: !manage
|
|
276
|
-
? t('schedules_readonly')
|
|
277
|
-
: !name.trim()
|
|
278
|
-
? t('schedule_name_required')
|
|
279
|
-
: (weekError ?? (zoneMissing ? t('schedule_tz_required') : null)),
|
|
281
|
+
canSave ? null : !manage ? t('schedules_readonly') : !name.trim() ? t('schedule_name_required') : weekError,
|
|
280
282
|
)
|
|
281
283
|
|
|
282
284
|
const save = createMutation(() => ({
|
|
@@ -292,16 +294,15 @@ const save = createMutation(() => ({
|
|
|
292
294
|
roundingDirection: roundingDirection as Schedule['roundingDirection'],
|
|
293
295
|
autoClockOutAfterMinutes: autoClockOut ? Number(autoClockOut) : null,
|
|
294
296
|
}
|
|
295
|
-
// Kind
|
|
296
|
-
//
|
|
297
|
+
// Kind is create-only in the contract, and deliberately: the days already computed were
|
|
298
|
+
// measured with it, so changing it would rewrite history rather than the rule.
|
|
299
|
+
//
|
|
300
|
+
// `tzMode`/`tz` are not sent. The contract defaults `tzMode` to `office` and leaves `tz` null,
|
|
301
|
+
// which is the only combination that describes what attendance actually does — see the
|
|
302
|
+
// docblock. Sending anything else would record a choice no computation will honour.
|
|
297
303
|
return current
|
|
298
304
|
? api.attendance.schedules.update({ ...common, scheduleId: current.id })
|
|
299
|
-
: api.attendance.schedules.create({
|
|
300
|
-
...common,
|
|
301
|
-
kind: kind as Schedule['kind'],
|
|
302
|
-
tzMode: tzMode as Schedule['tzMode'],
|
|
303
|
-
tz: tzMode === 'fixed' ? tz : null,
|
|
304
|
-
})
|
|
305
|
+
: api.attendance.schedules.create({ ...common, kind: kind as Schedule['kind'] })
|
|
305
306
|
},
|
|
306
307
|
onSuccess: (schedule) => {
|
|
307
308
|
toast.success(isNew ? t('schedule_created', { name: schedule.name }) : t('schedule_saved'))
|
|
@@ -418,34 +419,6 @@ const kindOptions = $derived([
|
|
|
418
419
|
{ value: 'shift', label: t('schedule_kind_shift'), description: t('schedule_kind_shift_desc') },
|
|
419
420
|
])
|
|
420
421
|
|
|
421
|
-
const tzModeOptions = $derived([
|
|
422
|
-
{ value: 'office', label: t('schedule_tz_office') },
|
|
423
|
-
{ value: 'person', label: t('schedule_tz_person') },
|
|
424
|
-
{ value: 'fixed', label: t('schedule_tz_fixed') },
|
|
425
|
-
])
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Every zone the runtime knows, grouped by area.
|
|
429
|
-
*
|
|
430
|
-
* `supportedValuesOf` is the only complete list available to a browser; where it is missing there
|
|
431
|
-
* is exactly one zone worth offering, the one this machine is in. Reading it is not formatting, so
|
|
432
|
-
* it does not go through the shared formatters.
|
|
433
|
-
*/
|
|
434
|
-
const ZONES: string[] = (() => {
|
|
435
|
-
try {
|
|
436
|
-
const supported = (Intl as { supportedValuesOf?: (key: string) => string[] }).supportedValuesOf
|
|
437
|
-
const list = supported?.call(Intl, 'timeZone') ?? []
|
|
438
|
-
return list.length ? list : [Intl.DateTimeFormat().resolvedOptions().timeZone]
|
|
439
|
-
} catch {
|
|
440
|
-
return [Intl.DateTimeFormat().resolvedOptions().timeZone]
|
|
441
|
-
}
|
|
442
|
-
})()
|
|
443
|
-
const zoneOptions = ZONES.map((zone) => ({
|
|
444
|
-
value: zone,
|
|
445
|
-
label: zone.split('/').slice(1).join(' / ').replace(/_/g, ' ') || zone,
|
|
446
|
-
group: zone.split('/')[0]?.replace(/_/g, ' ') ?? '',
|
|
447
|
-
}))
|
|
448
|
-
|
|
449
422
|
/** A break somebody actually typed into the database keeps its place in the list. */
|
|
450
423
|
const breakOptions = (current: number) =>
|
|
451
424
|
[...new Set([0, 15, 30, 45, 60, 90, current])]
|
|
@@ -716,35 +689,15 @@ const autoClockOutOptions = $derived([
|
|
|
716
689
|
</div>
|
|
717
690
|
</section>
|
|
718
691
|
|
|
719
|
-
<!--
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
<Field
|
|
729
|
-
label={t('schedule_tz_label')}
|
|
730
|
-
required
|
|
731
|
-
error={zoneMissing ? t('schedule_tz_required') : null}
|
|
732
|
-
>
|
|
733
|
-
{#snippet children(id)}
|
|
734
|
-
<Select {id} bind:value={tz} options={zoneOptions} placeholder={t('schedule_tz_label')} />
|
|
735
|
-
{/snippet}
|
|
736
|
-
</Field>
|
|
737
|
-
{/if}
|
|
738
|
-
</div>
|
|
739
|
-
{:else}
|
|
740
|
-
<div class="ro">
|
|
741
|
-
<span class="rolabel">{t('schedule_tz_mode')}</span>
|
|
742
|
-
<p class="rovalue">
|
|
743
|
-
{tzModeOptions.find((option) => option.value === tzMode)?.label ?? tzMode}{#if tz} · {tz}{/if}
|
|
744
|
-
</p>
|
|
745
|
-
<p class="note">{t('schedule_immutable_hint')}</p>
|
|
746
|
-
</div>
|
|
747
|
-
{/if}
|
|
692
|
+
<!--
|
|
693
|
+
Whose clock the readings above are on — a sentence, because it is not a choice.
|
|
694
|
+
|
|
695
|
+
A control stood here offering to pick the zone. Nothing read what it stored, and the answer it
|
|
696
|
+
was asking for is the same for every schedule in the workspace, so the honest version of the
|
|
697
|
+
field is the fact it was hiding. Read-only on an existing schedule was the same claim in the
|
|
698
|
+
past tense, so that went with it.
|
|
699
|
+
-->
|
|
700
|
+
<p class="note">{t('schedule_tz_note')}</p>
|
|
748
701
|
|
|
749
702
|
<!-- Grace -->
|
|
750
703
|
<div class="pair">
|