@kernhq/module-hr 0.14.0 → 0.15.0
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/router.d.ts +14 -2
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +11 -2
- package/dist/contract/router.js.map +1 -1
- package/dist/server/jobs.d.ts +5 -2
- package/dist/server/jobs.d.ts.map +1 -1
- package/dist/server/jobs.js +72 -2
- package/dist/server/jobs.js.map +1 -1
- package/dist/server/router.d.ts +4 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +1 -1
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +85 -0
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +32 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/approvals.d.ts +150 -6
- package/dist/server/services/approvals.d.ts.map +1 -1
- package/dist/server/services/approvals.js +428 -26
- package/dist/server/services/approvals.js.map +1 -1
- package/migrations/0010_approval_timeouts.sql +28 -0
- package/migrations/meta/0010_snapshot.json +4263 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +1 -1
- package/src/client/components/DecisionDialog.svelte +99 -7
- package/src/client/components/PersonPanel.svelte +5 -5
- package/src/client/components/redaction.ts +28 -40
- package/src/client/messages.ts +68 -0
- package/src/client/mock.ts +256 -15
- package/src/client/pages/ApprovalsPage.svelte +256 -27
- package/src/client/pages/DirectoryPage.svelte +10 -7
- package/src/client/permissions.ts +6 -16
- package/src/client/query.ts +2 -2
- package/src/client/widgets/ApprovalsWidget.svelte +1 -1
- package/src/contract/router.ts +11 -2
|
@@ -61,9 +61,16 @@
|
|
|
61
61
|
{
|
|
62
62
|
"idx": 9,
|
|
63
63
|
"version": "7",
|
|
64
|
-
"when":
|
|
64
|
+
"when": 1787848170497,
|
|
65
65
|
"tag": "0009_beyond_cap_minutes",
|
|
66
66
|
"breakpoints": true
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"idx": 10,
|
|
70
|
+
"version": "7",
|
|
71
|
+
"when": 1787851246006,
|
|
72
|
+
"tag": "0010_approval_timeouts",
|
|
73
|
+
"breakpoints": true
|
|
67
74
|
}
|
|
68
75
|
]
|
|
69
76
|
}
|
package/package.json
CHANGED
|
@@ -1,34 +1,90 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Button, Dialog, Field, Textarea } from '@kernhq/ui'
|
|
2
|
+
import { Button, Dialog, Field, Select, Textarea } from '@kernhq/ui'
|
|
3
|
+
import { untrack } from 'svelte'
|
|
3
4
|
import { t } from '../i18n.js'
|
|
4
5
|
import type { ApprovalRequest } from '../index.js'
|
|
5
6
|
import { summarise } from '../summary.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Confirming a decision, stating what it does and
|
|
9
|
+
* Confirming a decision, stating what it does, to whom — and in whose name.
|
|
9
10
|
*
|
|
10
11
|
* Approving leave moves somebody's balance and puts them on the team calendar; rejecting it sends
|
|
11
12
|
* them a notification and changes nothing else. "Are you sure?" says none of that, so the body text
|
|
12
13
|
* is chosen per subject type and per position in the chain — a middle step passes the request on
|
|
13
14
|
* rather than settling it, and an approver who thinks they just granted the leave is a support call.
|
|
15
|
+
*
|
|
16
|
+
* `identities` is who the reader may file this decision as: themselves (`onBehalfOfId: null`), or
|
|
17
|
+
* somebody who delegated their approvals to them. The rule this dialog exists to keep is that a
|
|
18
|
+
* decision is never filed against a person nobody named — so with one identity it is stated, with
|
|
19
|
+
* several it is chosen, and with none confirmed the confirm button does nothing.
|
|
14
20
|
*/
|
|
15
21
|
interface Props {
|
|
16
22
|
request: ApprovalRequest | null
|
|
17
23
|
decision: 'approve' | 'reject'
|
|
24
|
+
/**
|
|
25
|
+
* Recomputed by the caller as names and delegations arrive, so it may change while the dialog is
|
|
26
|
+
* open. Omitting it means the reader decides as themselves and nothing else is possible — which
|
|
27
|
+
* is what a surface with no delegation in it, like the directory panel or the dashboard widget,
|
|
28
|
+
* is saying.
|
|
29
|
+
*/
|
|
30
|
+
identities?: Array<{ onBehalfOfId: string | null; label: string }>
|
|
18
31
|
pending: boolean
|
|
19
32
|
error: string | null
|
|
20
|
-
onConfirm: (comment: string) => void
|
|
33
|
+
onConfirm: (comment: string, onBehalfOfId: string | null) => void
|
|
21
34
|
onCancel: () => void
|
|
22
35
|
}
|
|
23
|
-
const { request, decision, pending, error, onConfirm, onCancel }: Props = $props()
|
|
36
|
+
const { request, decision, identities, pending, error, onConfirm, onCancel }: Props = $props()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One identity by default, and it is the reader's own.
|
|
40
|
+
*
|
|
41
|
+
* A `$derived` rather than a destructuring fallback: a fallback is re-evaluated on every read, so a
|
|
42
|
+
* fresh array and a fresh `t()` call would land in the middle of the equality checks below.
|
|
43
|
+
*/
|
|
44
|
+
const options = $derived(identities ?? [{ onBehalfOfId: null, label: t('approvals_as_self') }])
|
|
24
45
|
|
|
25
46
|
let comment = $state('')
|
|
47
|
+
/** '' until chosen, 'self' for the reader's own decision, otherwise the person id being acted for. */
|
|
48
|
+
let actingAs = $state('')
|
|
49
|
+
|
|
50
|
+
/** A `Select` needs a string, and `null` is a real identity here rather than the absence of one. */
|
|
51
|
+
const keyOf = (identity: { onBehalfOfId: string | null }) => identity.onBehalfOfId ?? 'self'
|
|
26
52
|
|
|
27
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Reset between requests: yesterday's note must not ride along on today's decision, and neither
|
|
55
|
+
* must the name it was filed against.
|
|
56
|
+
*
|
|
57
|
+
* Only `request?.id` is tracked. `identities` changes whenever a name or a delegation arrives, and
|
|
58
|
+
* an effect that read it would wipe a half-typed comment under the reader's hands.
|
|
59
|
+
*/
|
|
28
60
|
$effect(() => {
|
|
29
61
|
void request?.id
|
|
30
|
-
|
|
62
|
+
untrack(() => {
|
|
63
|
+
comment = ''
|
|
64
|
+
// Preselected only where it cannot be a guess: the reader's own name. Which colleague they
|
|
65
|
+
// meant is the one thing this dialog must never assume.
|
|
66
|
+
actingAs = options.some((i) => i.onBehalfOfId === null) ? 'self' : ''
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The identity this click would file under, or null while it is still an open question.
|
|
72
|
+
*
|
|
73
|
+
* A single identity needs no selection — including one that arrives after the dialog opened, which
|
|
74
|
+
* is why this is derived from `identities` rather than from what was preselected.
|
|
75
|
+
*/
|
|
76
|
+
const chosen = $derived.by(() => {
|
|
77
|
+
if (options.length === 1) return options[0]!
|
|
78
|
+
return options.find((i) => keyOf(i) === actingAs) ?? null
|
|
31
79
|
})
|
|
80
|
+
const onBehalf = $derived(chosen?.onBehalfOfId ? chosen : null)
|
|
81
|
+
|
|
82
|
+
const confirm = () => {
|
|
83
|
+
// Guarded rather than trusted to the disabled attribute: an unnamed identity must not become a
|
|
84
|
+
// decision filed as the caller, which is the failure this whole dialog is arranged against.
|
|
85
|
+
if (!chosen || pending) return
|
|
86
|
+
onConfirm(comment, chosen.onBehalfOfId)
|
|
87
|
+
}
|
|
32
88
|
|
|
33
89
|
const isLastStep = $derived(request ? request.currentStep >= Math.max(request.steps.length - 1, 0) : true)
|
|
34
90
|
|
|
@@ -59,6 +115,27 @@ const title = $derived(decision === 'approve' ? t('approve_confirm_title') : t('
|
|
|
59
115
|
</p>
|
|
60
116
|
{/if}
|
|
61
117
|
|
|
118
|
+
<!--
|
|
119
|
+
The choice comes before the comment, because it changes what the comment is attached to. It is
|
|
120
|
+
only rendered where there is something to choose — one identity is stated below, not offered.
|
|
121
|
+
-->
|
|
122
|
+
{#if options.length > 1}
|
|
123
|
+
<Field label={t('approvals_decide_as')} hint={t('approvals_decide_as_hint')} required>
|
|
124
|
+
{#snippet children(id)}
|
|
125
|
+
<Select
|
|
126
|
+
{id}
|
|
127
|
+
bind:value={actingAs}
|
|
128
|
+
placeholder={t('approvals_decide_as_pick')}
|
|
129
|
+
options={options.map((i) => ({ value: keyOf(i), label: i.label }))}
|
|
130
|
+
/>
|
|
131
|
+
{/snippet}
|
|
132
|
+
</Field>
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
{#if onBehalf}
|
|
136
|
+
<p class="behalf">{t('approvals_behalf_notice', { name: onBehalf.label })}</p>
|
|
137
|
+
{/if}
|
|
138
|
+
|
|
62
139
|
<Field label={t('approval_comment')} hint={t('approval_comment_hint')} error={error}>
|
|
63
140
|
{#snippet children(id)}
|
|
64
141
|
<Textarea {id} bind:value={comment} rows={3} />
|
|
@@ -70,7 +147,8 @@ const title = $derived(decision === 'approve' ? t('approve_confirm_title') : t('
|
|
|
70
147
|
<Button
|
|
71
148
|
variant={decision === 'reject' ? 'danger' : 'primary'}
|
|
72
149
|
loading={pending}
|
|
73
|
-
|
|
150
|
+
disabled={!chosen}
|
|
151
|
+
onclick={confirm}
|
|
74
152
|
>
|
|
75
153
|
{decision === 'approve' ? t('approve') : t('reject')}
|
|
76
154
|
</Button>
|
|
@@ -87,4 +165,18 @@ const title = $derived(decision === 'approve' ? t('approve_confirm_title') : t('
|
|
|
87
165
|
font-weight: 400;
|
|
88
166
|
color: var(--kern-ink-500);
|
|
89
167
|
}
|
|
168
|
+
/*
|
|
169
|
+
Tinted rather than muted. This is the sentence that says the decision will carry somebody else's
|
|
170
|
+
name, so it has to be the thing the eye lands on before the confirm button — and `--kern-ink-700`
|
|
171
|
+
on `--kern-info-tint` is a pair that holds its contrast in both themes.
|
|
172
|
+
*/
|
|
173
|
+
.behalf {
|
|
174
|
+
margin: 0 0 12px;
|
|
175
|
+
padding: 10px 12px;
|
|
176
|
+
border-radius: var(--kern-r-md2);
|
|
177
|
+
background: var(--kern-info-tint);
|
|
178
|
+
color: var(--kern-ink-700);
|
|
179
|
+
font-size: 12.5px;
|
|
180
|
+
line-height: 1.5;
|
|
181
|
+
}
|
|
90
182
|
</style>
|
|
@@ -22,7 +22,7 @@ import { hrKeys, isoDate } from '../query.js'
|
|
|
22
22
|
import PersonDocumentsSection from './PersonDocumentsSection.svelte'
|
|
23
23
|
import PersonJobSection from './PersonJobSection.svelte'
|
|
24
24
|
import PersonSensitiveSection from './PersonSensitiveSection.svelte'
|
|
25
|
-
import {
|
|
25
|
+
import { personnelWithheld } from './redaction.js'
|
|
26
26
|
import { explainRefusal } from './refusal.js'
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -195,11 +195,11 @@ const left = $derived(person?.status === 'terminated')
|
|
|
195
195
|
* Whether the server withheld this person's personnel fields from this reader.
|
|
196
196
|
*
|
|
197
197
|
* The four it nulls arrive as nulls, so an empty phone row on this panel says "no phone number" —
|
|
198
|
-
* a different and wrong fact.
|
|
199
|
-
*
|
|
200
|
-
* list rather than once per field.
|
|
198
|
+
* a different and wrong fact. The record carries the answer (`personnelHidden`, set where the
|
|
199
|
+
* nulling happens), so this is a read rather than an inference; the panel then explains itself once
|
|
200
|
+
* below the list rather than once per field.
|
|
201
201
|
*/
|
|
202
|
-
const withheld = $derived(person ?
|
|
202
|
+
const withheld = $derived(person ? personnelWithheld(person) : false)
|
|
203
203
|
/**
|
|
204
204
|
* And only where something on *this* panel is actually blank because of it. The two are the same
|
|
205
205
|
* thing while the client and the server agree about the reader's keys; tying the sentence to what
|
|
@@ -1,51 +1,39 @@
|
|
|
1
|
-
import { session } from '@kernhq/ui'
|
|
2
|
-
import { canHr, canSeeFullRecords } from '../permissions.js'
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Whether the personnel fields on a card were withheld, or are genuinely empty.
|
|
6
3
|
*
|
|
7
4
|
* `HrAccessService` nulls four of them — personal email, phone, hire date, termination date — for
|
|
8
5
|
* anybody outside the reader's record scope, and nulls them **in place**: a redacted card parses as
|
|
9
|
-
* an ordinary `Person`, so a blank phone number
|
|
10
|
-
* showing you this" or "this person never gave us one". Those are two different facts
|
|
11
|
-
* colleague, and printing the second when the first is true is the defect this exists to
|
|
12
|
-
*
|
|
13
|
-
* Nothing on the record says which it is, so this reconstructs what it can from the reader's own
|
|
14
|
-
* keys — and answers `unknown` rather than guessing where it cannot:
|
|
6
|
+
* an ordinary `Person`, so a blank phone number would otherwise arrive at a screen meaning either
|
|
7
|
+
* "we are not showing you this" or "this person never gave us one". Those are two different facts
|
|
8
|
+
* about a colleague, and printing the second when the first is true is the defect this exists to
|
|
9
|
+
* stop.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* fields they could not see. Everybody reads their own record besides, holding nothing at all.
|
|
20
|
-
* - `withheld` — the reader holds none of the three widening keys, so every card but their own came
|
|
21
|
-
* back with all four nulled. Certain, and the common case: `hr.person.view` is a member default
|
|
22
|
-
* and most colleagues hold nothing above it.
|
|
23
|
-
* - `unknown` — the reader holds `view_team` or `view_office`. **Which** people those cover is
|
|
24
|
-
* resolved on the server from the org chart — headship of a unit or an office, direct reports, as
|
|
25
|
-
* of today — and none of that reaches the client. A screen renders the field exactly as it
|
|
26
|
-
* arrived rather than labelling a genuinely empty phone number "Hidden".
|
|
11
|
+
* **The record says which, and this only reads it.** `Person.personnelHidden` is set in `forViewer`,
|
|
12
|
+
* the single place that does the nulling — so the answer comes from the code that made the decision
|
|
13
|
+
* rather than from a client re-deriving it.
|
|
27
14
|
*
|
|
28
|
-
* That
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
15
|
+
* That matters because the client cannot re-derive it. This helper used to infer the answer from
|
|
16
|
+
* the reader's own permission keys, which works for the two extremes — somebody holding
|
|
17
|
+
* `hr.person.view_all` or `hr.person.manage` has nothing hidden, somebody holding none of the
|
|
18
|
+
* widening keys has everything but their own record hidden — and fails in the middle. A line
|
|
19
|
+
* manager or a country HR person holds `view_team` or `view_office`, and **which** people those
|
|
20
|
+
* cover is resolved on the server from the org chart: headship of a unit over an ltree subtree,
|
|
21
|
+
* headship of an office, `manager_person_id`, all as of today, across rows the directory never
|
|
22
|
+
* fetches. Inferring it would have meant a second implementation of that resolution, and "all four
|
|
23
|
+
* fields are null" is not evidence — a person really can have no personal email, no phone and no
|
|
24
|
+
* recorded hire date.
|
|
32
25
|
*/
|
|
33
|
-
export type PersonnelVisibility = 'full' | 'withheld' | 'unknown'
|
|
34
|
-
|
|
35
|
-
/** Enough of a person to answer for — a directory row, a whole record, an office roster entry. */
|
|
36
26
|
export interface PersonnelSubject {
|
|
37
|
-
/**
|
|
38
|
-
|
|
27
|
+
/**
|
|
28
|
+
* True when the server withheld the personnel fields on this record.
|
|
29
|
+
*
|
|
30
|
+
* Optional so a caller can pass a row that predates the field — `dev:mock` fixtures, an older
|
|
31
|
+
* cached payload — and get `false` rather than a crash. Absent means "not withheld", which is the
|
|
32
|
+
* safe direction: a screen that fails to mark a hidden field shows a blank, while one that marks
|
|
33
|
+
* an empty field asserts something false about a colleague.
|
|
34
|
+
*/
|
|
35
|
+
personnelHidden?: boolean
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (canHr('personViewAll') || canHr('personManage')) return 'full'
|
|
44
|
-
// Identity, never a grant. Plenty of employees have no account, so a null on either side is
|
|
45
|
-
// nobody's match rather than everybody's.
|
|
46
|
-
const userId = session.user?.id
|
|
47
|
-
if (userId && person.userId === userId) return 'full'
|
|
48
|
-
// `view_all` is answered above, so what is left of that union is team-or-office scope: a set of
|
|
49
|
-
// people only the server can name.
|
|
50
|
-
return canSeeFullRecords() ? 'unknown' : 'withheld'
|
|
51
|
-
}
|
|
38
|
+
/** Were this card's personnel fields withheld from the reader? */
|
|
39
|
+
export const personnelWithheld = (person: PersonnelSubject): boolean => person.personnelHidden === true
|
package/src/client/messages.ts
CHANGED
|
@@ -232,14 +232,25 @@ export const en: Record<string, Message> = {
|
|
|
232
232
|
'hr.approval_summary_overtime': 'Overtime on {date}',
|
|
233
233
|
'hr.approval_summary_regularization': 'Attendance correction for {date}',
|
|
234
234
|
'hr.approvals_actions': 'Actions',
|
|
235
|
+
'hr.approvals_as_self': 'Yourself',
|
|
236
|
+
'hr.approvals_behalf_notice':
|
|
237
|
+
'You are deciding for {name}. It is recorded in their name, with yours beside it.',
|
|
238
|
+
'hr.approvals_behalf_pick': 'Whose decision?',
|
|
239
|
+
'hr.approvals_behalf_someone': 'another approver',
|
|
235
240
|
'hr.approvals_chains_hint': 'Who signs what is set in the module settings.',
|
|
241
|
+
'hr.approvals_decide_as': 'Deciding as',
|
|
242
|
+
'hr.approvals_decide_as_hint': 'The decision is recorded against the person you choose.',
|
|
243
|
+
'hr.approvals_decide_as_pick': 'Choose a person',
|
|
236
244
|
'hr.approvals_decided': 'Decided',
|
|
245
|
+
'hr.approvals_decided_behalf': 'Decided by {actor} for {person}',
|
|
237
246
|
'hr.approvals_decided_none': 'Nothing decided yet',
|
|
238
247
|
'hr.approvals_decided_none_desc': 'Requests you have approved or rejected appear here.',
|
|
239
248
|
'hr.approvals_error': 'Your approvals could not be loaded',
|
|
240
249
|
'hr.approvals_for': 'for {name}',
|
|
250
|
+
'hr.approvals_later_step': 'Not your step yet',
|
|
241
251
|
'hr.approvals_none': 'Nothing waiting on you',
|
|
242
252
|
'hr.approvals_none_desc': 'Requests needing your decision appear here.',
|
|
253
|
+
'hr.approvals_on_behalf': 'On behalf of {name}',
|
|
243
254
|
'hr.approvals_request': 'Request',
|
|
244
255
|
'hr.approvals_requested': 'Requested',
|
|
245
256
|
'hr.approvals_requested_by': 'Requested by',
|
|
@@ -247,6 +258,7 @@ export const en: Record<string, Message> = {
|
|
|
247
258
|
'hr.approvals_step_of': 'Step {n} of {total}',
|
|
248
259
|
'hr.approvals_title': 'Approvals',
|
|
249
260
|
'hr.approvals_waiting': 'Waiting on you',
|
|
261
|
+
'hr.approvals_you_decided': 'You have decided',
|
|
250
262
|
'hr.approve': 'Approve',
|
|
251
263
|
'hr.approve_confirm_attendance': 'The corrected punches are applied and that day is recomputed.',
|
|
252
264
|
'hr.approve_confirm_leave': 'The days come off their balance and the team calendar shows them as away.',
|
|
@@ -641,6 +653,8 @@ export const en: Record<string, Message> = {
|
|
|
641
653
|
'hr.cmd_directory': 'Open the people directory',
|
|
642
654
|
'hr.cmd_request_leave': 'Request time off',
|
|
643
655
|
'hr.days': { one: 'day', other: 'days' },
|
|
656
|
+
'hr.decide_behalf_error':
|
|
657
|
+
'The decision was not recorded. The delegation may have ended — reload and try again.',
|
|
644
658
|
'hr.decide_error': 'The decision could not be recorded',
|
|
645
659
|
'hr.delegate': 'Delegate',
|
|
646
660
|
'hr.delegate_add': 'Add delegation',
|
|
@@ -1656,14 +1670,24 @@ export const ar: Record<string, Message> = {
|
|
|
1656
1670
|
'hr.approval_summary_overtime': 'عمل إضافي في {date}',
|
|
1657
1671
|
'hr.approval_summary_regularization': 'تصحيح حضور ليوم {date}',
|
|
1658
1672
|
'hr.approvals_actions': 'الإجراءات',
|
|
1673
|
+
'hr.approvals_as_self': 'أنت',
|
|
1674
|
+
'hr.approvals_behalf_notice': 'أنت تقرّر بدلاً من {name}. يُسجَّل القرار باسمه ويُذكر اسمك إلى جانبه.',
|
|
1675
|
+
'hr.approvals_behalf_pick': 'قرار مَن؟',
|
|
1676
|
+
'hr.approvals_behalf_someone': 'مُعتمِد آخر',
|
|
1659
1677
|
'hr.approvals_chains_hint': 'يُحدَّد من يوقّع على ماذا في إعدادات الوحدة.',
|
|
1678
|
+
'hr.approvals_decide_as': 'باسم مَن',
|
|
1679
|
+
'hr.approvals_decide_as_hint': 'يُسجَّل القرار باسم الشخص الذي تختاره.',
|
|
1680
|
+
'hr.approvals_decide_as_pick': 'اختر شخصًا',
|
|
1660
1681
|
'hr.approvals_decided': 'تم البتّ فيها',
|
|
1682
|
+
'hr.approvals_decided_behalf': 'قرّر {actor} نيابةً عن {person}',
|
|
1661
1683
|
'hr.approvals_decided_none': 'لم يتم البتّ في شيء بعد',
|
|
1662
1684
|
'hr.approvals_decided_none_desc': 'تظهر هنا الطلبات التي وافقت عليها أو رفضتها.',
|
|
1663
1685
|
'hr.approvals_error': 'تعذّر تحميل موافقاتك',
|
|
1664
1686
|
'hr.approvals_for': 'لـ {name}',
|
|
1687
|
+
'hr.approvals_later_step': 'ليست خطوتك بعد',
|
|
1665
1688
|
'hr.approvals_none': 'لا شيء بانتظارك',
|
|
1666
1689
|
'hr.approvals_none_desc': 'تظهر هنا الطلبات التي تنتظر قرارك.',
|
|
1690
|
+
'hr.approvals_on_behalf': 'نيابةً عن {name}',
|
|
1667
1691
|
'hr.approvals_request': 'الطلب',
|
|
1668
1692
|
'hr.approvals_requested': 'تاريخ الطلب',
|
|
1669
1693
|
'hr.approvals_requested_by': 'مقدّم الطلب',
|
|
@@ -1671,6 +1695,7 @@ export const ar: Record<string, Message> = {
|
|
|
1671
1695
|
'hr.approvals_step_of': 'الخطوة {n} من {total}',
|
|
1672
1696
|
'hr.approvals_title': 'الموافقات',
|
|
1673
1697
|
'hr.approvals_waiting': 'بانتظارك',
|
|
1698
|
+
'hr.approvals_you_decided': 'لقد قرّرت',
|
|
1674
1699
|
'hr.approve': 'موافقة',
|
|
1675
1700
|
'hr.approve_confirm_attendance': 'تُطبَّق التسجيلات المصحّحة ويُعاد احتساب ذلك اليوم.',
|
|
1676
1701
|
'hr.approve_confirm_leave': 'تُخصم الأيام من رصيده ويظهر في تقويم الفريق كغائب.',
|
|
@@ -2088,6 +2113,7 @@ export const ar: Record<string, Message> = {
|
|
|
2088
2113
|
'hr.cmd_directory': 'فتح دليل الأشخاص',
|
|
2089
2114
|
'hr.cmd_request_leave': 'طلب إجازة',
|
|
2090
2115
|
'hr.days': { zero: 'يوم', one: 'يوم', two: 'يومان', few: 'أيام', many: 'يومًا', other: 'يوم' },
|
|
2116
|
+
'hr.decide_behalf_error': 'لم يُسجَّل القرار. ربما انتهى التفويض — أعد تحميل الصفحة وحاول مرة أخرى.',
|
|
2091
2117
|
'hr.decide_error': 'تعذّر تسجيل القرار',
|
|
2092
2118
|
'hr.delegate': 'تفويض',
|
|
2093
2119
|
'hr.delegate_add': 'إضافة تفويض',
|
|
@@ -3127,14 +3153,25 @@ export const de: Record<string, Message> = {
|
|
|
3127
3153
|
'hr.approval_summary_overtime': 'Überstunden am {date}',
|
|
3128
3154
|
'hr.approval_summary_regularization': 'Zeitkorrektur für {date}',
|
|
3129
3155
|
'hr.approvals_actions': 'Aktionen',
|
|
3156
|
+
'hr.approvals_as_self': 'Sie selbst',
|
|
3157
|
+
'hr.approvals_behalf_notice':
|
|
3158
|
+
'Sie entscheiden für {name}. Die Entscheidung wird in deren Namen erfasst, Ihr Name steht daneben.',
|
|
3159
|
+
'hr.approvals_behalf_pick': 'Wessen Entscheidung?',
|
|
3160
|
+
'hr.approvals_behalf_someone': 'eine andere genehmigende Person',
|
|
3130
3161
|
'hr.approvals_chains_hint': 'Wer was freigibt, wird in den Moduleinstellungen festgelegt.',
|
|
3162
|
+
'hr.approvals_decide_as': 'Entscheiden als',
|
|
3163
|
+
'hr.approvals_decide_as_hint': 'Die Entscheidung wird der gewählten Person zugeschrieben.',
|
|
3164
|
+
'hr.approvals_decide_as_pick': 'Person wählen',
|
|
3131
3165
|
'hr.approvals_decided': 'Entschieden',
|
|
3166
|
+
'hr.approvals_decided_behalf': 'Von {actor} für {person} entschieden',
|
|
3132
3167
|
'hr.approvals_decided_none': 'Noch nichts entschieden',
|
|
3133
3168
|
'hr.approvals_decided_none_desc': 'Hier erscheinen Anträge, die Sie genehmigt oder abgelehnt haben.',
|
|
3134
3169
|
'hr.approvals_error': 'Ihre Freigaben konnten nicht geladen werden',
|
|
3135
3170
|
'hr.approvals_for': 'für {name}',
|
|
3171
|
+
'hr.approvals_later_step': 'Noch nicht Ihr Schritt',
|
|
3136
3172
|
'hr.approvals_none': 'Keine offenen Freigaben',
|
|
3137
3173
|
'hr.approvals_none_desc': 'Anträge, die auf Ihre Entscheidung warten, erscheinen hier.',
|
|
3174
|
+
'hr.approvals_on_behalf': 'Im Namen von {name}',
|
|
3138
3175
|
'hr.approvals_request': 'Antrag',
|
|
3139
3176
|
'hr.approvals_requested': 'Eingereicht',
|
|
3140
3177
|
'hr.approvals_requested_by': 'Beantragt von',
|
|
@@ -3142,6 +3179,7 @@ export const de: Record<string, Message> = {
|
|
|
3142
3179
|
'hr.approvals_step_of': 'Schritt {n} von {total}',
|
|
3143
3180
|
'hr.approvals_title': 'Freigaben',
|
|
3144
3181
|
'hr.approvals_waiting': 'Wartet auf Sie',
|
|
3182
|
+
'hr.approvals_you_decided': 'Sie haben entschieden',
|
|
3145
3183
|
'hr.approve': 'Genehmigen',
|
|
3146
3184
|
'hr.approve_confirm_attendance':
|
|
3147
3185
|
'Die korrigierten Buchungen werden übernommen und der Tag wird neu berechnet.',
|
|
@@ -3554,6 +3592,8 @@ export const de: Record<string, Message> = {
|
|
|
3554
3592
|
'hr.cmd_directory': 'Personenverzeichnis öffnen',
|
|
3555
3593
|
'hr.cmd_request_leave': 'Abwesenheit beantragen',
|
|
3556
3594
|
'hr.days': { one: 'Tag', other: 'Tage' },
|
|
3595
|
+
'hr.decide_behalf_error':
|
|
3596
|
+
'Die Entscheidung wurde nicht erfasst. Die Vertretung ist möglicherweise abgelaufen — neu laden und erneut versuchen.',
|
|
3557
3597
|
'hr.decide_error': 'Die Entscheidung konnte nicht gespeichert werden',
|
|
3558
3598
|
'hr.delegate': 'Vertretung',
|
|
3559
3599
|
'hr.delegate_add': 'Vertretung hinzufügen',
|
|
@@ -4527,14 +4567,25 @@ export const fa: Record<string, Message> = {
|
|
|
4527
4567
|
'hr.approval_summary_overtime': 'اضافهکاری در {date}',
|
|
4528
4568
|
'hr.approval_summary_regularization': 'اصلاح حضور برای {date}',
|
|
4529
4569
|
'hr.approvals_actions': 'کنشها',
|
|
4570
|
+
'hr.approvals_as_self': 'خودتان',
|
|
4571
|
+
'hr.approvals_behalf_notice':
|
|
4572
|
+
'شما بهجای {name} تصمیم میگیرید. تصمیم به نام او و نام شما در کنارش ثبت میشود.',
|
|
4573
|
+
'hr.approvals_behalf_pick': 'تصمیم از طرف چه کسی؟',
|
|
4574
|
+
'hr.approvals_behalf_someone': 'تأییدکنندهٔ دیگر',
|
|
4530
4575
|
'hr.approvals_chains_hint': 'اینکه چهکسی چهچیزی را امضا کند در تنظیمات ماژول تعیین میشود.',
|
|
4576
|
+
'hr.approvals_decide_as': 'تصمیم به نام',
|
|
4577
|
+
'hr.approvals_decide_as_hint': 'تصمیم به نام فردی که انتخاب میکنید ثبت میشود.',
|
|
4578
|
+
'hr.approvals_decide_as_pick': 'یک نفر را انتخاب کنید',
|
|
4531
4579
|
'hr.approvals_decided': 'تصمیمگرفتهشده',
|
|
4580
|
+
'hr.approvals_decided_behalf': '{actor} بهجای {person} تصمیم گرفت',
|
|
4532
4581
|
'hr.approvals_decided_none': 'هنوز چیزی تصمیمگیری نشده',
|
|
4533
4582
|
'hr.approvals_decided_none_desc': 'درخواستهایی که تأیید یا رد کردهاید اینجا نمایش داده میشوند.',
|
|
4534
4583
|
'hr.approvals_error': 'تأییدهای شما بارگیری نشد',
|
|
4535
4584
|
'hr.approvals_for': 'برای {name}',
|
|
4585
|
+
'hr.approvals_later_step': 'هنوز نوبت شما نیست',
|
|
4536
4586
|
'hr.approvals_none': 'چیزی منتظر تصمیم شما نیست',
|
|
4537
4587
|
'hr.approvals_none_desc': 'درخواستهایی که باید تصمیم بگیرید اینجا میآیند.',
|
|
4588
|
+
'hr.approvals_on_behalf': 'به نمایندگی از {name}',
|
|
4538
4589
|
'hr.approvals_request': 'درخواست',
|
|
4539
4590
|
'hr.approvals_requested': 'زمان درخواست',
|
|
4540
4591
|
'hr.approvals_requested_by': 'درخواستدهنده',
|
|
@@ -4542,6 +4593,7 @@ export const fa: Record<string, Message> = {
|
|
|
4542
4593
|
'hr.approvals_step_of': 'مرحلهٔ {n} از {total}',
|
|
4543
4594
|
'hr.approvals_title': 'تأییدها',
|
|
4544
4595
|
'hr.approvals_waiting': 'در انتظار شما',
|
|
4596
|
+
'hr.approvals_you_decided': 'شما تصمیم گرفتهاید',
|
|
4545
4597
|
'hr.approve': 'تأیید',
|
|
4546
4598
|
'hr.approve_confirm_attendance': 'ثبتهای اصلاحشده اعمال میشود و آن روز دوباره محاسبه میشود.',
|
|
4547
4599
|
'hr.approve_confirm_leave': 'روزها از موجودی او کم میشود و در تقویم تیم غایب نمایش داده میشود.',
|
|
@@ -4934,6 +4986,8 @@ export const fa: Record<string, Message> = {
|
|
|
4934
4986
|
'hr.cmd_directory': 'باز کردن فهرست افراد',
|
|
4935
4987
|
'hr.cmd_request_leave': 'درخواست مرخصی',
|
|
4936
4988
|
'hr.days': { one: 'روز', other: 'روز' },
|
|
4989
|
+
'hr.decide_behalf_error':
|
|
4990
|
+
'تصمیم ثبت نشد. شاید واگذاری به پایان رسیده باشد — صفحه را دوباره بارگذاری کنید و دوباره تلاش کنید.',
|
|
4937
4991
|
'hr.decide_error': 'تصمیم ثبت نشد',
|
|
4938
4992
|
'hr.delegate': 'واگذاری',
|
|
4939
4993
|
'hr.delegate_add': 'افزودن واگذاری',
|
|
@@ -5883,14 +5937,25 @@ export const tr: Record<string, Message> = {
|
|
|
5883
5937
|
'hr.approval_summary_overtime': '{date} tarihinde fazla mesai',
|
|
5884
5938
|
'hr.approval_summary_regularization': '{date} için devam düzeltmesi',
|
|
5885
5939
|
'hr.approvals_actions': 'İşlemler',
|
|
5940
|
+
'hr.approvals_as_self': 'Kendiniz',
|
|
5941
|
+
'hr.approvals_behalf_notice':
|
|
5942
|
+
'{name} adına karar veriyorsunuz. Karar onun adına, sizin adınız da yanında kaydedilir.',
|
|
5943
|
+
'hr.approvals_behalf_pick': 'Kimin kararı?',
|
|
5944
|
+
'hr.approvals_behalf_someone': 'başka bir onaylayan',
|
|
5886
5945
|
'hr.approvals_chains_hint': 'Neyi kimin imzalayacağı modül ayarlarından belirlenir.',
|
|
5946
|
+
'hr.approvals_decide_as': 'Kimin adına',
|
|
5947
|
+
'hr.approvals_decide_as_hint': 'Karar, seçtiğiniz kişinin adına kaydedilir.',
|
|
5948
|
+
'hr.approvals_decide_as_pick': 'Bir kişi seçin',
|
|
5887
5949
|
'hr.approvals_decided': 'Karara bağlananlar',
|
|
5950
|
+
'hr.approvals_decided_behalf': '{person} adına {actor} karar verdi',
|
|
5888
5951
|
'hr.approvals_decided_none': 'Henüz karara bağlanan bir şey yok',
|
|
5889
5952
|
'hr.approvals_decided_none_desc': 'Onayladığınız veya reddettiğiniz talepler burada görünür.',
|
|
5890
5953
|
'hr.approvals_error': 'Onaylarınız yüklenemedi',
|
|
5891
5954
|
'hr.approvals_for': '{name} için',
|
|
5955
|
+
'hr.approvals_later_step': 'Henüz sizin adımınız değil',
|
|
5892
5956
|
'hr.approvals_none': 'Sizi bekleyen bir şey yok',
|
|
5893
5957
|
'hr.approvals_none_desc': 'Kararınızı bekleyen talepler burada görünür.',
|
|
5958
|
+
'hr.approvals_on_behalf': '{name} adına',
|
|
5894
5959
|
'hr.approvals_request': 'Talep',
|
|
5895
5960
|
'hr.approvals_requested': 'Talep edildi',
|
|
5896
5961
|
'hr.approvals_requested_by': 'Talep eden',
|
|
@@ -5898,6 +5963,7 @@ export const tr: Record<string, Message> = {
|
|
|
5898
5963
|
'hr.approvals_step_of': '{total} adımdan {n}. adım',
|
|
5899
5964
|
'hr.approvals_title': 'Onaylar',
|
|
5900
5965
|
'hr.approvals_waiting': 'Sizi bekleyenler',
|
|
5966
|
+
'hr.approvals_you_decided': 'Kararınızı verdiniz',
|
|
5901
5967
|
'hr.approve': 'Onayla',
|
|
5902
5968
|
'hr.approve_confirm_attendance': 'Düzeltilen giriş-çıkışlar uygulanır ve o gün yeniden hesaplanır.',
|
|
5903
5969
|
'hr.approve_confirm_leave': 'Günler bakiyesinden düşer ve takım takviminde izinli görünür.',
|
|
@@ -6288,6 +6354,8 @@ export const tr: Record<string, Message> = {
|
|
|
6288
6354
|
'hr.cmd_directory': 'Kişi rehberini aç',
|
|
6289
6355
|
'hr.cmd_request_leave': 'İzin talep et',
|
|
6290
6356
|
'hr.days': { one: 'gün', other: 'gün' },
|
|
6357
|
+
'hr.decide_behalf_error':
|
|
6358
|
+
'Karar kaydedilmedi. Yetki devri sona ermiş olabilir — sayfayı yenileyip yeniden deneyin.',
|
|
6291
6359
|
'hr.decide_error': 'Karar kaydedilemedi',
|
|
6292
6360
|
'hr.delegate': 'Devret',
|
|
6293
6361
|
'hr.delegate_add': 'Devir ekle',
|