@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.
Files changed (141) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +5 -0
  4. package/app/assets/timesheets.css +368 -0
  5. package/app/components/TimesheetsActivityForm.vue +61 -0
  6. package/app/components/TimesheetsAdminActivities.vue +323 -0
  7. package/app/components/TimesheetsAdminApprovals.vue +449 -0
  8. package/app/components/TimesheetsAdminEmptyState.vue +28 -0
  9. package/app/components/TimesheetsAdminListToolbar.vue +19 -0
  10. package/app/components/TimesheetsAdminPaginatedList.vue +95 -0
  11. package/app/components/TimesheetsAdminProjects.vue +234 -0
  12. package/app/components/TimesheetsAdminRates.vue +83 -0
  13. package/app/components/TimesheetsAdminReports.vue +123 -0
  14. package/app/components/TimesheetsAdminSectionPage.vue +135 -0
  15. package/app/components/TimesheetsAdminSettings.vue +15 -0
  16. package/app/components/TimesheetsAdministrationOrganizationPanel.vue +26 -0
  17. package/app/components/TimesheetsClientSettingsPanel.vue +52 -0
  18. package/app/components/TimesheetsClientTimesheetBrowser.vue +439 -0
  19. package/app/components/TimesheetsDashboardCard.vue +260 -0
  20. package/app/components/TimesheetsDashboardClientApprovals.vue +1 -0
  21. package/app/components/TimesheetsDashboardInternalApprovals.vue +1 -0
  22. package/app/components/TimesheetsDashboardMyTimesheets.vue +1 -0
  23. package/app/components/TimesheetsDashboardMyWeek.vue +1 -0
  24. package/app/components/TimesheetsDashboardSupplierTimesheets.vue +47 -0
  25. package/app/components/TimesheetsOrganizationFeaturesCard.vue +66 -0
  26. package/app/components/TimesheetsPageShell.vue +48 -0
  27. package/app/components/TimesheetsProjectDetail.vue +338 -0
  28. package/app/components/TimesheetsProjectForm.vue +215 -0
  29. package/app/components/TimesheetsSetupChecklist.vue +48 -0
  30. package/app/components/TimesheetsSubmissionMessages.vue +30 -0
  31. package/app/components/TimesheetsSubmissionTimeline.vue +89 -0
  32. package/app/components/TimesheetsWorkspaceSettingsForm.vue +80 -0
  33. package/app/composables/useTimesheetMenu.ts +69 -0
  34. package/app/composables/useTimesheetMutationError.ts +35 -0
  35. package/app/composables/useTimesheets.ts +248 -0
  36. package/app/composables/useTimesheetsAdminList.ts +165 -0
  37. package/app/composables/useTimesheetsDashboard.ts +8 -0
  38. package/app/pages/admin/timesheets/activities.vue +3 -0
  39. package/app/pages/admin/timesheets/approvals.vue +3 -0
  40. package/app/pages/admin/timesheets/index.vue +7 -0
  41. package/app/pages/admin/timesheets/internal-approvals.vue +131 -0
  42. package/app/pages/admin/timesheets/projects/[id].vue +57 -0
  43. package/app/pages/admin/timesheets/projects.vue +8 -0
  44. package/app/pages/admin/timesheets/rates.vue +3 -0
  45. package/app/pages/admin/timesheets/reports.vue +3 -0
  46. package/app/pages/admin/timesheets/settings.vue +3 -0
  47. package/app/pages/timesheets/approvals/reviewers.vue +96 -0
  48. package/app/pages/timesheets/approvals.vue +11 -0
  49. package/app/pages/timesheets/client-approvals.vue +108 -0
  50. package/app/pages/timesheets/client.vue +17 -0
  51. package/app/pages/timesheets/index.vue +1458 -0
  52. package/app/pages/timesheets/internal-approvals.vue +100 -0
  53. package/app/pages/timesheets/suppliers.vue +23 -0
  54. package/app/plugins/timesheets-feature.ts +122 -0
  55. package/i18n/locales/en.json +455 -0
  56. package/i18n/locales/nl.json +455 -0
  57. package/migrations/0000_baseline.sql +409 -0
  58. package/migrations/0001_remove_unused_legacy_invoice_schema.sql +31 -0
  59. package/migrations/0002_submission_batches.sql +47 -0
  60. package/migrations/0003_sync_provider_clients.sql +26 -0
  61. package/migrations/0004_client_reply.sql +1 -0
  62. package/migrations/0005_timer_rounding.sql +1 -0
  63. package/migrations/meta/0000_snapshot.json +3178 -0
  64. package/migrations/meta/_journal.json +13 -0
  65. package/nuxt.config.ts +20 -0
  66. package/package.json +61 -0
  67. package/portal.manifest.mjs +10 -0
  68. package/server/api/timesheets/admin/activities/[id]/deletion.get.ts +18 -0
  69. package/server/api/timesheets/admin/activities/[id].delete.ts +20 -0
  70. package/server/api/timesheets/admin/activities/[id].patch.ts +19 -0
  71. package/server/api/timesheets/admin/activities/index.get.ts +19 -0
  72. package/server/api/timesheets/admin/activities/index.post.ts +19 -0
  73. package/server/api/timesheets/admin/approvals/[id].post.ts +19 -0
  74. package/server/api/timesheets/admin/bootstrap.get.ts +73 -0
  75. package/server/api/timesheets/admin/client-settings/[clientId]/index.get.ts +25 -0
  76. package/server/api/timesheets/admin/client-settings/[clientId]/index.put.ts +28 -0
  77. package/server/api/timesheets/admin/clients/[id]/deletion.get.ts +18 -0
  78. package/server/api/timesheets/admin/clients/[id].delete.ts +20 -0
  79. package/server/api/timesheets/admin/clients/[id].patch.ts +14 -0
  80. package/server/api/timesheets/admin/clients/index.get.ts +18 -0
  81. package/server/api/timesheets/admin/clients/index.post.ts +25 -0
  82. package/server/api/timesheets/admin/internal-approvals/[userId].put.ts +11 -0
  83. package/server/api/timesheets/admin/internal-approvals/index.get.ts +8 -0
  84. package/server/api/timesheets/admin/internal-approvals/index.patch.ts +11 -0
  85. package/server/api/timesheets/admin/organization-capabilities/[organizationId].get.ts +19 -0
  86. package/server/api/timesheets/admin/organization-capabilities/[organizationId].patch.ts +23 -0
  87. package/server/api/timesheets/admin/projects/[id]/deletion.get.ts +18 -0
  88. package/server/api/timesheets/admin/projects/[id].delete.ts +20 -0
  89. package/server/api/timesheets/admin/projects/[id].get.ts +22 -0
  90. package/server/api/timesheets/admin/projects/[id].patch.ts +19 -0
  91. package/server/api/timesheets/admin/projects/index.get.ts +18 -0
  92. package/server/api/timesheets/admin/projects/index.post.ts +19 -0
  93. package/server/api/timesheets/admin/report.get.ts +26 -0
  94. package/server/api/timesheets/admin/settings.patch.ts +19 -0
  95. package/server/api/timesheets/admin/tariffs/index.put.ts +20 -0
  96. package/server/api/timesheets/admin/team/[userId].put.ts +24 -0
  97. package/server/api/timesheets/bootstrap.get.ts +24 -0
  98. package/server/api/timesheets/capabilities.get.ts +58 -0
  99. package/server/api/timesheets/client/[workspaceClientId]/index.get.ts +12 -0
  100. package/server/api/timesheets/client/[workspaceClientId]/reviewers.get.ts +10 -0
  101. package/server/api/timesheets/client/[workspaceClientId]/reviewers.put.ts +18 -0
  102. package/server/api/timesheets/client/[workspaceClientId]/reviews/[submissionId].post.ts +18 -0
  103. package/server/api/timesheets/client/approval-members.get.ts +11 -0
  104. package/server/api/timesheets/client/approval-suppliers.get.ts +8 -0
  105. package/server/api/timesheets/client/approvals.get.ts +14 -0
  106. package/server/api/timesheets/client/reviewer-suppliers.get.ts +10 -0
  107. package/server/api/timesheets/client/supplier-options.get.ts +14 -0
  108. package/server/api/timesheets/client/supplier-timesheets.get.ts +17 -0
  109. package/server/api/timesheets/client/workspaces.get.ts +11 -0
  110. package/server/api/timesheets/client-approvals.get.ts +18 -0
  111. package/server/api/timesheets/dashboard.get.ts +191 -0
  112. package/server/api/timesheets/entries/[id].delete.ts +18 -0
  113. package/server/api/timesheets/entries/[id].patch.ts +23 -0
  114. package/server/api/timesheets/entries/index.post.ts +22 -0
  115. package/server/api/timesheets/internal-approvals/[id].post.ts +9 -0
  116. package/server/api/timesheets/internal-approvals/index.get.ts +37 -0
  117. package/server/api/timesheets/internal-approvals/list.get.ts +12 -0
  118. package/server/api/timesheets/setup-status.get.ts +21 -0
  119. package/server/api/timesheets/submissions/[id]/client-reply.post.ts +20 -0
  120. package/server/api/timesheets/submissions/[id]/request-client-approval.post.ts +21 -0
  121. package/server/api/timesheets/submissions/[id]/resubmit.post.ts +10 -0
  122. package/server/api/timesheets/timer/index.delete.ts +18 -0
  123. package/server/api/timesheets/timer/index.post.ts +23 -0
  124. package/server/api/timesheets/weeks/[id]/submissions.post.ts +19 -0
  125. package/server/db/schema/timesheets.ts +383 -0
  126. package/server/plugins/client-sync.ts +30 -0
  127. package/server/plugins/openapi-contracts.ts +57 -0
  128. package/server/utils/timesheet-admin-listing.ts +281 -0
  129. package/server/utils/timesheet-email.ts +221 -0
  130. package/server/utils/timesheet-repository.ts +3096 -0
  131. package/server/utils/timesheet-validation.ts +231 -0
  132. package/shared/client-approval-dashboard.ts +17 -0
  133. package/shared/emails.ts +403 -0
  134. package/shared/feature.ts +184 -0
  135. package/shared/period-lock.ts +8 -0
  136. package/shared/submission-reply.ts +3 -0
  137. package/shared/timer-rounding.ts +2 -0
  138. package/shared/timesheet-dates.ts +23 -0
  139. package/shared/timesheet-errors.ts +13 -0
  140. package/shared/types/timesheet.ts +424 -0
  141. package/tokens.css +25 -0
