@nuxt-customer-portal/timesheets 0.3.2 → 0.4.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/CHANGELOG.md +41 -0
- package/app/components/TimesheetsActivityForm.vue +6 -1
- package/app/components/TimesheetsAdminApprovals.vue +4 -4
- package/app/components/TimesheetsAdminRates.vue +3 -1
- package/app/components/TimesheetsClientSettingsPanel.vue +3 -1
- package/app/components/TimesheetsClientTimesheetBrowser.vue +1 -1
- package/app/components/TimesheetsProjectDetail.vue +8 -1
- package/app/components/TimesheetsProjectForm.vue +6 -1
- package/app/components/TimesheetsWorkspaceSettingsForm.vue +3 -2
- package/app/pages/timesheets/index.vue +16 -5
- package/package.json +5 -5
- package/portal.manifest.mjs +1 -1
- package/server/utils/timesheet-email.ts +9 -2
- package/server/utils/timesheet-validation.ts +2 -1
- package/shared/emails.ts +41 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @nuxt-customer-portal/timesheets
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 59c8921: Support organization and personal clients with optional verified self-registration, single-person access safeguards, and shared provider, client, and user timezone preferences. Preserve existing B2B defaults, module references, invoice history, and Timesheets timezone behavior.
|
|
8
|
+
|
|
9
|
+
Collect and store first and last names during signup and personal onboarding. Route ordinary personal registration and verified users without memberships or pending invitations through onboarding.
|
|
10
|
+
|
|
11
|
+
Allow users to edit first name, last name, and an independent display name in profile settings.
|
|
12
|
+
|
|
13
|
+
Synchronize display-name edits with the linked private client name and current billing name in the same transaction, preserving organizations and historical invoices.
|
|
14
|
+
|
|
15
|
+
Let private clients edit their own billing address in profile settings, independently of the active organization, with server-side ownership checks.
|
|
16
|
+
|
|
17
|
+
Private clients use their own invoice email without contact persons; hide contact management and reject private-client contact creation or selection server-side.
|
|
18
|
+
|
|
19
|
+
Add a B2C-only Invite private client action to the Users page, supporting new and existing private clients with atomic record creation, request retry protection, and pending invitation recovery when email delivery fails.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [59c8921]
|
|
22
|
+
- Updated dependencies [59c8921]
|
|
23
|
+
- Updated dependencies [59c8921]
|
|
24
|
+
- Updated dependencies [59c8921]
|
|
25
|
+
- Updated dependencies [59c8921]
|
|
26
|
+
- Updated dependencies [59c8921]
|
|
27
|
+
- Updated dependencies [59c8921]
|
|
28
|
+
- Updated dependencies [59c8921]
|
|
29
|
+
- Updated dependencies [59c8921]
|
|
30
|
+
- @nuxt-customer-portal/core@0.4.0
|
|
31
|
+
- @nuxt-customer-portal/clients@0.4.0
|
|
32
|
+
- @nuxt-customer-portal/organizations@0.4.0
|
|
33
|
+
- @nuxt-customer-portal/ui@0.4.0
|
|
34
|
+
|
|
35
|
+
## 0.3.3
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- @nuxt-customer-portal/clients@0.3.3
|
|
40
|
+
- @nuxt-customer-portal/core@0.3.3
|
|
41
|
+
- @nuxt-customer-portal/organizations@0.3.3
|
|
42
|
+
- @nuxt-customer-portal/ui@0.3.3
|
|
43
|
+
|
|
3
44
|
## 0.3.2
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
|
@@ -52,7 +52,12 @@ const schema = computed(() =>
|
|
|
52
52
|
<UButton v-if="showCancel" type="button" color="neutral" variant="outline" @click="emit('cancel')">
|
|
53
53
|
{{ t('features.timesheets.cancel') }}
|
|
54
54
|
</UButton>
|
|
55
|
-
<UButton
|
|
55
|
+
<UButton
|
|
56
|
+
type="submit"
|
|
57
|
+
:icon="editing ? 'i-lucide-save' : 'i-lucide-plus'"
|
|
58
|
+
class="ml-auto flex min-w-28 justify-center"
|
|
59
|
+
:loading="busy"
|
|
60
|
+
>
|
|
56
61
|
{{ editing ? t('features.timesheets.save') : t('features.timesheets.admin.createActivity') }}
|
|
57
62
|
</UButton>
|
|
58
63
|
</div>
|
|
@@ -372,7 +372,7 @@ const reject = () =>
|
|
|
372
372
|
</div>
|
|
373
373
|
</details>
|
|
374
374
|
|
|
375
|
-
<UModal v-model:open="requestOpen" :title="t('features.timesheets.submissions.title')">
|
|
375
|
+
<UModal v-if="requestOpen" v-model:open="requestOpen" :title="t('features.timesheets.submissions.title')">
|
|
376
376
|
<template #body>
|
|
377
377
|
<UForm
|
|
378
378
|
:schema="requestSchema"
|
|
@@ -395,7 +395,7 @@ const reject = () =>
|
|
|
395
395
|
</UForm>
|
|
396
396
|
</template>
|
|
397
397
|
</UModal>
|
|
398
|
-
<UModal v-model:open="clientReplyOpen" :title="t('features.timesheets.submissions.reply')">
|
|
398
|
+
<UModal v-if="clientReplyOpen" v-model:open="clientReplyOpen" :title="t('features.timesheets.submissions.reply')">
|
|
399
399
|
<template #body>
|
|
400
400
|
<UForm
|
|
401
401
|
:schema="clientReplySchema"
|
|
@@ -411,7 +411,7 @@ const reject = () =>
|
|
|
411
411
|
</UForm>
|
|
412
412
|
</template>
|
|
413
413
|
</UModal>
|
|
414
|
-
<UModal v-model:open="approvalOpen" :title="t('features.timesheets.admin.approve')">
|
|
414
|
+
<UModal v-if="approvalOpen" v-model:open="approvalOpen" :title="t('features.timesheets.admin.approve')">
|
|
415
415
|
<template #body>
|
|
416
416
|
<UForm :state="approvalState" :schema="approvalSchema" novalidate class="space-y-4" @submit="approve">
|
|
417
417
|
<UFormField name="comment" :label="t('features.timesheets.submissions.remark')">
|
|
@@ -428,7 +428,7 @@ const reject = () =>
|
|
|
428
428
|
</UForm>
|
|
429
429
|
</template>
|
|
430
430
|
</UModal>
|
|
431
|
-
<UModal v-model:open="rejectionOpen" :title="t('features.timesheets.admin.rejectTimesheet')">
|
|
431
|
+
<UModal v-if="rejectionOpen" v-model:open="rejectionOpen" :title="t('features.timesheets.admin.rejectTimesheet')">
|
|
432
432
|
<template #body>
|
|
433
433
|
<UForm novalidate :state="rejectionState" :schema="rejectionSchema" class="space-y-4" @submit="reject">
|
|
434
434
|
<UFormField name="comment" :label="t('features.timesheets.admin.rejectionReason')" required>
|
|
@@ -72,10 +72,12 @@ watch(
|
|
|
72
72
|
</UFormField>
|
|
73
73
|
<UButton
|
|
74
74
|
icon="i-lucide-save"
|
|
75
|
+
class="ml-auto flex min-w-28 justify-center"
|
|
75
76
|
:aria-label="t('features.timesheets.save')"
|
|
76
77
|
:loading="busy"
|
|
77
78
|
@click="saveMember(member.id)"
|
|
78
|
-
|
|
79
|
+
>{{ t('features.timesheets.save') }}</UButton
|
|
80
|
+
>
|
|
79
81
|
</div>
|
|
80
82
|
</div>
|
|
81
83
|
</UCard>
|
|
@@ -45,7 +45,9 @@ const save = async () => {
|
|
|
45
45
|
/>
|
|
46
46
|
</UFormField>
|
|
47
47
|
<div class="flex justify-end">
|
|
48
|
-
<UButton type="submit" icon="i-lucide-save" :loading="busy">{{
|
|
48
|
+
<UButton type="submit" icon="i-lucide-save" class="ml-auto flex min-w-28 justify-center" :loading="busy">{{
|
|
49
|
+
t('features.timesheets.save')
|
|
50
|
+
}}</UButton>
|
|
49
51
|
</div>
|
|
50
52
|
</UForm>
|
|
51
53
|
</UCard>
|
|
@@ -292,7 +292,7 @@ await listing.load()
|
|
|
292
292
|
</footer>
|
|
293
293
|
</article>
|
|
294
294
|
<UModal
|
|
295
|
-
v-if="isReview && selected && isApprovalItem(selected)"
|
|
295
|
+
v-if="disputeOpen && isReview && selected && isApprovalItem(selected)"
|
|
296
296
|
v-model:open="disputeOpen"
|
|
297
297
|
:title="t(`features.timesheets.clientPortal.${reviewAction === 'APPROVE' ? 'approve' : 'dispute'}`)"
|
|
298
298
|
>
|
|
@@ -251,7 +251,14 @@ onKeyStroke('Escape', () => {
|
|
|
251
251
|
/>
|
|
252
252
|
</div>
|
|
253
253
|
<div class="flex justify-end">
|
|
254
|
-
<UButton
|
|
254
|
+
<UButton
|
|
255
|
+
size="sm"
|
|
256
|
+
variant="outline"
|
|
257
|
+
icon="i-lucide-save"
|
|
258
|
+
class="ml-auto flex min-w-28 justify-center"
|
|
259
|
+
:loading="busy"
|
|
260
|
+
@click="saveRates"
|
|
261
|
+
>
|
|
255
262
|
{{ t('features.timesheets.admin.saveOverrides') }}
|
|
256
263
|
</UButton>
|
|
257
264
|
</div>
|
|
@@ -206,7 +206,12 @@ const createActivity = async () => {
|
|
|
206
206
|
<UButton v-if="showCancel" type="button" color="neutral" variant="outline" @click="emit('cancel')">
|
|
207
207
|
{{ t('features.timesheets.cancel') }}
|
|
208
208
|
</UButton>
|
|
209
|
-
<UButton
|
|
209
|
+
<UButton
|
|
210
|
+
type="submit"
|
|
211
|
+
:icon="editing ? 'i-lucide-save' : 'i-lucide-plus'"
|
|
212
|
+
class="ml-auto flex min-w-28 justify-center"
|
|
213
|
+
:loading="busy"
|
|
214
|
+
>
|
|
210
215
|
{{ editing ? t('features.timesheets.save') : t('features.timesheets.admin.createProject') }}
|
|
211
216
|
</UButton>
|
|
212
217
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
+
import { isValidTimezone } from '@nuxt-customer-portal/core/shared/timezone'
|
|
3
4
|
import type { TimesheetsAdminBootstrap } from '@nuxt-customer-portal/timesheets/app/composables/useTimesheets'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
@@ -23,7 +24,7 @@ const schema = computed(() =>
|
|
|
23
24
|
z.object({
|
|
24
25
|
timerRoundingMinutes: z.number().int().min(1).max(60),
|
|
25
26
|
currency: z.string().trim().length(3, t('features.timesheets.validation.currencyLength')),
|
|
26
|
-
timezone: z.string().trim().
|
|
27
|
+
timezone: z.string().trim().max(100).refine(isValidTimezone, t('timezones.invalid'))
|
|
27
28
|
})
|
|
28
29
|
)
|
|
29
30
|
|
|
@@ -72,7 +73,7 @@ const save = async () => {
|
|
|
72
73
|
>
|
|
73
74
|
<UInput v-model.number="draft.timerRoundingMinutes" type="number" :min="1" :max="60" class="w-full sm:w-28" />
|
|
74
75
|
</UFormField>
|
|
75
|
-
<UButton type="submit" class="w-
|
|
76
|
+
<UButton type="submit" class="ml-auto flex min-w-28 justify-center" icon="i-lucide-save" :loading="busy">
|
|
76
77
|
{{ t('features.timesheets.save') }}
|
|
77
78
|
</UButton>
|
|
78
79
|
</UForm>
|
|
@@ -1194,7 +1194,7 @@ const runningDuration = computed(() => {
|
|
|
1194
1194
|
</div>
|
|
1195
1195
|
</details>
|
|
1196
1196
|
|
|
1197
|
-
<UModal v-model:open="replyOpen" :title="t('features.timesheets.submissions.resubmit')">
|
|
1197
|
+
<UModal v-if="replyOpen" v-model:open="replyOpen" :title="t('features.timesheets.submissions.resubmit')">
|
|
1198
1198
|
<template #body>
|
|
1199
1199
|
<UForm :schema="submissionReplySchema" :state="replyState" novalidate class="space-y-4" @submit="resubmit">
|
|
1200
1200
|
<UFormField name="reply" :label="t('features.timesheets.submissions.reply')">
|
|
@@ -1208,6 +1208,7 @@ const runningDuration = computed(() => {
|
|
|
1208
1208
|
</UModal>
|
|
1209
1209
|
|
|
1210
1210
|
<UModal
|
|
1211
|
+
v-if="modalOpen"
|
|
1211
1212
|
v-model:open="modalOpen"
|
|
1212
1213
|
:title="form.id ? t('features.timesheets.editEntry') : t('features.timesheets.addEntry')"
|
|
1213
1214
|
@after:enter="focusPrefilledHours"
|
|
@@ -1283,7 +1284,13 @@ const runningDuration = computed(() => {
|
|
|
1283
1284
|
<UButton type="button" color="neutral" variant="outline" @click="modalOpen = false">
|
|
1284
1285
|
{{ t('features.timesheets.cancel') }}
|
|
1285
1286
|
</UButton>
|
|
1286
|
-
<UButton
|
|
1287
|
+
<UButton
|
|
1288
|
+
type="submit"
|
|
1289
|
+
icon="i-lucide-save"
|
|
1290
|
+
class="ml-auto flex min-w-28 justify-center"
|
|
1291
|
+
:loading="saving"
|
|
1292
|
+
:disabled="tariffMissing"
|
|
1293
|
+
>
|
|
1287
1294
|
{{ t('features.timesheets.save') }}
|
|
1288
1295
|
</UButton>
|
|
1289
1296
|
</div>
|
|
@@ -1292,7 +1299,7 @@ const runningDuration = computed(() => {
|
|
|
1292
1299
|
</template>
|
|
1293
1300
|
</UModal>
|
|
1294
1301
|
|
|
1295
|
-
<UModal v-model:open="cellEntriesOpen" :title="t('features.timesheets.cellEntries.title')">
|
|
1302
|
+
<UModal v-if="cellEntriesOpen" v-model:open="cellEntriesOpen" :title="t('features.timesheets.cellEntries.title')">
|
|
1296
1303
|
<template #body>
|
|
1297
1304
|
<div v-if="selectedCell" class="space-y-4">
|
|
1298
1305
|
<div>
|
|
@@ -1341,7 +1348,11 @@ const runningDuration = computed(() => {
|
|
|
1341
1348
|
</template>
|
|
1342
1349
|
</UModal>
|
|
1343
1350
|
|
|
1344
|
-
<UModal
|
|
1351
|
+
<UModal
|
|
1352
|
+
v-if="submissionModalOpen"
|
|
1353
|
+
v-model:open="submissionModalOpen"
|
|
1354
|
+
:title="t('features.timesheets.submissions.title')"
|
|
1355
|
+
>
|
|
1345
1356
|
<template #body>
|
|
1346
1357
|
<UForm :state="submissionState" :schema="submissionSchema" novalidate class="space-y-4" @submit="submit">
|
|
1347
1358
|
<p class="text-sm text-muted">{{ t('features.timesheets.submissions.description') }}</p>
|
|
@@ -1397,7 +1408,7 @@ const runningDuration = computed(() => {
|
|
|
1397
1408
|
</template>
|
|
1398
1409
|
</UModal>
|
|
1399
1410
|
|
|
1400
|
-
<UModal v-model:open="timerModalOpen" :title="t('features.timesheets.timer.start')">
|
|
1411
|
+
<UModal v-if="timerModalOpen" v-model:open="timerModalOpen" :title="t('features.timesheets.timer.start')">
|
|
1401
1412
|
<template #body>
|
|
1402
1413
|
<UAlert
|
|
1403
1414
|
v-if="!isOrganizationAdmin && workspaceStructureIncomplete"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxt-customer-portal/timesheets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://nuxt-customer-portal.com",
|
|
6
6
|
"repository": {
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"pg": "^8.22.0",
|
|
46
46
|
"resend": "^6.18.1",
|
|
47
47
|
"zod": "^4.4.3",
|
|
48
|
-
"@nuxt-customer-portal/core": "^0.
|
|
49
|
-
"@nuxt-customer-portal/
|
|
50
|
-
"@nuxt-customer-portal/
|
|
51
|
-
"@nuxt-customer-portal/ui": "^0.
|
|
48
|
+
"@nuxt-customer-portal/core": "^0.4.0",
|
|
49
|
+
"@nuxt-customer-portal/organizations": "^0.4.0",
|
|
50
|
+
"@nuxt-customer-portal/clients": "^0.4.0",
|
|
51
|
+
"@nuxt-customer-portal/ui": "^0.4.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"nuxt": "^4.5.1",
|
package/portal.manifest.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { and, eq, isNotNull } from 'drizzle-orm'
|
|
|
4
4
|
import { db } from '@nuxt-customer-portal/core/server/portal'
|
|
5
5
|
import { member, organization, user } from '@nuxt-customer-portal/core/schema'
|
|
6
6
|
import { getPortalEmailSettings, sendPortalEmail } from '@nuxt-customer-portal/core/server/utils/portal-email'
|
|
7
|
+
import { emailRecipientName } from '@nuxt-customer-portal/core/shared/email-recipient'
|
|
7
8
|
import { timesheetEmails } from '../../shared/emails'
|
|
8
9
|
import {
|
|
9
10
|
internalApproverAssignment,
|
|
@@ -48,7 +49,7 @@ export const notifyTimesheetEvent = async (
|
|
|
48
49
|
const baseUrl = process.env.BETTER_AUTH_URL || process.env.PUBLIC_URL || 'http://localhost:3051'
|
|
49
50
|
const send = async (
|
|
50
51
|
id: string,
|
|
51
|
-
recipient: { id: string; email: string; name: string },
|
|
52
|
+
recipient: { id: string; email: string; name: string; firstName?: string | null },
|
|
52
53
|
path: string,
|
|
53
54
|
clientName = '',
|
|
54
55
|
comment = '',
|
|
@@ -63,7 +64,13 @@ export const notifyTimesheetEvent = async (
|
|
|
63
64
|
to: recipient.email,
|
|
64
65
|
idempotencyKey: `timesheet/${submission.id}/${submission.version}/${id}/${scope}/${recipient.id}`,
|
|
65
66
|
values: {
|
|
66
|
-
recipient_name: escapeHtml(
|
|
67
|
+
recipient_name: escapeHtml(
|
|
68
|
+
emailRecipientName({
|
|
69
|
+
firstName: recipient.firstName,
|
|
70
|
+
displayName: recipient.name,
|
|
71
|
+
email: recipient.email
|
|
72
|
+
})
|
|
73
|
+
),
|
|
67
74
|
person_name: escapeHtml(person.name),
|
|
68
75
|
organization_name: escapeHtml(workspace.name),
|
|
69
76
|
period: escapeHtml(formatTimesheetPeriod(submission.periodStartsOn, submission.periodEndsOn, emailLocale)),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { timezoneSchema } from '@nuxt-customer-portal/core/server/utils/timezone-validation'
|
|
2
3
|
|
|
3
4
|
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
4
5
|
const id = z.string().min(1).max(128)
|
|
@@ -195,7 +196,7 @@ export const settingsUpdateSchema = z.object({
|
|
|
195
196
|
.length(3)
|
|
196
197
|
.transform((v) => v.toUpperCase())
|
|
197
198
|
.optional(),
|
|
198
|
-
timezone:
|
|
199
|
+
timezone: timezoneSchema.optional()
|
|
199
200
|
})
|
|
200
201
|
|
|
201
202
|
export const reviewSchema = z
|
package/shared/emails.ts
CHANGED
|
@@ -7,11 +7,15 @@ export const timesheetEmails: PortalEmailDefinition[] = [
|
|
|
7
7
|
defaults: {
|
|
8
8
|
en: {
|
|
9
9
|
subject: 'Internal approval requested — {{person_name}} ({{period}})',
|
|
10
|
-
body: 'Dear {{recipient_name}}
|
|
10
|
+
body: 'Dear {{recipient_name}},\n\nA timesheet is ready for your internal review. Please check the submitted hours and details before deciding whether it can move to the next step.\n\n- **Organization:** {{organization_name}}\n- **Team member:** {{person_name}}\n- **Period:** {{period}}\n- **Client:** {{client_name}}\n- **Comment:** {{comment}}\n\n[Review the timesheet]({{action_url}})',
|
|
11
|
+
footer:
|
|
12
|
+
'Thank you for taking the time to review this submission. Your prompt response helps keep the approval process moving smoothly.'
|
|
11
13
|
},
|
|
12
14
|
nl: {
|
|
13
15
|
subject: 'Interne goedkeuring gevraagd — {{person_name}} ({{period}})',
|
|
14
|
-
body: 'Beste {{recipient_name}}
|
|
16
|
+
body: 'Beste {{recipient_name}},\n\nEr staat een urenstaat klaar voor uw interne beoordeling. Controleer de ingediende uren en gegevens voordat u beslist of deze door kan naar de volgende stap.\n\n- **Organisatie:** {{organization_name}}\n- **Medewerker:** {{person_name}}\n- **Periode:** {{period}}\n- **Klant:** {{client_name}}\n- **Opmerking:** {{comment}}\n\n[Urenstaat beoordelen]({{action_url}})',
|
|
17
|
+
footer:
|
|
18
|
+
'Bedankt dat u de tijd neemt om deze urenstaat te beoordelen. Met een snelle reactie blijft het goedkeuringsproces soepel verlopen.'
|
|
15
19
|
}
|
|
16
20
|
},
|
|
17
21
|
placeholders: [
|
|
@@ -64,11 +68,15 @@ export const timesheetEmails: PortalEmailDefinition[] = [
|
|
|
64
68
|
defaults: {
|
|
65
69
|
en: {
|
|
66
70
|
subject: 'Timesheet approved internally — {{person_name}} ({{period}})',
|
|
67
|
-
body: 'Dear {{recipient_name}}
|
|
71
|
+
body: 'Dear {{recipient_name}},\n\nGood news: your timesheet has been approved internally and can now continue through the approval process.\n\n- **Organization:** {{organization_name}}\n- **Team member:** {{person_name}}\n- **Period:** {{period}}\n- **Client:** {{client_name}}\n- **Comment:** {{comment}}\n\n[View the approved timesheet]({{action_url}})',
|
|
72
|
+
footer:
|
|
73
|
+
'No further action is needed from you right now. You can use the link above whenever you want to review the latest status.'
|
|
68
74
|
},
|
|
69
75
|
nl: {
|
|
70
76
|
subject: 'Urenstaat intern goedgekeurd — {{person_name}} ({{period}})',
|
|
71
|
-
body: 'Beste {{recipient_name}}
|
|
77
|
+
body: 'Beste {{recipient_name}},\n\nGoed nieuws: uw urenstaat is intern goedgekeurd en kan nu verder in het goedkeuringsproces.\n\n- **Organisatie:** {{organization_name}}\n- **Medewerker:** {{person_name}}\n- **Periode:** {{period}}\n- **Klant:** {{client_name}}\n- **Opmerking:** {{comment}}\n\n[Goedgekeurde urenstaat bekijken]({{action_url}})',
|
|
78
|
+
footer:
|
|
79
|
+
'U hoeft op dit moment niets te doen. Via de bovenstaande link kunt u altijd de meest recente status bekijken.'
|
|
72
80
|
}
|
|
73
81
|
},
|
|
74
82
|
placeholders: [
|
|
@@ -121,11 +129,15 @@ export const timesheetEmails: PortalEmailDefinition[] = [
|
|
|
121
129
|
defaults: {
|
|
122
130
|
en: {
|
|
123
131
|
subject: 'Timesheet rejected internally — {{person_name}} ({{period}})',
|
|
124
|
-
body: 'Dear {{recipient_name}}
|
|
132
|
+
body: 'Dear {{recipient_name}},\n\nYour timesheet could not be approved internally yet. Please review the feedback below, update the submission where needed, and submit it again when it is ready.\n\n- **Organization:** {{organization_name}}\n- **Team member:** {{person_name}}\n- **Period:** {{period}}\n- **Client:** {{client_name}}\n- **Feedback:** {{comment}}\n\n[Review and update the timesheet]({{action_url}})',
|
|
133
|
+
footer:
|
|
134
|
+
'If the feedback is unclear, please contact the person responsible for the internal review before resubmitting.'
|
|
125
135
|
},
|
|
126
136
|
nl: {
|
|
127
137
|
subject: 'Urenstaat intern afgewezen — {{person_name}} ({{period}})',
|
|
128
|
-
body: 'Beste {{recipient_name}}
|
|
138
|
+
body: 'Beste {{recipient_name}},\n\nUw urenstaat kon intern nog niet worden goedgekeurd. Bekijk de feedback hieronder, pas de urenstaat waar nodig aan en dien deze opnieuw in zodra alles klopt.\n\n- **Organisatie:** {{organization_name}}\n- **Medewerker:** {{person_name}}\n- **Periode:** {{period}}\n- **Klant:** {{client_name}}\n- **Feedback:** {{comment}}\n\n[Urenstaat bekijken en aanpassen]({{action_url}})',
|
|
139
|
+
footer:
|
|
140
|
+
'Is de feedback niet duidelijk? Neem dan contact op met de interne beoordelaar voordat u de urenstaat opnieuw indient.'
|
|
129
141
|
}
|
|
130
142
|
},
|
|
131
143
|
placeholders: [
|
|
@@ -178,11 +190,14 @@ export const timesheetEmails: PortalEmailDefinition[] = [
|
|
|
178
190
|
defaults: {
|
|
179
191
|
en: {
|
|
180
192
|
subject: 'Timesheet reopened — {{person_name}} ({{period}})',
|
|
181
|
-
body: 'Dear {{recipient_name}}
|
|
193
|
+
body: 'Dear {{recipient_name}},\n\nYour timesheet has been reopened, so you can make changes again. Please review the reason below and update the relevant entries before resubmitting.\n\n- **Organization:** {{organization_name}}\n- **Team member:** {{person_name}}\n- **Period:** {{period}}\n- **Client:** {{client_name}}\n- **Comment:** {{comment}}\n\n[Open and edit the timesheet]({{action_url}})',
|
|
194
|
+
footer: 'When your changes are complete, submit the timesheet again so the approval process can continue.'
|
|
182
195
|
},
|
|
183
196
|
nl: {
|
|
184
197
|
subject: 'Urenstaat heropend — {{person_name}} ({{period}})',
|
|
185
|
-
body: 'Beste {{recipient_name}}
|
|
198
|
+
body: 'Beste {{recipient_name}},\n\nUw urenstaat is heropend, zodat u opnieuw wijzigingen kunt aanbrengen. Bekijk de reden hieronder en pas de betreffende registraties aan voordat u de urenstaat opnieuw indient.\n\n- **Organisatie:** {{organization_name}}\n- **Medewerker:** {{person_name}}\n- **Periode:** {{period}}\n- **Klant:** {{client_name}}\n- **Opmerking:** {{comment}}\n\n[Urenstaat openen en bewerken]({{action_url}})',
|
|
199
|
+
footer:
|
|
200
|
+
'Dien de urenstaat opnieuw in zodra uw wijzigingen klaar zijn, zodat het goedkeuringsproces verder kan gaan.'
|
|
186
201
|
}
|
|
187
202
|
},
|
|
188
203
|
placeholders: [
|
|
@@ -235,11 +250,15 @@ export const timesheetEmails: PortalEmailDefinition[] = [
|
|
|
235
250
|
defaults: {
|
|
236
251
|
en: {
|
|
237
252
|
subject: 'Client approval requested — {{person_name}} ({{period}})',
|
|
238
|
-
body: 'Dear {{recipient_name}}
|
|
253
|
+
body: 'Dear {{recipient_name}},\n\nA timesheet has completed its internal review and is now waiting for your organization’s approval. Please check the hours and supporting details below.\n\n- **Organization:** {{organization_name}}\n- **Team member:** {{person_name}}\n- **Period:** {{period}}\n- **Client:** {{client_name}}\n- **Comment:** {{comment}}\n\n[Review the timesheet]({{action_url}})',
|
|
254
|
+
footer:
|
|
255
|
+
'You can approve the timesheet or raise a concern from the review page. Thank you for helping us complete the process.'
|
|
239
256
|
},
|
|
240
257
|
nl: {
|
|
241
258
|
subject: 'Klantgoedkeuring gevraagd — {{person_name}} ({{period}})',
|
|
242
|
-
body: 'Beste {{recipient_name}}
|
|
259
|
+
body: 'Beste {{recipient_name}},\n\nEen urenstaat heeft de interne beoordeling doorlopen en wacht nu op goedkeuring door uw organisatie. Controleer de uren en aanvullende gegevens hieronder.\n\n- **Organisatie:** {{organization_name}}\n- **Medewerker:** {{person_name}}\n- **Periode:** {{period}}\n- **Klant:** {{client_name}}\n- **Opmerking:** {{comment}}\n\n[Urenstaat beoordelen]({{action_url}})',
|
|
260
|
+
footer:
|
|
261
|
+
'Op de beoordelingspagina kunt u de urenstaat goedkeuren of een bezwaar aangeven. Bedankt dat u helpt het proces af te ronden.'
|
|
243
262
|
}
|
|
244
263
|
},
|
|
245
264
|
placeholders: [
|
|
@@ -292,11 +311,15 @@ export const timesheetEmails: PortalEmailDefinition[] = [
|
|
|
292
311
|
defaults: {
|
|
293
312
|
en: {
|
|
294
313
|
subject: 'Timesheet approved by client — {{person_name}} ({{period}})',
|
|
295
|
-
body: 'Dear {{recipient_name}}
|
|
314
|
+
body: 'Dear {{recipient_name}},\n\nGood news: the client has reviewed and approved the timesheet below. The approval process for this submission is now complete.\n\n- **Organization:** {{organization_name}}\n- **Team member:** {{person_name}}\n- **Period:** {{period}}\n- **Client:** {{client_name}}\n- **Comment:** {{comment}}\n\n[View the approved timesheet]({{action_url}})',
|
|
315
|
+
footer:
|
|
316
|
+
'Thank you for keeping the timesheet accurate and complete. No further action is required for this submission.'
|
|
296
317
|
},
|
|
297
318
|
nl: {
|
|
298
319
|
subject: 'Urenstaat goedgekeurd door klant — {{person_name}} ({{period}})',
|
|
299
|
-
body: 'Beste {{recipient_name}}
|
|
320
|
+
body: 'Beste {{recipient_name}},\n\nGoed nieuws: de klant heeft de onderstaande urenstaat beoordeeld en goedgekeurd. Het goedkeuringsproces voor deze inzending is daarmee afgerond.\n\n- **Organisatie:** {{organization_name}}\n- **Medewerker:** {{person_name}}\n- **Periode:** {{period}}\n- **Klant:** {{client_name}}\n- **Opmerking:** {{comment}}\n\n[Goedgekeurde urenstaat bekijken]({{action_url}})',
|
|
321
|
+
footer:
|
|
322
|
+
'Bedankt voor het zorgvuldig en volledig bijhouden van de uren. Voor deze inzending hoeft u niets meer te doen.'
|
|
300
323
|
}
|
|
301
324
|
},
|
|
302
325
|
placeholders: [
|
|
@@ -349,11 +372,15 @@ export const timesheetEmails: PortalEmailDefinition[] = [
|
|
|
349
372
|
defaults: {
|
|
350
373
|
en: {
|
|
351
374
|
subject: 'Timesheet disputed by client — {{person_name}} ({{period}})',
|
|
352
|
-
body: 'Dear {{recipient_name}}
|
|
375
|
+
body: 'Dear {{recipient_name}},\n\nThe client has raised a concern about the timesheet below. Please review their feedback and coordinate any necessary corrections before the timesheet is submitted again.\n\n- **Organization:** {{organization_name}}\n- **Team member:** {{person_name}}\n- **Period:** {{period}}\n- **Client:** {{client_name}}\n- **Client feedback:** {{comment}}\n\n[Review the disputed timesheet]({{action_url}})',
|
|
376
|
+
footer:
|
|
377
|
+
'Please follow up with the client if clarification is needed. Resolving the concern promptly will help keep the approval process on track.'
|
|
353
378
|
},
|
|
354
379
|
nl: {
|
|
355
380
|
subject: 'Urenstaat betwist door klant — {{person_name}} ({{period}})',
|
|
356
|
-
body: 'Beste {{recipient_name}}
|
|
381
|
+
body: 'Beste {{recipient_name}},\n\nDe klant heeft een bezwaar aangegeven bij de onderstaande urenstaat. Bekijk de feedback en stem eventuele correcties af voordat de urenstaat opnieuw wordt ingediend.\n\n- **Organisatie:** {{organization_name}}\n- **Medewerker:** {{person_name}}\n- **Periode:** {{period}}\n- **Klant:** {{client_name}}\n- **Feedback van de klant:** {{comment}}\n\n[Betwiste urenstaat bekijken]({{action_url}})',
|
|
382
|
+
footer:
|
|
383
|
+
'Neem contact op met de klant als nadere uitleg nodig is. Door het bezwaar snel op te lossen, blijft het goedkeuringsproces op schema.'
|
|
357
384
|
}
|
|
358
385
|
},
|
|
359
386
|
placeholders: [
|