@kernhq/module-hr 0.10.0 → 0.10.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/contract/attendance.d.ts +1 -0
- package/dist/contract/attendance.d.ts.map +1 -1
- package/dist/contract/attendance.js +9 -0
- package/dist/contract/attendance.js.map +1 -1
- package/dist/contract/capabilities.d.ts +16 -0
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +16 -0
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/router.d.ts +1 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/policy/accrual.d.ts +35 -4
- package/dist/policy/accrual.d.ts.map +1 -1
- package/dist/policy/accrual.js +141 -15
- package/dist/policy/accrual.js.map +1 -1
- package/dist/policy/working-time.d.ts +115 -9
- package/dist/policy/working-time.d.ts.map +1 -1
- package/dist/policy/working-time.js +120 -20
- package/dist/policy/working-time.js.map +1 -1
- package/dist/server/jobs.d.ts +14 -6
- package/dist/server/jobs.d.ts.map +1 -1
- package/dist/server/jobs.js +489 -144
- package/dist/server/jobs.js.map +1 -1
- package/dist/server/packs/index.d.ts +14 -0
- package/dist/server/packs/index.d.ts.map +1 -1
- package/dist/server/packs/index.js +19 -0
- package/dist/server/packs/index.js.map +1 -1
- package/dist/server/router.d.ts +1888 -111
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +154 -63
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +17 -0
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +81 -2
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/approvals.d.ts +0 -2
- package/dist/server/services/approvals.d.ts.map +1 -1
- package/dist/server/services/approvals.js +12 -4
- package/dist/server/services/approvals.js.map +1 -1
- package/dist/server/services/attendance.d.ts +120 -8
- package/dist/server/services/attendance.d.ts.map +1 -1
- package/dist/server/services/attendance.js +226 -14
- package/dist/server/services/attendance.js.map +1 -1
- package/dist/server/services/ledger.d.ts +0 -2
- package/dist/server/services/ledger.d.ts.map +1 -1
- package/dist/server/services/ledger.js +1 -2
- package/dist/server/services/ledger.js.map +1 -1
- package/dist/server/services/policies.d.ts +6 -2
- package/dist/server/services/policies.d.ts.map +1 -1
- package/dist/server/services/policies.js +7 -2
- package/dist/server/services/policies.js.map +1 -1
- package/migrations/0005_approval_requester.sql +11 -1
- package/migrations/0006_schedule_no_overlap.sql +85 -0
- package/migrations/0007_hot_path_indexes.sql +38 -0
- package/migrations/0009_beyond_cap_minutes.sql +15 -0
- package/migrations/meta/0005_snapshot.json +4047 -0
- package/migrations/meta/0007_snapshot.json +4225 -0
- package/migrations/meta/0009_snapshot.json +4231 -0
- package/migrations/meta/_journal.json +21 -0
- package/package.json +7 -7
- package/src/client/components/ClockControls.svelte +127 -20
- package/src/client/components/ClockControls.test.ts +446 -0
- package/src/client/components/DelegationDialog.svelte +12 -3
- package/src/client/components/LeaveRequestDialog.svelte +254 -25
- package/src/client/components/PersonFormDialog.svelte +53 -3
- package/src/client/components/PersonPanel.svelte +134 -26
- package/src/client/i18n.ts +5 -927
- package/src/client/messages.test.ts +154 -0
- package/src/client/messages.ts +3388 -0
- package/src/client/mock.ts +1576 -161
- package/src/client/module.ts +15 -1
- package/src/client/pages/ApprovalsPage.svelte +120 -46
- package/src/client/pages/AttendancePage.svelte +106 -20
- package/src/client/pages/DirectoryPage.svelte +192 -55
- package/src/client/pages/LeavePage.svelte +280 -25
- package/src/client/pages/OfficesPage.svelte +26 -8
- package/src/client/pages/leave-and-attendance.test.ts +588 -0
- package/src/client/query.ts +12 -0
- package/src/client/settings/CalendarsSettings.svelte +1363 -12
- package/src/client/settings/CapabilitiesSettings.svelte +276 -19
- package/src/client/settings/GeneralSettings.svelte +420 -0
- package/src/client/settings/LeaveSettings.svelte +884 -12
- package/src/client/settings/OfficesSettings.svelte +1182 -12
- package/src/client/settings/SchedulesSettings.svelte +1149 -12
- package/src/client/summary.ts +14 -7
- package/src/client/widgets/ApprovalsWidget.svelte +133 -20
- package/src/client/widgets/HeadcountWidget.svelte +74 -8
- package/src/client/widgets/LeaveBalanceWidget.svelte +40 -5
- package/src/client/widgets/WhosOutWidget.svelte +47 -8
- package/src/contract/attendance.ts +9 -0
- package/src/contract/capabilities.ts +17 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { ar, de, en, fa, hrMessageBundles, tr } from './messages.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The bundles, structurally — and the counted messages, behaviourally.
|
|
6
|
+
*
|
|
7
|
+
* Nothing else looks at these. A key present in English and missing in Persian type-checks, builds,
|
|
8
|
+
* lints and ships, and the first person to see it is the one reading `hr.leave_none` on a screen.
|
|
9
|
+
*/
|
|
10
|
+
const BUNDLES = { en, ar, de, fa, tr } as const
|
|
11
|
+
type Locale = keyof typeof BUNDLES
|
|
12
|
+
|
|
13
|
+
const placeholders = (s: string) => new Set([...s.matchAll(/\{(\w+)\}/g)].map((m) => m[1]!))
|
|
14
|
+
const forms = (v: unknown): string[] => (typeof v === 'string' ? [v] : Object.values(v as object))
|
|
15
|
+
|
|
16
|
+
describe('bundles', () => {
|
|
17
|
+
it('declares every locale the module claims to ship', () => {
|
|
18
|
+
expect(Object.keys(hrMessageBundles).sort()).toEqual(['ar', 'de', 'en', 'fa', 'tr'])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('has the same key set in every locale', () => {
|
|
22
|
+
const keys = Object.keys(en).sort()
|
|
23
|
+
for (const [locale, bundle] of Object.entries(BUNDLES)) {
|
|
24
|
+
expect({ locale, keys: Object.keys(bundle).sort() }).toEqual({ locale, keys })
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('namespaces every key, so two modules cannot collide in the merged map', () => {
|
|
29
|
+
for (const key of Object.keys(en)) expect(key.startsWith('hr.')).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A plural form may drop `{count}` — "يوم واحد" and "one day" both read better without the
|
|
34
|
+
* numeral — but dropping any *other* placeholder loses information. The Arabic `one` form of the
|
|
35
|
+
* approval summary that forgot `{range}` would render "one day," and nothing else.
|
|
36
|
+
*/
|
|
37
|
+
it('keeps every non-count placeholder in every plural form', () => {
|
|
38
|
+
for (const [key, value] of Object.entries(en)) {
|
|
39
|
+
const expected = placeholders(forms(value).join(' '))
|
|
40
|
+
expected.delete('count')
|
|
41
|
+
for (const [locale, bundle] of Object.entries(BUNDLES)) {
|
|
42
|
+
for (const form of forms(bundle[key as keyof typeof bundle])) {
|
|
43
|
+
const got = placeholders(form)
|
|
44
|
+
got.delete('count')
|
|
45
|
+
expect({ key, locale, form, got: [...got].sort() }).toEqual({
|
|
46
|
+
key,
|
|
47
|
+
locale,
|
|
48
|
+
form,
|
|
49
|
+
got: [...expected].sort(),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('never invents a placeholder the English string does not have', () => {
|
|
57
|
+
for (const [key, value] of Object.entries(en)) {
|
|
58
|
+
const allowed = placeholders(forms(value).join(' '))
|
|
59
|
+
for (const [locale, bundle] of Object.entries(BUNDLES))
|
|
60
|
+
for (const form of forms(bundle[key as keyof typeof bundle]))
|
|
61
|
+
for (const name of placeholders(form))
|
|
62
|
+
expect({ key, locale, name, known: allowed.has(name) }).toEqual({
|
|
63
|
+
key,
|
|
64
|
+
locale,
|
|
65
|
+
name,
|
|
66
|
+
known: true,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/** Arabic inflects six ways. A bundle with only one/other picks `other` for two, and reads wrong. */
|
|
72
|
+
it('gives every Arabic plural all six CLDR categories', () => {
|
|
73
|
+
const wanted = new Intl.PluralRules('ar').resolvedOptions().pluralCategories.sort()
|
|
74
|
+
for (const [key, value] of Object.entries(ar)) {
|
|
75
|
+
if (typeof value === 'string') continue
|
|
76
|
+
expect({ key, cats: Object.keys(value).sort() }).toEqual({ key, cats: wanted })
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('makes a plural of a key wherever English has one', () => {
|
|
81
|
+
for (const [key, value] of Object.entries(en)) {
|
|
82
|
+
if (typeof value === 'string') continue
|
|
83
|
+
for (const [locale, bundle] of Object.entries(BUNDLES))
|
|
84
|
+
expect({ key, locale, plural: typeof bundle[key as keyof typeof bundle] }).toEqual({
|
|
85
|
+
key,
|
|
86
|
+
locale,
|
|
87
|
+
plural: 'object',
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The counted messages, resolved the way the runtime resolves them.
|
|
95
|
+
*
|
|
96
|
+
* `t()` lives in `@kernhq/ui`, whose entry point pulls in Svelte components this package's test
|
|
97
|
+
* setup cannot transform — and adding a compiler plugin here to reach one function is a dependency
|
|
98
|
+
* and a lockfile refresh for very little. So these drive the *data* through the same
|
|
99
|
+
* `Intl.PluralRules` selection `selectPlural` performs, and `t()` itself is tested where it lives
|
|
100
|
+
* (`packages/ui/src/lib/i18n.test.ts`), which is the honest split.
|
|
101
|
+
*/
|
|
102
|
+
describe('counted messages', () => {
|
|
103
|
+
const pick = (locale: Locale, key: string, count: number): string => {
|
|
104
|
+
const value = BUNDLES[locale][key as keyof (typeof BUNDLES)[Locale]] as
|
|
105
|
+
| string
|
|
106
|
+
| Partial<Record<Intl.LDMLPluralRule, string>>
|
|
107
|
+
if (typeof value === 'string') return value
|
|
108
|
+
const category = new Intl.PluralRules(locale).select(count)
|
|
109
|
+
const form = value[category] ?? value.other
|
|
110
|
+
if (form === undefined) throw new Error(`${locale} ${key} has no form for ${count}`)
|
|
111
|
+
return form.replace(/\{count\}/g, new Intl.NumberFormat(locale).format(count))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The bug this describe block exists for: the approvals inbox read "1 days, Aug 6, 2026".
|
|
116
|
+
* The message was a flat string with a `{days}` placeholder, so nothing could select a form, and
|
|
117
|
+
* the count arrived pre-formatted as text so the digits stayed Latin on a Persian screen too.
|
|
118
|
+
*/
|
|
119
|
+
it('says "1 day" and "2 days" in English', () => {
|
|
120
|
+
expect(pick('en', 'hr.days', 1)).toBe('day')
|
|
121
|
+
expect(pick('en', 'hr.days', 2)).toBe('days')
|
|
122
|
+
expect(pick('en', 'hr.leave_would_cost', 1)).toBe('1 working day')
|
|
123
|
+
expect(pick('en', 'hr.leave_would_cost', 3)).toBe('3 working days')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('says "1 Tag" and "2 Tage" in German', () => {
|
|
127
|
+
expect(pick('de', 'hr.days', 1)).toBe('Tag')
|
|
128
|
+
expect(pick('de', 'hr.days', 2)).toBe('Tage')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('follows Arabic numeral agreement rather than one-or-many', () => {
|
|
132
|
+
// Singular, dual, 3-10 and 11-99 each take a different form in Modern Standard Arabic.
|
|
133
|
+
expect(pick('ar', 'hr.days', 1)).toBe('يوم')
|
|
134
|
+
expect(pick('ar', 'hr.days', 2)).toBe('يومان')
|
|
135
|
+
expect(pick('ar', 'hr.days', 5)).toBe('أيام')
|
|
136
|
+
expect(pick('ar', 'hr.days', 11)).toBe('يومًا')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('does not inflect the noun after a numeral in Turkish or Persian', () => {
|
|
140
|
+
// "5 gün", not "5 günler" — the plural suffix is wrong once a number is present.
|
|
141
|
+
expect(pick('tr', 'hr.days', 1)).toBe(pick('tr', 'hr.days', 5))
|
|
142
|
+
expect(pick('fa', 'hr.days', 1)).toBe(pick('fa', 'hr.days', 5))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('formats the number in the reader\u2019s own digits', () => {
|
|
146
|
+
expect(pick('fa', 'hr.leave_would_cost', 5)).toContain('۵')
|
|
147
|
+
expect(pick('en', 'hr.leave_would_cost', 5)).toContain('5')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('keeps the date range when the singular form drops the numeral', () => {
|
|
151
|
+
expect(pick('ar', 'hr.approval_summary_leave', 1)).toContain('{range}')
|
|
152
|
+
expect(pick('en', 'hr.approval_summary_leave', 1)).toBe('1 day, {range}')
|
|
153
|
+
})
|
|
154
|
+
})
|