@@ -0,0 +1,260 @@
1
+ <script setup lang="ts">
2
+ import { formatTimesheetPeriod } from '@nuxt-customer-portal/timesheets/shared/timesheet-dates'
3
+ import { addDays, getISOWeek, parseISO } from 'date-fns'
4
+
5
+ const props = defineProps<{ section: 'myTimesheets' | 'myWeek' | 'internalApprovals' | 'clientApprovals' }>()
6
+ const { t, locale } = useI18n()
7
+ const { data, pending, error, refresh } = await useTimesheetsDashboard()
8
+ interface DashboardCardValue {
9
+ previousSubmissions?: Array<{
10
+ id: string
11
+ weekStartsOn: string
12
+ periodStartsOn: string
13
+ periodEndsOn: string
14
+ status: string
15
+ totalMinutes: number
16
+ }>
17
+ projects: Array<{ id: string; name: string; clientName: string; totalMinutes: number }>
18
+ weekStartsOn: string
19
+ status: string
20
+ totalMinutes: number
21
+ rejectionComment: string | null
22
+ hasRunningTimer: boolean
23
+ batches: Array<{
24
+ id: string
25
+ status: string
26
+ totalMinutes: number
27
+ periodStartsOn: string
28
+ periodEndsOn: string
29
+ }>
30
+ unsubmitted: { totalMinutes: number; periodStartsOn: string; periodEndsOn: string } | null
31
+ pendingCount: number
32
+ hasHistory: boolean
33
+ unassignedSupplierCount: number
34
+ items: Array<{
35
+ id: string
36
+ userName: string
37
+ weekStartsOn: string
38
+ periodStartsOn: string
39
+ periodEndsOn: string
40
+ totalMinutes: number
41
+ person: string
42
+ supplierName: string
43
+ status: string
44
+ }>
45
+ }
46
+ const value = computed(
47
+ () =>
48
+ data.value?.[props.section === 'myTimesheets' ? 'myWeek' : props.section] as unknown as
49
+ DashboardCardValue | undefined
50
+ )
51
+ const minutes = (amount: number) => `${Math.floor(amount / 60)}:${String(amount % 60).padStart(2, '0')}`
52
+ const duration = (amount: number) =>
53
+ t(amount === 60 ? 'features.timesheets.dashboard.duration.one' : 'features.timesheets.dashboard.duration.other', {
54
+ value: minutes(amount)
55
+ })
56
+ const submissionPeriod = (from: string, to: string) => formatTimesheetPeriod(from, to, locale.value)
57
+ const weekPeriod = (weekStartsOn: string) => {
58
+ const start = parseISO(weekStartsOn)
59
+ const end = addDays(start, 6)
60
+ const sameYear = start.getFullYear() === end.getFullYear()
61
+ const startLabel = new Intl.DateTimeFormat(locale.value, {
62
+ day: 'numeric',
63
+ month: 'short',
64
+ year: sameYear ? undefined : 'numeric'
65
+ }).format(start)
66
+ const endLabel = new Intl.DateTimeFormat(locale.value, {
67
+ day: 'numeric',
68
+ month: 'short',
69
+ year: 'numeric'
70
+ }).format(end)
71
+ return t('features.timesheets.dashboard.myWeek.period', {
72
+ week: getISOWeek(start),
73
+ start: startLabel,
74
+ end: endLabel
75
+ })
76
+ }
77
+ const title = computed(() => t(`features.timesheets.dashboard.${props.section}.title`))
78
+ const icon = computed(
79
+ () =>
80
+ ({
81
+ myTimesheets: 'i-lucide-calendar-check',
82
+ myWeek: 'i-lucide-clock-3',
83
+ internalApprovals: 'i-lucide-stamp',
84
+ clientApprovals: 'i-lucide-building-2'
85
+ })[props.section]
86
+ )
87
+ </script>
88
+
89
+ <template>
90
+ <UCard class="h-full">
91
+ <template #header>
92
+ <div class="flex items-center justify-between gap-3">
93
+ <h2 class="font-semibold">{{ title }}</h2>
94
+ <UIcon :name="icon" class="size-5 text-primary" />
95
+ </div>
96
+ </template>
97
+
98
+ <div v-if="pending" class="space-y-3" aria-live="polite">
99
+ <USkeleton class="h-8 w-24" /><USkeleton class="h-4 w-full" /><USkeleton class="h-4 w-2/3" />
100
+ </div>
101
+ <div v-else-if="error" class="py-3 text-center">
102
+ <p class="text-sm text-muted">{{ t('features.timesheets.dashboard.error') }}</p>
103
+ <UButton class="mt-3" color="neutral" variant="outline" size="sm" icon="i-lucide-refresh-cw" @click="refresh()">
104
+ {{ t('features.timesheets.dashboard.retry') }}
105
+ </UButton>
106
+ </div>
107
+ <div v-else-if="!value" class="py-4 text-center text-sm text-muted">
108
+ {{ t('features.timesheets.dashboard.unavailable') }}
109
+ </div>
110
+
111
+ <template v-else-if="section === 'myWeek'">
112
+ <div class="flex items-end justify-between gap-4">
113
+ <div class="min-w-0 flex-1">
114
+ <p class="text-3xl font-semibold tabular-nums">{{ duration(value.totalMinutes) }}</p>
115
+ <p class="mt-1 text-sm text-muted">
116
+ {{ weekPeriod(value.weekStartsOn) }}
117
+ </p>
118
+ <ul class="mt-3 space-y-2">
119
+ <li
120
+ v-for="project in value.projects"
121
+ :key="project.id"
122
+ class="flex items-center justify-between gap-4 text-sm"
123
+ >
124
+ <span>{{ project.clientName }} · {{ project.name }}</span>
125
+ <span class="shrink-0 tabular-nums">{{ duration(project.totalMinutes) }}</span>
126
+ </li>
127
+ </ul>
128
+ </div>
129
+ <UButton to="/timesheets" variant="outline" trailing-icon="i-lucide-arrow-right">
130
+ {{ t('features.timesheets.dashboard.open') }}
131
+ </UButton>
132
+ </div>
133
+
134
+ <p v-if="value.hasRunningTimer" class="mt-4 text-sm text-primary">
135
+ {{ t('features.timesheets.dashboard.myWeek.timerRunning') }}
136
+ </p>
137
+ </template>
138
+
139
+ <template v-else-if="section === 'myTimesheets'">
140
+ <section v-if="value.previousSubmissions?.length">
141
+ <ul class="divide-y divide-default">
142
+ <li v-for="submission in value.previousSubmissions" :key="submission.id">
143
+ <NuxtLink
144
+ :to="{ path: '/timesheets', query: { week: submission.weekStartsOn } }"
145
+ class="flex items-center justify-between gap-3 rounded py-2 hover:bg-elevated"
146
+ >
147
+ <span class="text-sm">{{
148
+ formatTimesheetPeriod(submission.periodStartsOn, submission.periodEndsOn, locale)
149
+ }}</span>
150
+ <span class="flex shrink-0 items-center gap-2">
151
+ <span class="text-sm tabular-nums">{{ duration(submission.totalMinutes) }}</span>
152
+ <UBadge
153
+ :color="
154
+ submission.status === 'APPROVED'
155
+ ? 'success'
156
+ : submission.status === 'REJECTED'
157
+ ? 'error'
158
+ : 'warning'
159
+ "
160
+ variant="subtle"
161
+ >{{ t(`features.timesheets.status.${submission.status.toLowerCase()}`) }}</UBadge
162
+ >
163
+ </span>
164
+ </NuxtLink>
165
+ </li>
166
+ </ul>
167
+ </section>
168
+ <p v-else class="text-sm text-muted">{{ t('features.timesheets.dashboard.myTimesheets.empty') }}</p>
169
+ </template>
170
+
171
+ <template v-else-if="section === 'internalApprovals'">
172
+ <div class="flex items-center justify-between">
173
+ <p class="text-3xl font-semibold tabular-nums">{{ value.pendingCount }}</p>
174
+ <UButton
175
+ v-if="value.pendingCount > 0 || value.items.length > 0"
176
+ :to="
177
+ value.pendingCount > 0
178
+ ? '/timesheets/internal-approvals?status=SUBMITTED'
179
+ : '/timesheets/internal-approvals'
180
+ "
181
+ variant="outline"
182
+ >
183
+ {{
184
+ t(value.pendingCount > 0 ? 'features.timesheets.dashboard.review' : 'features.timesheets.dashboard.viewAll')
185
+ }}
186
+ </UButton>
187
+ </div>
188
+ <p v-if="!value.items.length" class="mt-4 text-sm text-muted">
189
+ {{ t('features.timesheets.dashboard.internalApprovals.empty') }}
190
+ </p>
191
+ <ul v-else class="mt-4 divide-y divide-default">
192
+ <li v-for="item in value.items" :key="item.id" class="flex items-center justify-between gap-3 py-2 text-sm">
193
+ <span class="truncate"
194
+ ><strong>{{ item.userName }}</strong
195
+ ><span class="block text-muted">{{ submissionPeriod(item.periodStartsOn, item.periodEndsOn) }}</span></span
196
+ ><span class="shrink-0 text-right"
197
+ ><strong class="block tabular-nums">{{ duration(item.totalMinutes) }}</strong
198
+ ><UBadge
199
+ :color="item.status === 'APPROVED' ? 'success' : item.status === 'REJECTED' ? 'error' : 'warning'"
200
+ variant="subtle"
201
+ size="sm"
202
+ class="mt-1"
203
+ >{{ t(`features.timesheets.status.${item.status.toLowerCase()}`) }}</UBadge
204
+ ></span
205
+ >
206
+ </li>
207
+ </ul>
208
+ </template>
209
+
210
+ <template v-else-if="section === 'clientApprovals'">
211
+ <div class="flex items-center justify-between">
212
+ <p class="text-3xl font-semibold tabular-nums">{{ value.pendingCount }}</p>
213
+ <UButton
214
+ v-if="value.pendingCount > 0 || value.hasHistory"
215
+ :to="value.pendingCount > 0 ? '/timesheets/approvals?status=PENDING' : '/timesheets/approvals'"
216
+ variant="outline"
217
+ >
218
+ {{
219
+ t(value.pendingCount > 0 ? 'features.timesheets.dashboard.review' : 'features.timesheets.dashboard.viewAll')
220
+ }}
221
+ </UButton>
222
+ </div>
223
+ <UAlert
224
+ v-if="value.unassignedSupplierCount"
225
+ class="mt-4"
226
+ color="warning"
227
+ variant="subtle"
228
+ icon="i-lucide-user-round-x"
229
+ :description="t('features.timesheets.dashboard.clientApprovals.noReviewer')"
230
+ />
231
+ <p v-if="!value.items.length" class="mt-4 text-sm text-muted">
232
+ {{ t('features.timesheets.dashboard.clientApprovals.empty') }}
233
+ </p>
234
+ <ul v-else class="mt-4 divide-y divide-default">
235
+ <li v-for="item in value.items" :key="item.id" class="flex justify-between gap-3 py-2 text-sm">
236
+ <span class="truncate"
237
+ ><strong>{{ item.person }}</strong
238
+ ><span class="block text-muted">{{ item.supplierName }}</span>
239
+ <span class="block text-muted">{{ submissionPeriod(item.periodStartsOn, item.periodEndsOn) }}</span></span
240
+ ><span class="shrink-0 text-right">
241
+ <strong class="block tabular-nums">{{ duration(item.totalMinutes) }}</strong>
242
+ <UBadge
243
+ :color="item.status === 'APPROVED' ? 'success' : item.status === 'DISPUTED' ? 'error' : 'warning'"
244
+ variant="subtle"
245
+ size="sm"
246
+ class="mt-1"
247
+ >{{
248
+ t(
249
+ item.status === 'PENDING'
250
+ ? 'features.timesheets.approvals.pending'
251
+ : `features.timesheets.clientPortal.${item.status.toLowerCase()}`
252
+ )
253
+ }}</UBadge
254
+ >
255
+ </span>
256
+ </li>
257
+ </ul>
258
+ </template>
259
+ </UCard>
260
+ </template>
@@ -0,0 +1 @@
1
+ <template><TimesheetsDashboardCard section="clientApprovals" /></template>
@@ -0,0 +1 @@
1
+ <template><TimesheetsDashboardCard section="internalApprovals" /></template>
@@ -0,0 +1 @@
1
+ <template><TimesheetsDashboardCard section="myTimesheets" /></template>
@@ -0,0 +1 @@
1
+ <template><TimesheetsDashboardCard section="myWeek" /></template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ const { t, locale } = useI18n()
3
+ const { data, pending, error, refresh } = await useTimesheetsDashboard()
4
+ const minutes = (amount: number) => `${Math.floor(amount / 60)}:${String(amount % 60).padStart(2, '0')}`
5
+ const date = (input: string) =>
6
+ new Intl.DateTimeFormat(locale.value, { dateStyle: 'medium' }).format(new Date(`${input}T00:00:00`))
7
+ </script>
8
+
9
+ <template>
10
+ <UCard class="h-full">
11
+ <template #header>
12
+ <div class="flex items-center justify-between gap-3">
13
+ <h2 class="font-semibold">{{ t('features.timesheets.dashboard.supplierTimesheets.title') }}</h2>
14
+ <UIcon name="i-lucide-building-2" class="size-5 text-primary" />
15
+ </div>
16
+ </template>
17
+ <div v-if="pending" class="space-y-3"><USkeleton class="h-8 w-24" /><USkeleton class="h-4 w-full" /></div>
18
+ <div v-else-if="error" class="py-3 text-center">
19
+ <p class="text-sm text-muted">{{ t('features.timesheets.dashboard.error') }}</p>
20
+ <UButton class="mt-3" color="neutral" variant="outline" size="sm" icon="i-lucide-refresh-cw" @click="refresh()">
21
+ {{ t('features.timesheets.dashboard.retry') }}
22
+ </UButton>
23
+ </div>
24
+ <template v-else-if="data?.supplierTimesheets">
25
+ <p v-if="!data.supplierTimesheets.items.length" class="py-4 text-sm text-muted">
26
+ {{ t('features.timesheets.dashboard.supplierTimesheets.empty') }}
27
+ </p>
28
+ <ul v-else class="divide-y divide-default">
29
+ <li
30
+ v-for="item in data.supplierTimesheets.items"
31
+ :key="item.id"
32
+ class="flex items-center justify-between gap-3 py-3 text-sm"
33
+ >
34
+ <span class="min-w-0"
35
+ ><strong class="block truncate">{{ item.person }}</strong
36
+ ><span class="text-muted">{{ item.supplierName }} · {{ date(item.weekStartsOn) }}</span></span
37
+ ><strong class="shrink-0 tabular-nums">{{ minutes(item.totalMinutes) }}</strong>
38
+ </li>
39
+ </ul>
40
+ <div class="mt-3 text-right">
41
+ <UButton to="/timesheets/suppliers" color="neutral" variant="ghost" trailing-icon="i-lucide-arrow-right">
42
+ {{ t('features.timesheets.dashboard.open') }}
43
+ </UButton>
44
+ </div>
45
+ </template>
46
+ </UCard>
47
+ </template>
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import type { OrganizationTimesheetCapabilities } from '@nuxt-customer-portal/timesheets/app/composables/useTimesheets'
3
+
4
+ const props = defineProps<{ organizationId: string; modelValue: OrganizationTimesheetCapabilities }>()
5
+ const emit = defineEmits<{ 'update:modelValue': [value: OrganizationTimesheetCapabilities] }>()
6
+ const { t } = useI18n()
7
+ const toast = useToast()
8
+ const api = useTimesheets()
9
+ const busy = ref(false)
10
+ const update = async (change: Partial<Pick<OrganizationTimesheetCapabilities, 'workspaceEnabled'>>) => {
11
+ const next = { ...props.modelValue, ...change }
12
+ busy.value = true
13
+ try {
14
+ await api.updateOrganizationTimesheetCapabilities(props.organizationId, {
15
+ workspaceEnabled: next.workspaceEnabled
16
+ })
17
+ emit('update:modelValue', next)
18
+ toast.add({ title: t('features.timesheets.organizationFeatures.saved'), color: 'success' })
19
+ } catch (error) {
20
+ toast.add({ title: t('features.timesheets.messages.saveError'), description: String(error), color: 'error' })
21
+ } finally {
22
+ busy.value = false
23
+ }
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <UCard>
29
+ <template #header>
30
+ <div class="flex items-center gap-2">
31
+ <UIcon name="i-lucide-blocks" class="size-5" />
32
+ <h2 class="text-lg font-semibold">{{ t('features.timesheets.organizationFeatures.title') }}</h2>
33
+ </div>
34
+ </template>
35
+ <div class="space-y-4">
36
+ <div class="flex items-start justify-between gap-4">
37
+ <div>
38
+ <p class="font-medium">{{ t('features.timesheets.organizationFeatures.workspace') }}</p>
39
+ <p class="text-sm text-muted">{{ t('features.timesheets.organizationFeatures.workspaceDescription') }}</p>
40
+ </div>
41
+ <USwitch
42
+ :model-value="modelValue.workspaceEnabled"
43
+ :disabled="busy"
44
+ @update:model-value="update({ workspaceEnabled: $event })"
45
+ />
46
+ </div>
47
+ <div class="border-t border-default pt-4">
48
+ <p class="font-medium">{{ t('features.timesheets.organizationFeatures.clientRelationships') }}</p>
49
+ <p v-if="!modelValue.clientOf.length" class="mt-1 text-sm text-muted">
50
+ {{ t('features.timesheets.organizationFeatures.noClientRelationships') }}
51
+ </p>
52
+ <div v-else class="mt-2 flex flex-wrap gap-2">
53
+ <UBadge
54
+ v-for="relationship in modelValue.clientOf"
55
+ :key="relationship.workspaceOrganizationId"
56
+ color="neutral"
57
+ variant="subtle"
58
+ >
59
+ {{ relationship.workspaceName }} ·
60
+ {{ t(`features.timesheets.clientAccess.${relationship.accessMode.toLowerCase()}`) }}
61
+ </UBadge>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </UCard>
66
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import type { TimesheetsSetupStatusDto } from '@nuxt-customer-portal/timesheets/shared/types/timesheet'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ as?: string
7
+ width?: 'wide' | 'narrow'
8
+ setupStatus?: TimesheetsSetupStatusDto
9
+ }>(),
10
+ {
11
+ as: 'div',
12
+ width: 'wide',
13
+ setupStatus: undefined
14
+ }
15
+ )
16
+ const { isOrganizationAdmin } = useTimesheetMenu()
17
+ const { activeOrganizationType } = usePortalSession()
18
+ const timesheets = useTimesheets()
19
+ const { data: loadedSetupStatus } = await useAsyncData<TimesheetsSetupStatusDto | null>(
20
+ 'timesheets-setup-status',
21
+ async () =>
22
+ activeOrganizationType.value === 'PROVIDER' && isOrganizationAdmin.value && !props.setupStatus
23
+ ? await timesheets.setupStatus()
24
+ : null
25
+ )
26
+ const visibleSetupStatus = computed(() => props.setupStatus ?? loadedSetupStatus.value)
27
+ </script>
28
+
29
+ <template>
30
+ <component
31
+ :is="as"
32
+ data-timesheets-page-shell
33
+ class="mx-auto w-full px-4 pt-4 pb-5 sm:px-5 md:px-6 md:py-6 lg:px-8 lg:py-8"
34
+ :class="width === 'narrow' ? 'max-w-6xl' : 'max-w-[1440px]'"
35
+ >
36
+ <TimesheetsSetupChecklist
37
+ v-if="
38
+ activeOrganizationType === 'PROVIDER' &&
39
+ isOrganizationAdmin &&
40
+ visibleSetupStatus &&
41
+ !visibleSetupStatus.complete
42
+ "
43
+ :status="visibleSetupStatus"
44
+ class="mb-5"
45
+ />
46
+ <slot />
47
+ </component>
48
+ </template>