@nuxt-customer-portal/timesheets 0.3.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 +16 -0
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/app/assets/timesheets.css +368 -0
- package/app/components/TimesheetsActivityForm.vue +61 -0
- package/app/components/TimesheetsAdminActivities.vue +323 -0
- package/app/components/TimesheetsAdminApprovals.vue +449 -0
- package/app/components/TimesheetsAdminEmptyState.vue +28 -0
- package/app/components/TimesheetsAdminListToolbar.vue +19 -0
- package/app/components/TimesheetsAdminPaginatedList.vue +95 -0
- package/app/components/TimesheetsAdminProjects.vue +234 -0
- package/app/components/TimesheetsAdminRates.vue +83 -0
- package/app/components/TimesheetsAdminReports.vue +123 -0
- package/app/components/TimesheetsAdminSectionPage.vue +135 -0
- package/app/components/TimesheetsAdminSettings.vue +15 -0
- package/app/components/TimesheetsAdministrationOrganizationPanel.vue +26 -0
- package/app/components/TimesheetsClientSettingsPanel.vue +52 -0
- package/app/components/TimesheetsClientTimesheetBrowser.vue +439 -0
- package/app/components/TimesheetsDashboardCard.vue +260 -0
- package/app/components/TimesheetsDashboardClientApprovals.vue +1 -0
- package/app/components/TimesheetsDashboardInternalApprovals.vue +1 -0
- package/app/components/TimesheetsDashboardMyTimesheets.vue +1 -0
- package/app/components/TimesheetsDashboardMyWeek.vue +1 -0
- package/app/components/TimesheetsDashboardSupplierTimesheets.vue +47 -0
- package/app/components/TimesheetsOrganizationFeaturesCard.vue +66 -0
- package/app/components/TimesheetsPageShell.vue +48 -0
- package/app/components/TimesheetsProjectDetail.vue +338 -0
- package/app/components/TimesheetsProjectForm.vue +215 -0
- package/app/components/TimesheetsSetupChecklist.vue +48 -0
- package/app/components/TimesheetsSubmissionMessages.vue +30 -0
- package/app/components/TimesheetsSubmissionTimeline.vue +89 -0
- package/app/components/TimesheetsWorkspaceSettingsForm.vue +80 -0
- package/app/composables/useTimesheetMenu.ts +69 -0
- package/app/composables/useTimesheetMutationError.ts +35 -0
- package/app/composables/useTimesheets.ts +248 -0
- package/app/composables/useTimesheetsAdminList.ts +165 -0
- package/app/composables/useTimesheetsDashboard.ts +8 -0
- package/app/pages/admin/timesheets/activities.vue +3 -0
- package/app/pages/admin/timesheets/approvals.vue +3 -0
- package/app/pages/admin/timesheets/index.vue +7 -0
- package/app/pages/admin/timesheets/internal-approvals.vue +131 -0
- package/app/pages/admin/timesheets/projects/[id].vue +57 -0
- package/app/pages/admin/timesheets/projects.vue +8 -0
- package/app/pages/admin/timesheets/rates.vue +3 -0
- package/app/pages/admin/timesheets/reports.vue +3 -0
- package/app/pages/admin/timesheets/settings.vue +3 -0
- package/app/pages/timesheets/approvals/reviewers.vue +96 -0
- package/app/pages/timesheets/approvals.vue +11 -0
- package/app/pages/timesheets/client-approvals.vue +108 -0
- package/app/pages/timesheets/client.vue +17 -0
- package/app/pages/timesheets/index.vue +1458 -0
- package/app/pages/timesheets/internal-approvals.vue +100 -0
- package/app/pages/timesheets/suppliers.vue +23 -0
- package/app/plugins/timesheets-feature.ts +122 -0
- package/i18n/locales/en.json +455 -0
- package/i18n/locales/nl.json +455 -0
- package/migrations/0000_baseline.sql +409 -0
- package/migrations/0001_remove_unused_legacy_invoice_schema.sql +31 -0
- package/migrations/0002_submission_batches.sql +47 -0
- package/migrations/0003_sync_provider_clients.sql +26 -0
- package/migrations/0004_client_reply.sql +1 -0
- package/migrations/0005_timer_rounding.sql +1 -0
- package/migrations/meta/0000_snapshot.json +3178 -0
- package/migrations/meta/_journal.json +13 -0
- package/nuxt.config.ts +20 -0
- package/package.json +61 -0
- package/portal.manifest.mjs +10 -0
- package/server/api/timesheets/admin/activities/[id]/deletion.get.ts +18 -0
- package/server/api/timesheets/admin/activities/[id].delete.ts +20 -0
- package/server/api/timesheets/admin/activities/[id].patch.ts +19 -0
- package/server/api/timesheets/admin/activities/index.get.ts +19 -0
- package/server/api/timesheets/admin/activities/index.post.ts +19 -0
- package/server/api/timesheets/admin/approvals/[id].post.ts +19 -0
- package/server/api/timesheets/admin/bootstrap.get.ts +73 -0
- package/server/api/timesheets/admin/client-settings/[clientId]/index.get.ts +25 -0
- package/server/api/timesheets/admin/client-settings/[clientId]/index.put.ts +28 -0
- package/server/api/timesheets/admin/clients/[id]/deletion.get.ts +18 -0
- package/server/api/timesheets/admin/clients/[id].delete.ts +20 -0
- package/server/api/timesheets/admin/clients/[id].patch.ts +14 -0
- package/server/api/timesheets/admin/clients/index.get.ts +18 -0
- package/server/api/timesheets/admin/clients/index.post.ts +25 -0
- package/server/api/timesheets/admin/internal-approvals/[userId].put.ts +11 -0
- package/server/api/timesheets/admin/internal-approvals/index.get.ts +8 -0
- package/server/api/timesheets/admin/internal-approvals/index.patch.ts +11 -0
- package/server/api/timesheets/admin/organization-capabilities/[organizationId].get.ts +19 -0
- package/server/api/timesheets/admin/organization-capabilities/[organizationId].patch.ts +23 -0
- package/server/api/timesheets/admin/projects/[id]/deletion.get.ts +18 -0
- package/server/api/timesheets/admin/projects/[id].delete.ts +20 -0
- package/server/api/timesheets/admin/projects/[id].get.ts +22 -0
- package/server/api/timesheets/admin/projects/[id].patch.ts +19 -0
- package/server/api/timesheets/admin/projects/index.get.ts +18 -0
- package/server/api/timesheets/admin/projects/index.post.ts +19 -0
- package/server/api/timesheets/admin/report.get.ts +26 -0
- package/server/api/timesheets/admin/settings.patch.ts +19 -0
- package/server/api/timesheets/admin/tariffs/index.put.ts +20 -0
- package/server/api/timesheets/admin/team/[userId].put.ts +24 -0
- package/server/api/timesheets/bootstrap.get.ts +24 -0
- package/server/api/timesheets/capabilities.get.ts +58 -0
- package/server/api/timesheets/client/[workspaceClientId]/index.get.ts +12 -0
- package/server/api/timesheets/client/[workspaceClientId]/reviewers.get.ts +10 -0
- package/server/api/timesheets/client/[workspaceClientId]/reviewers.put.ts +18 -0
- package/server/api/timesheets/client/[workspaceClientId]/reviews/[submissionId].post.ts +18 -0
- package/server/api/timesheets/client/approval-members.get.ts +11 -0
- package/server/api/timesheets/client/approval-suppliers.get.ts +8 -0
- package/server/api/timesheets/client/approvals.get.ts +14 -0
- package/server/api/timesheets/client/reviewer-suppliers.get.ts +10 -0
- package/server/api/timesheets/client/supplier-options.get.ts +14 -0
- package/server/api/timesheets/client/supplier-timesheets.get.ts +17 -0
- package/server/api/timesheets/client/workspaces.get.ts +11 -0
- package/server/api/timesheets/client-approvals.get.ts +18 -0
- package/server/api/timesheets/dashboard.get.ts +191 -0
- package/server/api/timesheets/entries/[id].delete.ts +18 -0
- package/server/api/timesheets/entries/[id].patch.ts +23 -0
- package/server/api/timesheets/entries/index.post.ts +22 -0
- package/server/api/timesheets/internal-approvals/[id].post.ts +9 -0
- package/server/api/timesheets/internal-approvals/index.get.ts +37 -0
- package/server/api/timesheets/internal-approvals/list.get.ts +12 -0
- package/server/api/timesheets/setup-status.get.ts +21 -0
- package/server/api/timesheets/submissions/[id]/client-reply.post.ts +20 -0
- package/server/api/timesheets/submissions/[id]/request-client-approval.post.ts +21 -0
- package/server/api/timesheets/submissions/[id]/resubmit.post.ts +10 -0
- package/server/api/timesheets/timer/index.delete.ts +18 -0
- package/server/api/timesheets/timer/index.post.ts +23 -0
- package/server/api/timesheets/weeks/[id]/submissions.post.ts +19 -0
- package/server/db/schema/timesheets.ts +383 -0
- package/server/plugins/client-sync.ts +30 -0
- package/server/plugins/openapi-contracts.ts +57 -0
- package/server/utils/timesheet-admin-listing.ts +281 -0
- package/server/utils/timesheet-email.ts +221 -0
- package/server/utils/timesheet-repository.ts +3096 -0
- package/server/utils/timesheet-validation.ts +231 -0
- package/shared/client-approval-dashboard.ts +17 -0
- package/shared/emails.ts +403 -0
- package/shared/feature.ts +184 -0
- package/shared/period-lock.ts +8 -0
- package/shared/submission-reply.ts +3 -0
- package/shared/timer-rounding.ts +2 -0
- package/shared/timesheet-dates.ts +23 -0
- package/shared/timesheet-errors.ts +13 -0
- package/shared/types/timesheet.ts +424 -0
- package/tokens.css +25 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
4
|
+
const id = z.string().min(1).max(128)
|
|
5
|
+
const moneyMinor = z.number().int().min(0).max(2_000_000_000)
|
|
6
|
+
const listBase = {
|
|
7
|
+
search: z.string().trim().max(200).optional(),
|
|
8
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
9
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
10
|
+
sortDir: z.enum(['asc', 'desc']).default('asc')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const projectListQuerySchema = z.object({
|
|
14
|
+
...listBase,
|
|
15
|
+
clientOrganizationId: id.optional(),
|
|
16
|
+
status: z.enum(['ALL', 'ACTIVE', 'ARCHIVED']).optional(),
|
|
17
|
+
sortBy: z.enum(['name', 'clientName', 'startsOn']).default('name')
|
|
18
|
+
})
|
|
19
|
+
export const clientListQuerySchema = z.object({
|
|
20
|
+
...listBase,
|
|
21
|
+
configured: z.enum(['configured', 'incomplete']).optional(),
|
|
22
|
+
sortBy: z.enum(['name']).default('name')
|
|
23
|
+
})
|
|
24
|
+
export const activityListQuerySchema = z.object({
|
|
25
|
+
...listBase,
|
|
26
|
+
active: z.enum(['true', 'false']).optional(),
|
|
27
|
+
billable: z.enum(['true', 'false']).optional(),
|
|
28
|
+
sortBy: z.enum(['name', 'active', 'billable']).default('name')
|
|
29
|
+
})
|
|
30
|
+
export const clientApprovalListQuerySchema = z.object({
|
|
31
|
+
...listBase,
|
|
32
|
+
userId: id.optional(),
|
|
33
|
+
workspaceClientId: id.optional(),
|
|
34
|
+
status: z.enum(['PENDING', 'APPROVED', 'DISPUTED']).optional(),
|
|
35
|
+
sortBy: z.enum(['weekStartsOn', 'supplierName', 'person', 'status', 'totalMinutes']).default('weekStartsOn'),
|
|
36
|
+
sortDir: z.enum(['asc', 'desc']).default('desc')
|
|
37
|
+
})
|
|
38
|
+
export const internalApprovalListQuerySchema = z.object({
|
|
39
|
+
...listBase,
|
|
40
|
+
userId: id.optional(),
|
|
41
|
+
status: z.enum(['SUBMITTED', 'APPROVED', 'REJECTED']).optional(),
|
|
42
|
+
sortBy: z.enum(['weekStartsOn', 'userName', 'status', 'totalMinutes']).default('weekStartsOn'),
|
|
43
|
+
sortDir: z.enum(['asc', 'desc']).default('desc')
|
|
44
|
+
})
|
|
45
|
+
export type InternalApprovalListQuery = z.infer<typeof internalApprovalListQuerySchema>
|
|
46
|
+
export const clientSupplierTimesheetListQuerySchema = z.object({
|
|
47
|
+
...listBase,
|
|
48
|
+
workspaceClientId: id.optional(),
|
|
49
|
+
sortBy: z.enum(['weekStartsOn', 'supplierName', 'person', 'totalMinutes']).default('weekStartsOn'),
|
|
50
|
+
sortDir: z.enum(['asc', 'desc']).default('desc')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export type ProjectListQuery = z.infer<typeof projectListQuerySchema>
|
|
54
|
+
export type ClientListQuery = z.infer<typeof clientListQuerySchema>
|
|
55
|
+
export type ActivityListQuery = z.infer<typeof activityListQuerySchema>
|
|
56
|
+
export type ClientApprovalListQuery = z.infer<typeof clientApprovalListQuerySchema>
|
|
57
|
+
export type ClientSupplierTimesheetListQuery = z.infer<typeof clientSupplierTimesheetListQuerySchema>
|
|
58
|
+
|
|
59
|
+
export const weekQuerySchema = z.object({
|
|
60
|
+
week: isoDate.optional()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export const entryCreateSchema = z.object({
|
|
64
|
+
projectId: id,
|
|
65
|
+
activityTypeId: id,
|
|
66
|
+
entryDate: isoDate,
|
|
67
|
+
durationMinutes: z
|
|
68
|
+
.number()
|
|
69
|
+
.int()
|
|
70
|
+
.min(1)
|
|
71
|
+
.max(24 * 60),
|
|
72
|
+
note: z.string().trim().max(2000).nullable().optional()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const entryUpdateSchema = entryCreateSchema
|
|
76
|
+
.partial()
|
|
77
|
+
.refine((value) => Object.keys(value).length > 0, 'At least one field is required')
|
|
78
|
+
|
|
79
|
+
export const timerStartSchema = z.object({
|
|
80
|
+
projectId: id,
|
|
81
|
+
activityTypeId: id,
|
|
82
|
+
entryDate: isoDate,
|
|
83
|
+
note: z.string().trim().max(2000).nullable().optional()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
export const clientCreateSchema = z.discriminatedUnion('mode', [
|
|
87
|
+
z.object({ mode: z.literal('link'), organizationId: id }),
|
|
88
|
+
z.object({
|
|
89
|
+
mode: z.literal('create'),
|
|
90
|
+
name: z.string().trim().min(2).max(160),
|
|
91
|
+
slug: z
|
|
92
|
+
.string()
|
|
93
|
+
.trim()
|
|
94
|
+
.min(1)
|
|
95
|
+
.max(160)
|
|
96
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
|
97
|
+
})
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
export const clientDeleteSchema = z.object({ clientName: z.string().min(1).max(160) })
|
|
101
|
+
export const clientAccessUpdateSchema = z.object({ accessMode: z.enum(['DISABLED', 'VIEW', 'REVIEW']) })
|
|
102
|
+
export const organizationCapabilitiesUpdateSchema = z.object({
|
|
103
|
+
workspaceEnabled: z.boolean()
|
|
104
|
+
})
|
|
105
|
+
export const clientReviewerUpdateSchema = z.object({ userId: id, assigned: z.boolean() })
|
|
106
|
+
export const clientReviewSchema = z
|
|
107
|
+
.object({
|
|
108
|
+
action: z.enum(['APPROVE', 'DISPUTE']),
|
|
109
|
+
expectedVersion: z.number().int().min(1),
|
|
110
|
+
comment: z.string().trim().max(2000).nullable().optional()
|
|
111
|
+
})
|
|
112
|
+
.superRefine((value, context) => {
|
|
113
|
+
if (value.action === 'DISPUTE' && !value.comment) {
|
|
114
|
+
context.addIssue({ code: 'custom', path: ['comment'], message: 'A dispute comment is required' })
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export const activityCreateSchema = z.object({
|
|
119
|
+
name: z.string().trim().min(2).max(120),
|
|
120
|
+
billable: z.boolean().default(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export const activityUpdateSchema = activityCreateSchema.partial().extend({
|
|
124
|
+
active: z.boolean().optional()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
export const activityDeleteSchema = z.object({
|
|
128
|
+
activityName: z.string().min(1).max(120)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const projectSchema = z.object({
|
|
132
|
+
clientOrganizationId: id,
|
|
133
|
+
name: z.string().trim().min(2).max(160),
|
|
134
|
+
code: z.string().trim().max(40).nullable().optional(),
|
|
135
|
+
startsOn: isoDate.nullable().optional(),
|
|
136
|
+
endsOn: isoDate.nullable().optional(),
|
|
137
|
+
budgetMinutes: z.number().int().positive().nullable().optional(),
|
|
138
|
+
budgetMinor: moneyMinor.positive().nullable().optional(),
|
|
139
|
+
activityTypeIds: z.array(id).min(1)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
export const projectCreateSchema = projectSchema.refine((v) => !v.startsOn || !v.endsOn || v.endsOn >= v.startsOn, {
|
|
143
|
+
message: 'End date must not precede start date',
|
|
144
|
+
path: ['endsOn']
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
export const projectUpdateSchema = projectSchema
|
|
148
|
+
.partial()
|
|
149
|
+
.extend({
|
|
150
|
+
status: z.enum(['ACTIVE', 'ARCHIVED']).optional(),
|
|
151
|
+
personRates: z.record(id, moneyMinor).optional()
|
|
152
|
+
})
|
|
153
|
+
.refine((v) => !v.startsOn || !v.endsOn || v.endsOn >= v.startsOn, {
|
|
154
|
+
message: 'End date must not precede start date',
|
|
155
|
+
path: ['endsOn']
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
export const hasInvalidProjectActivityAssignments = (
|
|
159
|
+
requestedIds: string[],
|
|
160
|
+
activities: Array<{ id: string; active: boolean }>,
|
|
161
|
+
existingIds: string[] = []
|
|
162
|
+
) => {
|
|
163
|
+
const existing = new Set(existingIds)
|
|
164
|
+
return requestedIds.some((id) => {
|
|
165
|
+
const activity = activities.find((item) => item.id === id)
|
|
166
|
+
return !activity || (!activity.active && !existing.has(id))
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const projectDeleteSchema = z.object({
|
|
171
|
+
projectName: z.string().min(1).max(160)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export const tariffUpdateSchema = z.object({
|
|
175
|
+
userId: id,
|
|
176
|
+
hourlyRateMinor: moneyMinor
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
export const teamMemberSettingsUpdateSchema = z.object({
|
|
180
|
+
canEnterTime: z.boolean(),
|
|
181
|
+
defaultHourlyRateMinor: moneyMinor.nullable()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
export const internalApprovalWorkspaceUpdateSchema = z.object({ enabled: z.boolean() })
|
|
185
|
+
|
|
186
|
+
export const internalApprovalMemberUpdateSchema = z.object({
|
|
187
|
+
required: z.boolean(),
|
|
188
|
+
approverUserIds: z.array(id).max(100)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
export const settingsUpdateSchema = z.object({
|
|
192
|
+
timerRoundingMinutes: z.number().int().min(1).max(60).optional(),
|
|
193
|
+
currency: z
|
|
194
|
+
.string()
|
|
195
|
+
.length(3)
|
|
196
|
+
.transform((v) => v.toUpperCase())
|
|
197
|
+
.optional(),
|
|
198
|
+
timezone: z.string().min(3).max(100).optional()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
export const reviewSchema = z
|
|
202
|
+
.object({
|
|
203
|
+
action: z.enum(['APPROVE', 'REJECT', 'REOPEN']),
|
|
204
|
+
comment: z.string().trim().max(2000).nullable().optional()
|
|
205
|
+
})
|
|
206
|
+
.superRefine((value, context) => {
|
|
207
|
+
if (value.action === 'REJECT' && !value.comment) {
|
|
208
|
+
context.addIssue({ code: 'custom', path: ['comment'], message: 'A rejection comment is required' })
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
export const submissionCreateSchema = z.object({
|
|
213
|
+
cutoffDate: isoDate,
|
|
214
|
+
comment: z.string().trim().max(2000).optional()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
export const reportQuerySchema = z.object({
|
|
218
|
+
from: isoDate.optional(),
|
|
219
|
+
to: isoDate.optional(),
|
|
220
|
+
clientOrganizationId: id.optional(),
|
|
221
|
+
projectId: id.optional(),
|
|
222
|
+
userId: id.optional(),
|
|
223
|
+
activityTypeId: id.optional(),
|
|
224
|
+
billable: z
|
|
225
|
+
.enum(['true', 'false'])
|
|
226
|
+
.transform((v) => v === 'true')
|
|
227
|
+
.optional(),
|
|
228
|
+
status: z.enum(['DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED']).optional()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
export type ReportQuery = z.infer<typeof reportQuerySchema>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ClientApprovalItemDto } from './types/timesheet'
|
|
2
|
+
|
|
3
|
+
type DashboardApproval = Pick<
|
|
4
|
+
ClientApprovalItemDto,
|
|
5
|
+
'id' | 'status' | 'canAct' | 'periodEndsOn' | 'reviewerUserId' | 'reviewedAt'
|
|
6
|
+
>
|
|
7
|
+
|
|
8
|
+
export const selectClientDashboardApprovals = <Item extends DashboardApproval>(items: Item[], userId: string) => {
|
|
9
|
+
const pending = items
|
|
10
|
+
.filter((item) => item.status === 'PENDING' && item.canAct)
|
|
11
|
+
.sort((a, b) => b.periodEndsOn.localeCompare(a.periodEndsOn) || a.id.localeCompare(b.id))
|
|
12
|
+
const history = items
|
|
13
|
+
.filter((item) => item.status === 'APPROVED' && item.reviewerUserId === userId)
|
|
14
|
+
.sort((a, b) => (b.reviewedAt ?? '').localeCompare(a.reviewedAt ?? '') || a.id.localeCompare(b.id))
|
|
15
|
+
.slice(0, 5)
|
|
16
|
+
return { pendingCount: pending.length, hasHistory: history.length > 0, items: [...pending, ...history] }
|
|
17
|
+
}
|
package/shared/emails.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import type { PortalEmailDefinition } from '@nuxt-customer-portal/core/shared/types/feature'
|
|
2
|
+
|
|
3
|
+
export const timesheetEmails: PortalEmailDefinition[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'internal-requested',
|
|
6
|
+
labelKey: 'features.timesheets.email.messages.internal-requested',
|
|
7
|
+
defaults: {
|
|
8
|
+
en: {
|
|
9
|
+
subject: 'Internal approval requested — {{person_name}} ({{period}})',
|
|
10
|
+
body: 'Dear {{recipient_name}},<br><br>Please review the timesheet.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Open timesheets</a>'
|
|
11
|
+
},
|
|
12
|
+
nl: {
|
|
13
|
+
subject: 'Interne goedkeuring gevraagd — {{person_name}} ({{period}})',
|
|
14
|
+
body: 'Beste {{recipient_name}},<br><br>Beoordeel de urenstaat.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Urenstaten openen</a>'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
placeholders: [
|
|
18
|
+
{
|
|
19
|
+
key: 'recipient_name',
|
|
20
|
+
labelKey: 'features.timesheets.email.placeholders.recipient_name',
|
|
21
|
+
example: 'Alex Example'
|
|
22
|
+
},
|
|
23
|
+
{ key: 'period', labelKey: 'features.timesheets.email.placeholders.period', example: 'do 3 – vr 4 sep 2026' },
|
|
24
|
+
{
|
|
25
|
+
key: 'person_name',
|
|
26
|
+
labelKey: 'features.timesheets.email.placeholders.person_name',
|
|
27
|
+
example: 'Alex Example'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'organization_name',
|
|
31
|
+
labelKey: 'features.timesheets.email.placeholders.organization_name',
|
|
32
|
+
example: 'Example Company'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'period_start',
|
|
36
|
+
labelKey: 'features.timesheets.email.placeholders.period_start',
|
|
37
|
+
example: '2026-09-01'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'period_end',
|
|
41
|
+
labelKey: 'features.timesheets.email.placeholders.period_end',
|
|
42
|
+
example: '2026-09-03'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'client_name',
|
|
46
|
+
labelKey: 'features.timesheets.email.placeholders.client_name',
|
|
47
|
+
example: 'Example Client'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'comment',
|
|
51
|
+
labelKey: 'features.timesheets.email.placeholders.comment',
|
|
52
|
+
example: ''
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'action_url',
|
|
56
|
+
labelKey: 'features.timesheets.email.placeholders.action_url',
|
|
57
|
+
example: 'https://portal.example.com/timesheets'
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'internal-approved',
|
|
63
|
+
labelKey: 'features.timesheets.email.messages.internal-approved',
|
|
64
|
+
defaults: {
|
|
65
|
+
en: {
|
|
66
|
+
subject: 'Timesheet approved internally — {{person_name}} ({{period}})',
|
|
67
|
+
body: 'Dear {{recipient_name}},<br><br>Your timesheet has been approved internally.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Open timesheets</a>'
|
|
68
|
+
},
|
|
69
|
+
nl: {
|
|
70
|
+
subject: 'Urenstaat intern goedgekeurd — {{person_name}} ({{period}})',
|
|
71
|
+
body: 'Beste {{recipient_name}},<br><br>Uw urenstaat is intern goedgekeurd.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Urenstaten openen</a>'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
placeholders: [
|
|
75
|
+
{
|
|
76
|
+
key: 'recipient_name',
|
|
77
|
+
labelKey: 'features.timesheets.email.placeholders.recipient_name',
|
|
78
|
+
example: 'Alex Example'
|
|
79
|
+
},
|
|
80
|
+
{ key: 'period', labelKey: 'features.timesheets.email.placeholders.period', example: 'do 3 – vr 4 sep 2026' },
|
|
81
|
+
{
|
|
82
|
+
key: 'person_name',
|
|
83
|
+
labelKey: 'features.timesheets.email.placeholders.person_name',
|
|
84
|
+
example: 'Alex Example'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'organization_name',
|
|
88
|
+
labelKey: 'features.timesheets.email.placeholders.organization_name',
|
|
89
|
+
example: 'Example Company'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'period_start',
|
|
93
|
+
labelKey: 'features.timesheets.email.placeholders.period_start',
|
|
94
|
+
example: '2026-09-01'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: 'period_end',
|
|
98
|
+
labelKey: 'features.timesheets.email.placeholders.period_end',
|
|
99
|
+
example: '2026-09-03'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'client_name',
|
|
103
|
+
labelKey: 'features.timesheets.email.placeholders.client_name',
|
|
104
|
+
example: 'Example Client'
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: 'comment',
|
|
108
|
+
labelKey: 'features.timesheets.email.placeholders.comment',
|
|
109
|
+
example: ''
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: 'action_url',
|
|
113
|
+
labelKey: 'features.timesheets.email.placeholders.action_url',
|
|
114
|
+
example: 'https://portal.example.com/timesheets'
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'internal-rejected',
|
|
120
|
+
labelKey: 'features.timesheets.email.messages.internal-rejected',
|
|
121
|
+
defaults: {
|
|
122
|
+
en: {
|
|
123
|
+
subject: 'Timesheet rejected internally — {{person_name}} ({{period}})',
|
|
124
|
+
body: 'Dear {{recipient_name}},<br><br>Your timesheet has been rejected internally.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Open timesheets</a>'
|
|
125
|
+
},
|
|
126
|
+
nl: {
|
|
127
|
+
subject: 'Urenstaat intern afgewezen — {{person_name}} ({{period}})',
|
|
128
|
+
body: 'Beste {{recipient_name}},<br><br>Uw urenstaat is intern afgewezen.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Urenstaten openen</a>'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
placeholders: [
|
|
132
|
+
{
|
|
133
|
+
key: 'recipient_name',
|
|
134
|
+
labelKey: 'features.timesheets.email.placeholders.recipient_name',
|
|
135
|
+
example: 'Alex Example'
|
|
136
|
+
},
|
|
137
|
+
{ key: 'period', labelKey: 'features.timesheets.email.placeholders.period', example: 'do 3 – vr 4 sep 2026' },
|
|
138
|
+
{
|
|
139
|
+
key: 'person_name',
|
|
140
|
+
labelKey: 'features.timesheets.email.placeholders.person_name',
|
|
141
|
+
example: 'Alex Example'
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: 'organization_name',
|
|
145
|
+
labelKey: 'features.timesheets.email.placeholders.organization_name',
|
|
146
|
+
example: 'Example Company'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: 'period_start',
|
|
150
|
+
labelKey: 'features.timesheets.email.placeholders.period_start',
|
|
151
|
+
example: '2026-09-01'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: 'period_end',
|
|
155
|
+
labelKey: 'features.timesheets.email.placeholders.period_end',
|
|
156
|
+
example: '2026-09-03'
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: 'client_name',
|
|
160
|
+
labelKey: 'features.timesheets.email.placeholders.client_name',
|
|
161
|
+
example: 'Example Client'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: 'comment',
|
|
165
|
+
labelKey: 'features.timesheets.email.placeholders.comment',
|
|
166
|
+
example: ''
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: 'action_url',
|
|
170
|
+
labelKey: 'features.timesheets.email.placeholders.action_url',
|
|
171
|
+
example: 'https://portal.example.com/timesheets'
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'internal-reopened',
|
|
177
|
+
labelKey: 'features.timesheets.email.messages.internal-reopened',
|
|
178
|
+
defaults: {
|
|
179
|
+
en: {
|
|
180
|
+
subject: 'Timesheet reopened — {{person_name}} ({{period}})',
|
|
181
|
+
body: 'Dear {{recipient_name}},<br><br>Your timesheet has been reopened for editing.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Open timesheets</a>'
|
|
182
|
+
},
|
|
183
|
+
nl: {
|
|
184
|
+
subject: 'Urenstaat heropend — {{person_name}} ({{period}})',
|
|
185
|
+
body: 'Beste {{recipient_name}},<br><br>Uw urenstaat is heropend voor wijzigingen.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Urenstaten openen</a>'
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
placeholders: [
|
|
189
|
+
{
|
|
190
|
+
key: 'recipient_name',
|
|
191
|
+
labelKey: 'features.timesheets.email.placeholders.recipient_name',
|
|
192
|
+
example: 'Alex Example'
|
|
193
|
+
},
|
|
194
|
+
{ key: 'period', labelKey: 'features.timesheets.email.placeholders.period', example: 'do 3 – vr 4 sep 2026' },
|
|
195
|
+
{
|
|
196
|
+
key: 'person_name',
|
|
197
|
+
labelKey: 'features.timesheets.email.placeholders.person_name',
|
|
198
|
+
example: 'Alex Example'
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
key: 'organization_name',
|
|
202
|
+
labelKey: 'features.timesheets.email.placeholders.organization_name',
|
|
203
|
+
example: 'Example Company'
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: 'period_start',
|
|
207
|
+
labelKey: 'features.timesheets.email.placeholders.period_start',
|
|
208
|
+
example: '2026-09-01'
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
key: 'period_end',
|
|
212
|
+
labelKey: 'features.timesheets.email.placeholders.period_end',
|
|
213
|
+
example: '2026-09-03'
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: 'client_name',
|
|
217
|
+
labelKey: 'features.timesheets.email.placeholders.client_name',
|
|
218
|
+
example: 'Example Client'
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
key: 'comment',
|
|
222
|
+
labelKey: 'features.timesheets.email.placeholders.comment',
|
|
223
|
+
example: ''
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: 'action_url',
|
|
227
|
+
labelKey: 'features.timesheets.email.placeholders.action_url',
|
|
228
|
+
example: 'https://portal.example.com/timesheets'
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'client-requested',
|
|
234
|
+
labelKey: 'features.timesheets.email.messages.client-requested',
|
|
235
|
+
defaults: {
|
|
236
|
+
en: {
|
|
237
|
+
subject: 'Client approval requested — {{person_name}} ({{period}})',
|
|
238
|
+
body: 'Dear {{recipient_name}},<br><br>Please review the timesheet for your organization.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Open timesheets</a>'
|
|
239
|
+
},
|
|
240
|
+
nl: {
|
|
241
|
+
subject: 'Klantgoedkeuring gevraagd — {{person_name}} ({{period}})',
|
|
242
|
+
body: 'Beste {{recipient_name}},<br><br>Beoordeel de urenstaat voor uw organisatie.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Urenstaten openen</a>'
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
placeholders: [
|
|
246
|
+
{
|
|
247
|
+
key: 'recipient_name',
|
|
248
|
+
labelKey: 'features.timesheets.email.placeholders.recipient_name',
|
|
249
|
+
example: 'Alex Example'
|
|
250
|
+
},
|
|
251
|
+
{ key: 'period', labelKey: 'features.timesheets.email.placeholders.period', example: 'do 3 – vr 4 sep 2026' },
|
|
252
|
+
{
|
|
253
|
+
key: 'person_name',
|
|
254
|
+
labelKey: 'features.timesheets.email.placeholders.person_name',
|
|
255
|
+
example: 'Alex Example'
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: 'organization_name',
|
|
259
|
+
labelKey: 'features.timesheets.email.placeholders.organization_name',
|
|
260
|
+
example: 'Example Company'
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: 'period_start',
|
|
264
|
+
labelKey: 'features.timesheets.email.placeholders.period_start',
|
|
265
|
+
example: '2026-09-01'
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: 'period_end',
|
|
269
|
+
labelKey: 'features.timesheets.email.placeholders.period_end',
|
|
270
|
+
example: '2026-09-03'
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
key: 'client_name',
|
|
274
|
+
labelKey: 'features.timesheets.email.placeholders.client_name',
|
|
275
|
+
example: 'Example Client'
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
key: 'comment',
|
|
279
|
+
labelKey: 'features.timesheets.email.placeholders.comment',
|
|
280
|
+
example: ''
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
key: 'action_url',
|
|
284
|
+
labelKey: 'features.timesheets.email.placeholders.action_url',
|
|
285
|
+
example: 'https://portal.example.com/timesheets'
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'client-approved',
|
|
291
|
+
labelKey: 'features.timesheets.email.messages.client-approved',
|
|
292
|
+
defaults: {
|
|
293
|
+
en: {
|
|
294
|
+
subject: 'Timesheet approved by client — {{person_name}} ({{period}})',
|
|
295
|
+
body: 'Dear {{recipient_name}},<br><br>The client has approved your timesheet.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Open timesheets</a>'
|
|
296
|
+
},
|
|
297
|
+
nl: {
|
|
298
|
+
subject: 'Urenstaat goedgekeurd door klant — {{person_name}} ({{period}})',
|
|
299
|
+
body: 'Beste {{recipient_name}},<br><br>De klant heeft uw urenstaat goedgekeurd.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Urenstaten openen</a>'
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
placeholders: [
|
|
303
|
+
{
|
|
304
|
+
key: 'recipient_name',
|
|
305
|
+
labelKey: 'features.timesheets.email.placeholders.recipient_name',
|
|
306
|
+
example: 'Alex Example'
|
|
307
|
+
},
|
|
308
|
+
{ key: 'period', labelKey: 'features.timesheets.email.placeholders.period', example: 'do 3 – vr 4 sep 2026' },
|
|
309
|
+
{
|
|
310
|
+
key: 'person_name',
|
|
311
|
+
labelKey: 'features.timesheets.email.placeholders.person_name',
|
|
312
|
+
example: 'Alex Example'
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
key: 'organization_name',
|
|
316
|
+
labelKey: 'features.timesheets.email.placeholders.organization_name',
|
|
317
|
+
example: 'Example Company'
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
key: 'period_start',
|
|
321
|
+
labelKey: 'features.timesheets.email.placeholders.period_start',
|
|
322
|
+
example: '2026-09-01'
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
key: 'period_end',
|
|
326
|
+
labelKey: 'features.timesheets.email.placeholders.period_end',
|
|
327
|
+
example: '2026-09-03'
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
key: 'client_name',
|
|
331
|
+
labelKey: 'features.timesheets.email.placeholders.client_name',
|
|
332
|
+
example: 'Example Client'
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
key: 'comment',
|
|
336
|
+
labelKey: 'features.timesheets.email.placeholders.comment',
|
|
337
|
+
example: ''
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
key: 'action_url',
|
|
341
|
+
labelKey: 'features.timesheets.email.placeholders.action_url',
|
|
342
|
+
example: 'https://portal.example.com/timesheets'
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 'client-disputed',
|
|
348
|
+
labelKey: 'features.timesheets.email.messages.client-disputed',
|
|
349
|
+
defaults: {
|
|
350
|
+
en: {
|
|
351
|
+
subject: 'Timesheet disputed by client — {{person_name}} ({{period}})',
|
|
352
|
+
body: 'Dear {{recipient_name}},<br><br>The client has disputed your timesheet.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Open timesheets</a>'
|
|
353
|
+
},
|
|
354
|
+
nl: {
|
|
355
|
+
subject: 'Urenstaat betwist door klant — {{person_name}} ({{period}})',
|
|
356
|
+
body: 'Beste {{recipient_name}},<br><br>De klant heeft uw urenstaat betwist.<br><br>{{organization_name}}<br>{{person_name}}: {{period}}<br>{{client_name}}<br>{{comment}}<br><br><a href="{{action_url}}" class="button">Urenstaten openen</a>'
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
placeholders: [
|
|
360
|
+
{
|
|
361
|
+
key: 'recipient_name',
|
|
362
|
+
labelKey: 'features.timesheets.email.placeholders.recipient_name',
|
|
363
|
+
example: 'Alex Example'
|
|
364
|
+
},
|
|
365
|
+
{ key: 'period', labelKey: 'features.timesheets.email.placeholders.period', example: 'do 3 – vr 4 sep 2026' },
|
|
366
|
+
{
|
|
367
|
+
key: 'person_name',
|
|
368
|
+
labelKey: 'features.timesheets.email.placeholders.person_name',
|
|
369
|
+
example: 'Alex Example'
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
key: 'organization_name',
|
|
373
|
+
labelKey: 'features.timesheets.email.placeholders.organization_name',
|
|
374
|
+
example: 'Example Company'
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
key: 'period_start',
|
|
378
|
+
labelKey: 'features.timesheets.email.placeholders.period_start',
|
|
379
|
+
example: '2026-09-01'
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
key: 'period_end',
|
|
383
|
+
labelKey: 'features.timesheets.email.placeholders.period_end',
|
|
384
|
+
example: '2026-09-03'
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
key: 'client_name',
|
|
388
|
+
labelKey: 'features.timesheets.email.placeholders.client_name',
|
|
389
|
+
example: 'Example Client'
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
key: 'comment',
|
|
393
|
+
labelKey: 'features.timesheets.email.placeholders.comment',
|
|
394
|
+
example: ''
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
key: 'action_url',
|
|
398
|
+
labelKey: 'features.timesheets.email.placeholders.action_url',
|
|
399
|
+
example: 'https://portal.example.com/timesheets'
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
]
|