@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.
Files changed (90) hide show
  1. package/dist/contract/attendance.d.ts +1 -0
  2. package/dist/contract/attendance.d.ts.map +1 -1
  3. package/dist/contract/attendance.js +9 -0
  4. package/dist/contract/attendance.js.map +1 -1
  5. package/dist/contract/capabilities.d.ts +16 -0
  6. package/dist/contract/capabilities.d.ts.map +1 -1
  7. package/dist/contract/capabilities.js +16 -0
  8. package/dist/contract/capabilities.js.map +1 -1
  9. package/dist/contract/router.d.ts +1 -0
  10. package/dist/contract/router.d.ts.map +1 -1
  11. package/dist/policy/accrual.d.ts +35 -4
  12. package/dist/policy/accrual.d.ts.map +1 -1
  13. package/dist/policy/accrual.js +141 -15
  14. package/dist/policy/accrual.js.map +1 -1
  15. package/dist/policy/working-time.d.ts +115 -9
  16. package/dist/policy/working-time.d.ts.map +1 -1
  17. package/dist/policy/working-time.js +120 -20
  18. package/dist/policy/working-time.js.map +1 -1
  19. package/dist/server/jobs.d.ts +14 -6
  20. package/dist/server/jobs.d.ts.map +1 -1
  21. package/dist/server/jobs.js +489 -144
  22. package/dist/server/jobs.js.map +1 -1
  23. package/dist/server/packs/index.d.ts +14 -0
  24. package/dist/server/packs/index.d.ts.map +1 -1
  25. package/dist/server/packs/index.js +19 -0
  26. package/dist/server/packs/index.js.map +1 -1
  27. package/dist/server/router.d.ts +1888 -111
  28. package/dist/server/router.d.ts.map +1 -1
  29. package/dist/server/router.js +154 -63
  30. package/dist/server/router.js.map +1 -1
  31. package/dist/server/schema.d.ts +17 -0
  32. package/dist/server/schema.d.ts.map +1 -1
  33. package/dist/server/schema.js +81 -2
  34. package/dist/server/schema.js.map +1 -1
  35. package/dist/server/services/approvals.d.ts +0 -2
  36. package/dist/server/services/approvals.d.ts.map +1 -1
  37. package/dist/server/services/approvals.js +12 -4
  38. package/dist/server/services/approvals.js.map +1 -1
  39. package/dist/server/services/attendance.d.ts +120 -8
  40. package/dist/server/services/attendance.d.ts.map +1 -1
  41. package/dist/server/services/attendance.js +226 -14
  42. package/dist/server/services/attendance.js.map +1 -1
  43. package/dist/server/services/ledger.d.ts +0 -2
  44. package/dist/server/services/ledger.d.ts.map +1 -1
  45. package/dist/server/services/ledger.js +1 -2
  46. package/dist/server/services/ledger.js.map +1 -1
  47. package/dist/server/services/policies.d.ts +6 -2
  48. package/dist/server/services/policies.d.ts.map +1 -1
  49. package/dist/server/services/policies.js +7 -2
  50. package/dist/server/services/policies.js.map +1 -1
  51. package/migrations/0005_approval_requester.sql +11 -1
  52. package/migrations/0006_schedule_no_overlap.sql +85 -0
  53. package/migrations/0007_hot_path_indexes.sql +38 -0
  54. package/migrations/0009_beyond_cap_minutes.sql +15 -0
  55. package/migrations/meta/0005_snapshot.json +4047 -0
  56. package/migrations/meta/0007_snapshot.json +4225 -0
  57. package/migrations/meta/0009_snapshot.json +4231 -0
  58. package/migrations/meta/_journal.json +21 -0
  59. package/package.json +7 -7
  60. package/src/client/components/ClockControls.svelte +127 -20
  61. package/src/client/components/ClockControls.test.ts +446 -0
  62. package/src/client/components/DelegationDialog.svelte +12 -3
  63. package/src/client/components/LeaveRequestDialog.svelte +254 -25
  64. package/src/client/components/PersonFormDialog.svelte +53 -3
  65. package/src/client/components/PersonPanel.svelte +134 -26
  66. package/src/client/i18n.ts +5 -927
  67. package/src/client/messages.test.ts +154 -0
  68. package/src/client/messages.ts +3388 -0
  69. package/src/client/mock.ts +1576 -161
  70. package/src/client/module.ts +15 -1
  71. package/src/client/pages/ApprovalsPage.svelte +120 -46
  72. package/src/client/pages/AttendancePage.svelte +106 -20
  73. package/src/client/pages/DirectoryPage.svelte +192 -55
  74. package/src/client/pages/LeavePage.svelte +280 -25
  75. package/src/client/pages/OfficesPage.svelte +26 -8
  76. package/src/client/pages/leave-and-attendance.test.ts +588 -0
  77. package/src/client/query.ts +12 -0
  78. package/src/client/settings/CalendarsSettings.svelte +1363 -12
  79. package/src/client/settings/CapabilitiesSettings.svelte +276 -19
  80. package/src/client/settings/GeneralSettings.svelte +420 -0
  81. package/src/client/settings/LeaveSettings.svelte +884 -12
  82. package/src/client/settings/OfficesSettings.svelte +1182 -12
  83. package/src/client/settings/SchedulesSettings.svelte +1149 -12
  84. package/src/client/summary.ts +14 -7
  85. package/src/client/widgets/ApprovalsWidget.svelte +133 -20
  86. package/src/client/widgets/HeadcountWidget.svelte +74 -8
  87. package/src/client/widgets/LeaveBalanceWidget.svelte +40 -5
  88. package/src/client/widgets/WhosOutWidget.svelte +47 -8
  89. package/src/contract/attendance.ts +9 -0
  90. 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
+ })