@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,100 @@
1
+ <script setup lang="ts">
2
+ import type { ApprovalQueueItemDto } from '@nuxt-customer-portal/timesheets/shared/types/timesheet'
3
+
4
+ const { t } = useI18n()
5
+ const timesheets = useTimesheets()
6
+ const { data, error } = await useAsyncData('timesheets-internal-approval-context', () =>
7
+ timesheets.internalApprovalQueue(true)
8
+ )
9
+ const listing = useTimesheetsAdminList<ApprovalQueueItemDto>({
10
+ endpoint: '/api/timesheets/internal-approvals/list',
11
+ filterKeys: ['status', 'userId'],
12
+ defaultSort: 'weekStartsOn',
13
+ defaultSortDir: 'desc'
14
+ })
15
+ const filters = computed(() => [
16
+ {
17
+ key: 'userId',
18
+ placeholder: t('features.timesheets.internalApprovals.memberFilter'),
19
+ items: [
20
+ { label: t('features.timesheets.internalApprovals.allMembers'), value: undefined },
21
+ ...(data.value?.teamMembers ?? []).map((member) => ({ label: member.name, value: member.id }))
22
+ ]
23
+ },
24
+ {
25
+ key: 'status',
26
+ placeholder: t('features.timesheets.approvals.statusFilter'),
27
+ items: [
28
+ { label: t('features.timesheets.approvals.allStatuses'), value: undefined },
29
+ ...['SUBMITTED', 'APPROVED', 'REJECTED'].map((status) => ({
30
+ label: t(`features.timesheets.status.${status.toLowerCase()}`),
31
+ value: status
32
+ }))
33
+ ]
34
+ }
35
+ ])
36
+ const sortOptions = computed(() => [
37
+ { label: t('features.timesheets.approvals.sortNewest'), value: 'weekStartsOn' },
38
+ { label: t('features.timesheets.approvals.sortPerson'), value: 'userName' },
39
+ { label: t('features.timesheets.approvals.sortHours'), value: 'totalMinutes' },
40
+ { label: t('features.timesheets.approvals.sortStatus'), value: 'status' }
41
+ ])
42
+ const approvalData = computed(() => (data.value ? { ...data.value, approvals: [...listing.items.value] } : null))
43
+ await listing.load()
44
+ useSeoMeta({ title: () => t('features.timesheets.internalApprovals.title') })
45
+ </script>
46
+
47
+ <template>
48
+ <TimesheetsPageShell class="flex h-full min-h-0 flex-col gap-4 overflow-hidden">
49
+ <header class="flex shrink-0 items-center justify-between gap-3 border-b border-default pb-4 sm:items-end">
50
+ <div class="flex min-w-0 gap-3">
51
+ <UIcon name="i-lucide-stamp" class="mt-1 size-6 shrink-0 text-primary" />
52
+ <div class="min-w-0">
53
+ <h1 class="text-2xl font-semibold text-highlighted">
54
+ {{ t('features.timesheets.internalApprovals.title') }}
55
+ </h1>
56
+ <p class="hidden text-sm text-muted sm:block">
57
+ {{ t('features.timesheets.internalApprovals.subtitle') }}
58
+ </p>
59
+ </div>
60
+ </div>
61
+ </header>
62
+ <TimesheetsAdminListToolbar
63
+ v-model:search="listing.search.value"
64
+ class="shrink-0"
65
+ :filters="filters"
66
+ :filter-values="listing.filters"
67
+ :sort-options="sortOptions"
68
+ :sort-by="listing.sortBy.value"
69
+ :sort-dir="listing.sortDir.value"
70
+ @filter="listing.setFilter"
71
+ @sort="listing.sortBy.value = $event"
72
+ @toggle-direction="listing.toggleSortDir"
73
+ />
74
+ <TimesheetsAdminPaginatedList
75
+ class="min-h-0 flex-1"
76
+ :pagination="listing.pagination.value"
77
+ :pending="listing.pending.value"
78
+ :loading-next="listing.loadingNextPage.value"
79
+ :loading-previous="listing.loadingPreviousPage.value"
80
+ :has-next="listing.hasNextPage.value"
81
+ :has-previous="listing.hasPreviousPage.value"
82
+ @next="listing.loadNext"
83
+ @previous="listing.loadPrevious"
84
+ @page="listing.goToPage"
85
+ >
86
+ <UAlert v-if="error || listing.error.value" color="error" :title="t('features.timesheets.dashboard.error')" />
87
+ <div v-else-if="listing.pending.value && !listing.items.value.length" class="space-y-3">
88
+ <USkeleton v-for="index in 3" :key="index" class="h-32 w-full" />
89
+ </div>
90
+ <UAlert
91
+ v-else-if="!listing.items.value.length"
92
+ icon="i-lucide-search-x"
93
+ :title="t('features.timesheets.approvals.emptyFiltered')"
94
+ :description="t('features.timesheets.approvals.emptyFilteredDescription')"
95
+ variant="outline"
96
+ />
97
+ <TimesheetsAdminApprovals v-else-if="approvalData" :data="approvalData" :refresh="listing.refresh" />
98
+ </TimesheetsAdminPaginatedList>
99
+ </TimesheetsPageShell>
100
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ const { t } = useI18n()
3
+ const capabilities = await $fetch<{ canAccessApprovals: boolean; canEnterTime: boolean; canViewSupplierTime: boolean }>(
4
+ '/api/timesheets/capabilities'
5
+ )
6
+
7
+ if (!capabilities.canViewSupplierTime) {
8
+ await navigateTo(
9
+ capabilities.canEnterTime
10
+ ? '/timesheets'
11
+ : capabilities.canAccessApprovals
12
+ ? '/timesheets/approvals'
13
+ : '/dashboard',
14
+ { replace: true }
15
+ )
16
+ }
17
+
18
+ useSeoMeta({ title: () => t('features.timesheets.suppliers.title') })
19
+ </script>
20
+
21
+ <template>
22
+ <TimesheetsClientTimesheetBrowser mode="view" />
23
+ </template>
@@ -0,0 +1,122 @@
1
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
2
+ import type { TimesheetCapabilitiesDto } from '@nuxt-customer-portal/timesheets/shared/types/timesheet'
3
+
4
+ export default defineNuxtPlugin(() => {
5
+ const portalFeatures = usePortalFeatures()
6
+ const { activeOrganizationId } = usePortalSession()
7
+ const primaryBadge = (count: number) =>
8
+ count
9
+ ? {
10
+ label: count,
11
+ color: 'primary' as const,
12
+ variant: 'solid' as const,
13
+ square: true,
14
+ class: 'size-5 justify-center rounded-full p-0'
15
+ }
16
+ : undefined
17
+ const register = (capabilities: TimesheetCapabilitiesDto) => {
18
+ const approvalActionCount = capabilities.pendingInternalApprovalCount + capabilities.pendingClientApprovalCount
19
+ const keep = (id: string) =>
20
+ id === 'client-approvals'
21
+ ? capabilities.canReviewClientTimesheets
22
+ : id === 'timesheets-approvals'
23
+ ? capabilities.canApproveInternalTimesheets
24
+ : id === 'timesheets-admin'
25
+ ? capabilities.canManageTimesheets
26
+ : ['internal-approval-settings', 'provider-client-approvals'].includes(id)
27
+ ? capabilities.canManageTimesheets
28
+ : id === 'approval-reviewers'
29
+ ? capabilities.canManageClientReviewers
30
+ : ['supplier-timesheets', 'timesheets-suppliers'].includes(id)
31
+ ? capabilities.canViewSupplierTime
32
+ : capabilities.canEnterTime
33
+ const canAccessTimesheets =
34
+ capabilities.canEnterTime ||
35
+ capabilities.canViewSupplierTime ||
36
+ capabilities.canReviewClientTimesheets ||
37
+ capabilities.canApproveInternalTimesheets ||
38
+ capabilities.canManageTimesheets
39
+ const timesheetsLandingTo = capabilities.canEnterTime
40
+ ? '/timesheets'
41
+ : capabilities.canReviewClientTimesheets
42
+ ? '/timesheets/approvals'
43
+ : capabilities.canApproveInternalTimesheets
44
+ ? '/timesheets/internal-approvals'
45
+ : capabilities.canViewSupplierTime
46
+ ? '/timesheets/suppliers'
47
+ : '/admin/timesheets/internal-approvals'
48
+ portalFeatures.registerFeature({
49
+ ...timesheetsFeature,
50
+ navigation: timesheetsFeature.navigation
51
+ ?.filter((item) => keep(item.id))
52
+ .map((item) => ({
53
+ ...item,
54
+ badge:
55
+ item.id === 'timesheets-approvals' ? primaryBadge(capabilities.pendingInternalApprovalCount) : undefined
56
+ })),
57
+ modules: timesheetsFeature.modules
58
+ ?.filter(() => canAccessTimesheets)
59
+ .map((module) => ({
60
+ ...module,
61
+ to: timesheetsLandingTo,
62
+ badge: module.id === 'timesheets' ? primaryBadge(approvalActionCount) : undefined,
63
+ menuItems: module.menuItems
64
+ ?.filter((item) => keep(item.id))
65
+ .map((item) => ({
66
+ ...item,
67
+ badge:
68
+ item.id === 'client-approvals'
69
+ ? primaryBadge(capabilities.pendingClientApprovalCount)
70
+ : item.id === 'timesheet-approvals'
71
+ ? primaryBadge(capabilities.pendingInternalApprovalCount)
72
+ : item.id === 'approval-reviewers'
73
+ ? primaryBadge(capabilities.unassignedClientReviewerSupplierCount)
74
+ : undefined
75
+ }))
76
+ })),
77
+ dashboardWidgets: timesheetsFeature.dashboardWidgets?.filter((widget) =>
78
+ ['timesheets-my-week', 'timesheets-my-timesheets'].includes(widget.id)
79
+ ? capabilities.canEnterTime
80
+ : widget.id === 'timesheets-internal-approvals'
81
+ ? capabilities.canApproveInternalTimesheets
82
+ : widget.id === 'timesheets-client-approvals'
83
+ ? capabilities.canReviewClientTimesheets
84
+ : capabilities.canViewSupplierTime
85
+ )
86
+ })
87
+ }
88
+ const empty: TimesheetCapabilitiesDto = {
89
+ canEnterTime: false,
90
+ canApproveInternalTimesheets: false,
91
+ hasInternalApprovalAssignments: false,
92
+ canManageTimesheets: false,
93
+ canReviewClientTimesheets: false,
94
+ canViewSupplierTime: false,
95
+ canAccessApprovals: false,
96
+ canManageClientReviewers: false,
97
+ pendingInternalApprovalCount: 0,
98
+ pendingClientApprovalCount: 0,
99
+ unassignedClientReviewerSupplierCount: 0
100
+ }
101
+ register(empty)
102
+ const refreshCapabilities = async () => {
103
+ try {
104
+ register(await $fetch('/api/timesheets/capabilities'))
105
+ } catch {
106
+ register(empty)
107
+ }
108
+ }
109
+ if (import.meta.client) {
110
+ window.addEventListener('timesheets:capabilities-refresh', refreshCapabilities)
111
+ }
112
+ watch(
113
+ activeOrganizationId,
114
+ async (organizationId) => {
115
+ if (!organizationId) {
116
+ return register(empty)
117
+ }
118
+ await refreshCapabilities()
119
+ },
120
+ { immediate: true }
121
+ )
122
+ })