@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,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1786258239902,
9
+ "tag": "0000_baseline",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
package/nuxt.config.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { fileURLToPath } from 'node:url'
2
+
3
+ export default defineNuxtConfig({
4
+ $meta: { name: 'nuxt-customer-portal-timesheets' },
5
+ compatibilityDate: '2025-10-24',
6
+ modules: ['@nuxtjs/i18n'],
7
+ css: [fileURLToPath(new URL('./app/assets/timesheets.css', import.meta.url))],
8
+ components: [
9
+ {
10
+ path: fileURLToPath(new URL('./app/components', import.meta.url)),
11
+ global: true
12
+ }
13
+ ],
14
+ i18n: {
15
+ locales: [
16
+ { code: 'en', iso: 'en-US', name: 'English', file: 'en.json' },
17
+ { code: 'nl', iso: 'nl-NL', name: 'Nederlands', file: 'nl.json' }
18
+ ]
19
+ }
20
+ })
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@nuxt-customer-portal/timesheets",
3
+ "version": "0.3.0",
4
+ "license": "MIT",
5
+ "homepage": "https://nuxt-customer-portal.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ludulicious/nuxt-customer-portal.git"
9
+ },
10
+ "type": "module",
11
+ "main": "./nuxt.config.ts",
12
+ "files": [
13
+ "LICENSE",
14
+ "CHANGELOG.md",
15
+ "app",
16
+ "i18n",
17
+ "migrations",
18
+ "server",
19
+ "shared",
20
+ "tokens.css",
21
+ "nuxt.config.ts",
22
+ "portal.manifest.mjs",
23
+ "README.md"
24
+ ],
25
+ "exports": {
26
+ ".": "./nuxt.config.ts",
27
+ "./feature": "./shared/feature.ts",
28
+ "./types": "./shared/types/timesheet.ts",
29
+ "./schema": "./server/db/schema/timesheets.ts",
30
+ "./portal-manifest": "./portal.manifest.mjs",
31
+ "./app/*": "./app/*.ts",
32
+ "./server/*": "./server/*.ts",
33
+ "./shared/*": "./shared/*.ts"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@nuxtjs/i18n": "^10.6.0",
40
+ "@types/pg": "8.15.5",
41
+ "date-fns": "^4.4.0",
42
+ "drizzle-orm": "^0.45.2",
43
+ "nanoid": "^5.1.6",
44
+ "pdf-lib": "^1.17.1",
45
+ "pg": "^8.22.0",
46
+ "resend": "^6.18.1",
47
+ "zod": "^4.4.3",
48
+ "@nuxt-customer-portal/core": "^0.3.0",
49
+ "@nuxt-customer-portal/ui": "^0.3.0",
50
+ "@nuxt-customer-portal/clients": "^0.3.0",
51
+ "@nuxt-customer-portal/organizations": "^0.3.0"
52
+ },
53
+ "peerDependencies": {
54
+ "nuxt": "^4.5.1",
55
+ "vue": "^3.5.0"
56
+ },
57
+ "devDependencies": {
58
+ "nuxt": "^4.5.1",
59
+ "vue": "^3.5.0"
60
+ }
61
+ }
@@ -0,0 +1,10 @@
1
+ export default {
2
+ id: 'timesheets',
3
+ version: '0.3.0',
4
+ source: '@nuxt-customer-portal/timesheets',
5
+ clientModuleId: 'timesheets',
6
+ dependsOn: ['core', 'ui', 'organizations', 'clients'],
7
+ schema: './server/db/schema/timesheets.ts',
8
+ migrations: './migrations',
9
+ migrationSearchPath: ['timesheets', 'public']
10
+ }
@@ -0,0 +1,18 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { getActivityDeletionEligibility } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Timesheets'],
8
+ operationId: 'timesheetsAdminActivitiesByIdDeletionGet',
9
+ summary: 'Check activity deletion eligibility',
10
+ description:
11
+ 'Check activity deletion eligibility. Scoped to the active organization and the applicable Timesheets permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
17
+ return getActivityDeletionEligibility(organizationId, getRouterParam(event, 'id')!)
18
+ })
@@ -0,0 +1,20 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { deleteActivity } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { activityDeleteSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminActivitiesByIdDelete',
10
+ summary: 'Delete a timesheet activity',
11
+ description:
12
+ 'Delete a timesheet activity. Scoped to the active organization and the applicable Timesheets permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
18
+ const input = activityDeleteSchema.parse(await readBody(event))
19
+ return deleteActivity(organizationId, getRouterParam(event, 'id')!, input.activityName)
20
+ })
@@ -0,0 +1,19 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { updateActivity } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { activityUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminActivitiesByIdPatch',
10
+ summary: 'Update a timesheet activity',
11
+ description:
12
+ 'Update a timesheet activity. Scoped to the active organization and the applicable Timesheets permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
18
+ return updateActivity(organizationId, getRouterParam(event, 'id')!, activityUpdateSchema.parse(await readBody(event)))
19
+ })
@@ -0,0 +1,19 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { activityListQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
4
+ import { listActivitiesPage } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-admin-listing'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminActivitiesGet',
10
+ summary: 'List timesheet activities',
11
+ description:
12
+ 'List timesheet activities. Scoped to the active organization and the applicable Timesheets permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
18
+ return listActivitiesPage(organizationId, activityListQuerySchema.parse(getQuery(event)))
19
+ })
@@ -0,0 +1,19 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { createActivity } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { activityCreateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminActivitiesPost',
10
+ summary: 'Create a timesheet activity',
11
+ description:
12
+ 'Create a timesheet activity. Scoped to the active organization and the applicable Timesheets permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
18
+ return createActivity(organizationId, activityCreateSchema.parse(await readBody(event)))
19
+ })
@@ -0,0 +1,19 @@
1
+ import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
2
+ import { reviewSubmission } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
3
+ import { reviewSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Timesheets'],
8
+ operationId: 'timesheetsAdminApprovalsByIdPost',
9
+ summary: 'Review a submitted timesheet',
10
+ description:
11
+ 'Review a submitted timesheet. Scoped to the active organization and the applicable Timesheets permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { session, organizationId } = await requireActiveOrganizationRole(event)
17
+ const input = reviewSchema.parse(await readBody(event))
18
+ return reviewSubmission(organizationId, session.user.id, getRouterParam(event, 'id')!, input.action, input.comment)
19
+ })
@@ -0,0 +1,73 @@
1
+ import { db, requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { organization } from '@nuxt-customer-portal/core/schema'
3
+ import { eq } from 'drizzle-orm'
4
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
5
+ import {
6
+ ensureSettings,
7
+ listActivities,
8
+ listAvailableClientOrganizations,
9
+ listClients,
10
+ getTimesheetsSetupStatus,
11
+ listProjects,
12
+ listTeam
13
+ } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
14
+
15
+ defineRouteMeta({
16
+ openAPI: {
17
+ tags: ['Timesheets'],
18
+ operationId: 'timesheetsAdminBootstrapGet',
19
+ summary: 'Get timesheets administration data',
20
+ description:
21
+ 'Get timesheets administration data. Scoped to the active organization and the applicable Timesheets permission.'
22
+ }
23
+ })
24
+
25
+ export default defineEventHandler(async (event) => {
26
+ const { organizationId, session } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
27
+ const section = String(getQuery(event).section ?? '')
28
+ const settings = await ensureSettings(organizationId)
29
+ const [
30
+ providerOrganizations,
31
+ clients,
32
+ availableClientOrganizations,
33
+ projects,
34
+ activities,
35
+ team,
36
+ approvals,
37
+ setupStatus
38
+ ] = await Promise.all([
39
+ db
40
+ .select({ organizationId: organization.id, name: organization.name })
41
+ .from(organization)
42
+ .where(eq(organization.id, organizationId))
43
+ .limit(1),
44
+ section === 'clients' ? Promise.resolve([]) : listClients(organizationId),
45
+ listAvailableClientOrganizations(organizationId, session.user.id, session.user.role === 'admin'),
46
+ section === 'projects' ? Promise.resolve([]) : listProjects(organizationId),
47
+ section === 'activities' ? Promise.resolve([]) : listActivities(organizationId),
48
+ listTeam(organizationId),
49
+ Promise.resolve([]),
50
+ getTimesheetsSetupStatus(organizationId)
51
+ ])
52
+ const providerOrganization = providerOrganizations[0]
53
+ if (!providerOrganization) {
54
+ throw createError({ statusCode: 404, message: 'Provider organization not found' })
55
+ }
56
+ return {
57
+ providerOrganization,
58
+ settings: {
59
+ currency: settings.currency,
60
+ timezone: settings.timezone,
61
+ timerRoundingMinutes: settings.timerRoundingMinutes,
62
+ weekStartsOn: settings.weekStartsOn,
63
+ internalApprovalsEnabled: settings.internalApprovalsEnabled
64
+ },
65
+ clients,
66
+ availableClientOrganizations,
67
+ projects,
68
+ activities,
69
+ team,
70
+ approvals,
71
+ setupStatus
72
+ }
73
+ })
@@ -0,0 +1,25 @@
1
+ import { and, eq } from 'drizzle-orm'
2
+ import { requireFeatureAccess, db } from '@nuxt-customer-portal/core/server/portal'
3
+ import { requireClientModuleEnabled } from '@nuxt-customer-portal/clients/server/utils/client-repository'
4
+ import { workspaceClient } from '@nuxt-customer-portal/timesheets/server/db/schema/timesheets'
5
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
6
+
7
+ export default defineEventHandler(async (event) => {
8
+ const { organizationId, organizationType } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
9
+ if (organizationType !== 'PROVIDER') {
10
+ throw createError({ statusCode: 403, message: 'PROVIDER organization required' })
11
+ }
12
+ const clientOrganizationId = getRouterParam(event, 'clientId')!
13
+ await requireClientModuleEnabled(clientOrganizationId, 'timesheets')
14
+ const [settings] = await db
15
+ .select()
16
+ .from(workspaceClient)
17
+ .where(
18
+ and(
19
+ eq(workspaceClient.workspaceOrganizationId, organizationId),
20
+ eq(workspaceClient.clientOrganizationId, clientOrganizationId)
21
+ )
22
+ )
23
+ .limit(1)
24
+ return settings ?? { clientOrganizationId, accessMode: 'DISABLED' }
25
+ })
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod'
2
+ import { and, eq } from 'drizzle-orm'
3
+ import { requireFeatureAccess, db } from '@nuxt-customer-portal/core/server/portal'
4
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
5
+ import { workspaceClient } from '@nuxt-customer-portal/timesheets/server/db/schema/timesheets'
6
+ import { ensureTimesheetClientSettings } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
7
+
8
+ const schema = z.object({ accessMode: z.enum(['DISABLED', 'VIEW', 'REVIEW']) })
9
+
10
+ export default defineEventHandler(async (event) => {
11
+ const { organizationId, organizationType } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
12
+ if (organizationType !== 'PROVIDER') {
13
+ throw createError({ statusCode: 403, message: 'PROVIDER organization required' })
14
+ }
15
+ const clientOrganizationId = getRouterParam(event, 'clientId')!
16
+ await ensureTimesheetClientSettings(organizationId, clientOrganizationId)
17
+ const [updated] = await db
18
+ .update(workspaceClient)
19
+ .set({ ...schema.parse(await readBody(event)), updatedAt: new Date() })
20
+ .where(
21
+ and(
22
+ eq(workspaceClient.workspaceOrganizationId, organizationId),
23
+ eq(workspaceClient.clientOrganizationId, clientOrganizationId)
24
+ )
25
+ )
26
+ .returning()
27
+ return updated
28
+ })
@@ -0,0 +1,18 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { getClientDeletionEligibility } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Timesheets'],
8
+ operationId: 'timesheetsAdminClientsByIdDeletionGet',
9
+ summary: 'Check client deletion eligibility',
10
+ description:
11
+ 'Check client deletion eligibility. Scoped to the active organization and the applicable Timesheets permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
17
+ return getClientDeletionEligibility(organizationId, getRouterParam(event, 'id')!)
18
+ })
@@ -0,0 +1,20 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { deleteClient } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { clientDeleteSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminClientsByIdDelete',
10
+ summary: 'Remove a timesheet client',
11
+ description:
12
+ 'Remove a timesheet client. Scoped to the active organization and the applicable Timesheets permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
18
+ const input = clientDeleteSchema.parse(await readBody(event))
19
+ return deleteClient(organizationId, getRouterParam(event, 'id')!, input.clientName)
20
+ })
@@ -0,0 +1,14 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { updateClientAccess } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { clientAccessUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
8
+ const body = await readBody(event)
9
+ return updateClientAccess(
10
+ organizationId,
11
+ getRouterParam(event, 'id')!,
12
+ clientAccessUpdateSchema.parse(body).accessMode
13
+ )
14
+ })
@@ -0,0 +1,18 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { clientListQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
4
+ import { listClientsPage } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-admin-listing'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminClientsGet',
10
+ summary: 'List timesheet clients',
11
+ description: 'List timesheet clients. Scoped to the active organization and the applicable Timesheets permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
17
+ return listClientsPage(organizationId, clientListQuerySchema.parse(getQuery(event)))
18
+ })
@@ -0,0 +1,25 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { linkClient } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { clientCreateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminClientsPost',
10
+ summary: 'Link a timesheet client',
11
+ description: 'Link a timesheet client. Scoped to the active organization and the applicable Timesheets permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId, session } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
17
+ const input = clientCreateSchema.parse(await readBody(event))
18
+ if (input.mode === 'create') {
19
+ throw createError({
20
+ statusCode: 410,
21
+ message: 'Create clients in the Clients module before linking them to Timesheets'
22
+ })
23
+ }
24
+ return linkClient(organizationId, session.user.id, session.user.role === 'admin', input.organizationId)
25
+ })
@@ -0,0 +1,11 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { updateInternalApprovalMember } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { internalApprovalMemberUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ const { organizationId, session } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
8
+ const input = internalApprovalMemberUpdateSchema.parse(await readBody(event))
9
+ await updateInternalApprovalMember(organizationId, session.user.id, getRouterParam(event, 'userId')!, input)
10
+ return { updated: true }
11
+ })
@@ -0,0 +1,8 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { getInternalApprovalConfiguration } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
7
+ return getInternalApprovalConfiguration(organizationId)
8
+ })
@@ -0,0 +1,11 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { updateInternalApprovalWorkspace } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { internalApprovalWorkspaceUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
8
+ const input = internalApprovalWorkspaceUpdateSchema.parse(await readBody(event))
9
+ await updateInternalApprovalWorkspace(organizationId, input.enabled)
10
+ return { updated: true }
11
+ })
@@ -0,0 +1,19 @@
1
+ import { getPortalOrganization, requireSession } from '@nuxt-customer-portal/core/server/portal'
2
+ import { getOrganizationTimesheetCapabilities } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const session = await requireSession(event)
6
+ if (session.user.role !== 'admin') {
7
+ throw createError({ statusCode: 403, message: 'System administrator access required' })
8
+ }
9
+ const organizationId = getRouterParam(event, 'organizationId')!
10
+ const organization = await getPortalOrganization(organizationId)
11
+ if (!organization) {
12
+ throw createError({ statusCode: 404, message: 'Organization not found' })
13
+ }
14
+ const capabilities = await getOrganizationTimesheetCapabilities(organizationId)
15
+ return {
16
+ ...capabilities,
17
+ workspaceEnabled: organization.organizationType === 'PROVIDER' && capabilities.workspaceEnabled
18
+ }
19
+ })
@@ -0,0 +1,23 @@
1
+ import { getPortalOrganization, requireSession } from '@nuxt-customer-portal/core/server/portal'
2
+ import { updateOrganizationTimesheetCapabilities } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
3
+ import { organizationCapabilitiesUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ const session = await requireSession(event)
7
+ if (session.user.role !== 'admin') {
8
+ throw createError({ statusCode: 403, message: 'System administrator access required' })
9
+ }
10
+ const organizationId = getRouterParam(event, 'organizationId')!
11
+ const input = organizationCapabilitiesUpdateSchema.parse(await readBody(event))
12
+ const organization = await getPortalOrganization(organizationId)
13
+ if (!organization) {
14
+ throw createError({ statusCode: 404, message: 'Organization not found' })
15
+ }
16
+ if (organization.organizationType !== 'PROVIDER' && input.workspaceEnabled) {
17
+ throw createError({
18
+ statusCode: 400,
19
+ message: 'Timesheets workspaces can only be enabled for provider organizations'
20
+ })
21
+ }
22
+ return updateOrganizationTimesheetCapabilities(organizationId, input)
23
+ })
@@ -0,0 +1,18 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { getProjectDeletionEligibility } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Timesheets'],
8
+ operationId: 'timesheetsAdminProjectsByIdDeletionGet',
9
+ summary: 'Check project deletion eligibility',
10
+ description:
11
+ 'Check project deletion eligibility. Scoped to the active organization and the applicable Timesheets permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
17
+ return getProjectDeletionEligibility(organizationId, getRouterParam(event, 'id')!)
18
+ })
@@ -0,0 +1,20 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { deleteProject } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { projectDeleteSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminProjectsByIdDelete',
10
+ summary: 'Delete a timesheet project',
11
+ description:
12
+ 'Delete a timesheet project. Scoped to the active organization and the applicable Timesheets permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
18
+ const input = projectDeleteSchema.parse(await readBody(event))
19
+ return deleteProject(organizationId, getRouterParam(event, 'id')!, input.projectName)
20
+ })
@@ -0,0 +1,22 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { getProject } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+
5
+ defineRouteMeta({
6
+ openAPI: {
7
+ tags: ['Timesheets'],
8
+ operationId: 'timesheetsAdminProjectsByIdGet',
9
+ summary: 'Get a timesheet project',
10
+ description:
11
+ 'Get one timesheet project. Scoped to the active organization and the applicable Timesheets permission.'
12
+ }
13
+ })
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
17
+ const selected = await getProject(organizationId, getRouterParam(event, 'id')!)
18
+ if (!selected) {
19
+ throw createError({ statusCode: 404, message: 'Project not found' })
20
+ }
21
+ return selected
22
+ })
@@ -0,0 +1,19 @@
1
+ import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
2
+ import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
3
+ import { updateProject } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
4
+ import { projectUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
5
+
6
+ defineRouteMeta({
7
+ openAPI: {
8
+ tags: ['Timesheets'],
9
+ operationId: 'timesheetsAdminProjectsByIdPatch',
10
+ summary: 'Update a timesheet project',
11
+ description:
12
+ 'Update a timesheet project. Scoped to the active organization and the applicable Timesheets permission.'
13
+ }
14
+ })
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
18
+ return updateProject(organizationId, getRouterParam(event, 'id')!, projectUpdateSchema.parse(await readBody(event)))
19
+ })