@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,18 @@
|
|
|
1
|
+
import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
|
|
3
|
+
import { projectListQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
4
|
+
import { listProjectsPage } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-admin-listing'
|
|
5
|
+
|
|
6
|
+
defineRouteMeta({
|
|
7
|
+
openAPI: {
|
|
8
|
+
tags: ['Timesheets'],
|
|
9
|
+
operationId: 'timesheetsAdminProjectsGet',
|
|
10
|
+
summary: 'List timesheet projects',
|
|
11
|
+
description: 'List timesheet projects. 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 listProjectsPage(organizationId, projectListQuerySchema.parse(getQuery(event)))
|
|
18
|
+
})
|
|
@@ -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 { createProject } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
4
|
+
import { projectCreateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
5
|
+
|
|
6
|
+
defineRouteMeta({
|
|
7
|
+
openAPI: {
|
|
8
|
+
tags: ['Timesheets'],
|
|
9
|
+
operationId: 'timesheetsAdminProjectsPost',
|
|
10
|
+
summary: 'Create a timesheet project',
|
|
11
|
+
description:
|
|
12
|
+
'Create 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 createProject(organizationId, projectCreateSchema.parse(await readBody(event)))
|
|
19
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
|
|
3
|
+
import { getReport, reportToCsv } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
4
|
+
import { reportQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
5
|
+
|
|
6
|
+
defineRouteMeta({
|
|
7
|
+
openAPI: {
|
|
8
|
+
tags: ['Timesheets'],
|
|
9
|
+
operationId: 'timesheetsAdminReportGet',
|
|
10
|
+
summary: 'Get a timesheets report',
|
|
11
|
+
description: 'Get a timesheets report. 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, 'report')
|
|
17
|
+
const rawQuery = getQuery(event)
|
|
18
|
+
const format = rawQuery.format
|
|
19
|
+
const report = await getReport(organizationId, reportQuerySchema.parse(rawQuery))
|
|
20
|
+
if (format === 'csv') {
|
|
21
|
+
setResponseHeader(event, 'content-type', 'text/csv; charset=utf-8')
|
|
22
|
+
setResponseHeader(event, 'content-disposition', 'attachment; filename="timesheets.csv"')
|
|
23
|
+
return reportToCsv(report)
|
|
24
|
+
}
|
|
25
|
+
return report
|
|
26
|
+
})
|
|
@@ -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 { updateSettings } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
4
|
+
import { settingsUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
5
|
+
|
|
6
|
+
defineRouteMeta({
|
|
7
|
+
openAPI: {
|
|
8
|
+
tags: ['Timesheets'],
|
|
9
|
+
operationId: 'timesheetsAdminSettingsPatch',
|
|
10
|
+
summary: 'Update timesheets workspace settings',
|
|
11
|
+
description:
|
|
12
|
+
'Update timesheets workspace settings. 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 updateSettings(organizationId, settingsUpdateSchema.parse(await readBody(event)))
|
|
19
|
+
})
|
|
@@ -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 { setTeamTariff } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
4
|
+
import { tariffUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
5
|
+
|
|
6
|
+
defineRouteMeta({
|
|
7
|
+
openAPI: {
|
|
8
|
+
tags: ['Timesheets'],
|
|
9
|
+
operationId: 'timesheetsAdminTariffsPut',
|
|
10
|
+
summary: 'Update team billing rates',
|
|
11
|
+
description:
|
|
12
|
+
'Update team billing rates. 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 = tariffUpdateSchema.parse(await readBody(event))
|
|
19
|
+
return setTeamTariff(organizationId, input.userId, input.hourlyRateMinor)
|
|
20
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
|
|
3
|
+
import { updateTeamMemberSettings } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
4
|
+
import { teamMemberSettingsUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
5
|
+
|
|
6
|
+
defineRouteMeta({
|
|
7
|
+
openAPI: {
|
|
8
|
+
tags: ['Timesheets'],
|
|
9
|
+
operationId: 'timesheetsAdminTeamMemberPut',
|
|
10
|
+
summary: 'Update a team member’s Timesheets settings',
|
|
11
|
+
description: 'Update time-entry eligibility and the default hourly rate for an organization member.'
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export default defineEventHandler(async (event) => {
|
|
16
|
+
const { organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'manage')
|
|
17
|
+
const userId = getRouterParam(event, 'userId')
|
|
18
|
+
if (!userId) {
|
|
19
|
+
throw createError({ statusCode: 400, message: 'Team member is required' })
|
|
20
|
+
}
|
|
21
|
+
const input = teamMemberSettingsUpdateSchema.parse(await readBody(event))
|
|
22
|
+
await updateTeamMemberSettings(organizationId, userId, input)
|
|
23
|
+
return { updated: true }
|
|
24
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
|
|
3
|
+
import {
|
|
4
|
+
getBootstrap,
|
|
5
|
+
requireTimesheetWorkspace
|
|
6
|
+
} from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
7
|
+
import { weekQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
8
|
+
|
|
9
|
+
defineRouteMeta({
|
|
10
|
+
openAPI: {
|
|
11
|
+
tags: ['Timesheets'],
|
|
12
|
+
operationId: 'timesheetsBootstrapGet',
|
|
13
|
+
summary: 'Get timesheets workspace data',
|
|
14
|
+
description:
|
|
15
|
+
'Get timesheets workspace data. Scoped to the active organization and the applicable Timesheets permission.'
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default defineEventHandler(async (event) => {
|
|
20
|
+
const { session, organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'read')
|
|
21
|
+
const query = weekQuerySchema.parse(getQuery(event))
|
|
22
|
+
await requireTimesheetWorkspace(organizationId)
|
|
23
|
+
return getBootstrap(organizationId, session.user.id, query.week)
|
|
24
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { db, requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
3
|
+
import { workspaceSettings } from '@nuxt-customer-portal/timesheets/server/db/schema/timesheets'
|
|
4
|
+
import {
|
|
5
|
+
canMemberEnterTime,
|
|
6
|
+
hasInternalApprovalAssignment,
|
|
7
|
+
listApprovalQueue,
|
|
8
|
+
listClientApprovals,
|
|
9
|
+
listClientReviewerSuppliers,
|
|
10
|
+
listClientWorkspaces
|
|
11
|
+
} from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
12
|
+
import type { TimesheetCapabilitiesDto } from '@nuxt-customer-portal/timesheets/shared/types/timesheet'
|
|
13
|
+
|
|
14
|
+
export default defineEventHandler(async (event) => {
|
|
15
|
+
const { session, organizationId, organizationType, role } = await requireActiveOrganizationRole(event)
|
|
16
|
+
const isAdmin = role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
17
|
+
const [settings, clientWorkspaces, approvals, reviewerSuppliers, memberCanEnterTime] = await Promise.all([
|
|
18
|
+
db
|
|
19
|
+
.select({
|
|
20
|
+
workspaceEnabled: workspaceSettings.workspaceEnabled,
|
|
21
|
+
internalApprovalsEnabled: workspaceSettings.internalApprovalsEnabled
|
|
22
|
+
})
|
|
23
|
+
.from(workspaceSettings)
|
|
24
|
+
.where(eq(workspaceSettings.organizationId, organizationId))
|
|
25
|
+
.limit(1),
|
|
26
|
+
listClientWorkspaces(organizationId, session.user.id, isAdmin),
|
|
27
|
+
listClientApprovals(organizationId, session.user.id, isAdmin),
|
|
28
|
+
isAdmin ? listClientReviewerSuppliers(organizationId, session.user.id) : Promise.resolve([]),
|
|
29
|
+
canMemberEnterTime(organizationId, session.user.id)
|
|
30
|
+
])
|
|
31
|
+
const reviewWorkspaces = clientWorkspaces.filter((item) => item.accessMode === 'REVIEW')
|
|
32
|
+
const workspaceEnabled = organizationType === 'PROVIDER' && (settings[0]?.workspaceEnabled ?? false)
|
|
33
|
+
const internalApprovals =
|
|
34
|
+
workspaceEnabled && (settings[0]?.internalApprovalsEnabled ?? true)
|
|
35
|
+
? await listApprovalQueue(organizationId, session.user.id)
|
|
36
|
+
: []
|
|
37
|
+
const hasInternalApprovalAssignments =
|
|
38
|
+
workspaceEnabled &&
|
|
39
|
+
(settings[0]?.internalApprovalsEnabled ?? true) &&
|
|
40
|
+
(await hasInternalApprovalAssignment(organizationId, session.user.id))
|
|
41
|
+
return {
|
|
42
|
+
canEnterTime: workspaceEnabled && memberCanEnterTime,
|
|
43
|
+
canApproveInternalTimesheets: hasInternalApprovalAssignments,
|
|
44
|
+
hasInternalApprovalAssignments,
|
|
45
|
+
canManageTimesheets: organizationType === 'PROVIDER' && isAdmin && workspaceEnabled,
|
|
46
|
+
canReviewClientTimesheets:
|
|
47
|
+
reviewWorkspaces.length > 0 &&
|
|
48
|
+
(isAdmin || reviewWorkspaces.some((item) => item.canReview) || approvals.items.length > 0),
|
|
49
|
+
canViewSupplierTime: organizationType === 'CLIENT' && clientWorkspaces.some((item) => item.accessMode === 'VIEW'),
|
|
50
|
+
canAccessApprovals:
|
|
51
|
+
reviewWorkspaces.length > 0 &&
|
|
52
|
+
(isAdmin || reviewWorkspaces.some((item) => item.canReview) || approvals.items.length > 0),
|
|
53
|
+
canManageClientReviewers: isAdmin && reviewWorkspaces.length > 0,
|
|
54
|
+
pendingInternalApprovalCount: internalApprovals.filter((item) => item.status === 'SUBMITTED').length,
|
|
55
|
+
pendingClientApprovalCount: approvals.pendingCount,
|
|
56
|
+
unassignedClientReviewerSupplierCount: reviewerSuppliers.filter((item) => item.reviewerCount === 0).length
|
|
57
|
+
} satisfies TimesheetCapabilitiesDto
|
|
58
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { getClientTimesheets } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
6
|
+
return getClientTimesheets(
|
|
7
|
+
getRouterParam(event, 'workspaceClientId')!,
|
|
8
|
+
organizationId,
|
|
9
|
+
session.user.id,
|
|
10
|
+
role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
11
|
+
)
|
|
12
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { listClientReviewers } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
6
|
+
if (role !== 'owner' && role !== 'admin' && session.user.role !== 'admin') {
|
|
7
|
+
throw createError({ statusCode: 403, message: 'Client organization admin access required' })
|
|
8
|
+
}
|
|
9
|
+
return listClientReviewers(getRouterParam(event, 'workspaceClientId')!, organizationId)
|
|
10
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { setClientReviewer } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
import { clientReviewerUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
7
|
+
if (role !== 'owner' && role !== 'admin' && session.user.role !== 'admin') {
|
|
8
|
+
throw createError({ statusCode: 403, message: 'Client organization admin access required' })
|
|
9
|
+
}
|
|
10
|
+
const input = clientReviewerUpdateSchema.parse(await readBody(event))
|
|
11
|
+
return setClientReviewer(
|
|
12
|
+
getRouterParam(event, 'workspaceClientId')!,
|
|
13
|
+
organizationId,
|
|
14
|
+
session.user.id,
|
|
15
|
+
input.userId,
|
|
16
|
+
input.assigned
|
|
17
|
+
)
|
|
18
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { reviewClientTimesheet } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
import { clientReviewSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
7
|
+
const input = clientReviewSchema.parse(await readBody(event))
|
|
8
|
+
return reviewClientTimesheet(
|
|
9
|
+
getRouterParam(event, 'workspaceClientId')!,
|
|
10
|
+
organizationId,
|
|
11
|
+
session.user.id,
|
|
12
|
+
role === 'owner' || role === 'admin' || session.user.role === 'admin',
|
|
13
|
+
getRouterParam(event, 'submissionId')!,
|
|
14
|
+
input.action,
|
|
15
|
+
input.expectedVersion,
|
|
16
|
+
input.comment
|
|
17
|
+
)
|
|
18
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { listClientApprovals } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
6
|
+
const isAdmin = role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
7
|
+
const approvals = await listClientApprovals(organizationId, session.user.id, isAdmin)
|
|
8
|
+
return [
|
|
9
|
+
...new Map(approvals.items.map((item) => [item.userId, { id: item.userId, name: item.person }])).values()
|
|
10
|
+
].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id))
|
|
11
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { listClientApprovalSuppliers } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
6
|
+
const isAdmin = role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
7
|
+
return listClientApprovalSuppliers(organizationId, session.user.id, isAdmin)
|
|
8
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { clientApprovalListQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
3
|
+
import { listClientApprovalsPage } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-admin-listing'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
7
|
+
const isAdmin = role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
8
|
+
return listClientApprovalsPage(
|
|
9
|
+
organizationId,
|
|
10
|
+
session.user.id,
|
|
11
|
+
isAdmin,
|
|
12
|
+
clientApprovalListQuerySchema.parse(getQuery(event))
|
|
13
|
+
)
|
|
14
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { listClientReviewerSuppliers } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
6
|
+
if (role !== 'owner' && role !== 'admin' && session.user.role !== 'admin') {
|
|
7
|
+
throw createError({ statusCode: 403, message: 'Client organization admin access required' })
|
|
8
|
+
}
|
|
9
|
+
return listClientReviewerSuppliers(organizationId, session.user.id)
|
|
10
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { listClientSupplierOptions } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const { session, organizationId, organizationType, role } = await requireActiveOrganizationRole(event)
|
|
6
|
+
if (organizationType !== 'CLIENT') {
|
|
7
|
+
throw createError({ statusCode: 403, message: 'Supplier timesheets are only available to client organizations' })
|
|
8
|
+
}
|
|
9
|
+
return listClientSupplierOptions(
|
|
10
|
+
organizationId,
|
|
11
|
+
session.user.id,
|
|
12
|
+
role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
13
|
+
)
|
|
14
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { clientSupplierTimesheetListQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
3
|
+
import { listClientSupplierTimesheetsPage } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-admin-listing'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const { session, organizationId, organizationType, role } = await requireActiveOrganizationRole(event)
|
|
7
|
+
if (organizationType !== 'CLIENT') {
|
|
8
|
+
throw createError({ statusCode: 403, message: 'Supplier timesheets are only available to client organizations' })
|
|
9
|
+
}
|
|
10
|
+
const isAdmin = role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
11
|
+
return listClientSupplierTimesheetsPage(
|
|
12
|
+
organizationId,
|
|
13
|
+
session.user.id,
|
|
14
|
+
isAdmin,
|
|
15
|
+
clientSupplierTimesheetListQuerySchema.parse(getQuery(event))
|
|
16
|
+
)
|
|
17
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { listClientWorkspaces } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
6
|
+
return listClientWorkspaces(
|
|
7
|
+
organizationId,
|
|
8
|
+
session.user.id,
|
|
9
|
+
role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
10
|
+
)
|
|
11
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
3
|
+
import { internalApprovalListQuerySchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
4
|
+
import { listProviderClientApprovalsPage } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-admin-listing'
|
|
5
|
+
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
const { session, organizationId, role } = await requireActiveOrganizationRole(event)
|
|
8
|
+
if (!['owner', 'admin'].includes(role)) {
|
|
9
|
+
throw createError({ statusCode: 403, message: 'Workspace administrator access required' })
|
|
10
|
+
}
|
|
11
|
+
const query = internalApprovalListQuerySchema
|
|
12
|
+
.extend({
|
|
13
|
+
clientOrganizationId: z.string().min(1).optional(),
|
|
14
|
+
status: z.enum(['PENDING', 'APPROVED', 'DISPUTED', 'AUTO_APPROVED']).optional()
|
|
15
|
+
})
|
|
16
|
+
.parse(getQuery(event))
|
|
17
|
+
return listProviderClientApprovalsPage(organizationId, session.user.id, query)
|
|
18
|
+
})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { selectClientDashboardApprovals } from '@nuxt-customer-portal/timesheets/shared/client-approval-dashboard'
|
|
2
|
+
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
|
|
3
|
+
import { db, requireActiveOrganizationRole } from '@nuxt-customer-portal/core/server/portal'
|
|
4
|
+
import {
|
|
5
|
+
workspaceSettings,
|
|
6
|
+
timesheetSubmission,
|
|
7
|
+
weeklyTimesheet,
|
|
8
|
+
timeEntry
|
|
9
|
+
} from '@nuxt-customer-portal/timesheets/server/db/schema/timesheets'
|
|
10
|
+
import {
|
|
11
|
+
canMemberEnterTime,
|
|
12
|
+
getBootstrap,
|
|
13
|
+
hasInternalApprovalAssignment,
|
|
14
|
+
listApprovalQueue,
|
|
15
|
+
listClientApprovals,
|
|
16
|
+
listClientReviewerSuppliers,
|
|
17
|
+
listClientSupplierTimesheets,
|
|
18
|
+
listClientWorkspaces
|
|
19
|
+
} from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
20
|
+
import type { TimesheetsDashboardDto } from '@nuxt-customer-portal/timesheets/shared/types/timesheet'
|
|
21
|
+
|
|
22
|
+
export default defineEventHandler(async (event): Promise<TimesheetsDashboardDto> => {
|
|
23
|
+
const { session, organizationId, organizationType, role } = await requireActiveOrganizationRole(event)
|
|
24
|
+
const isAdmin = role === 'owner' || role === 'admin' || session.user.role === 'admin'
|
|
25
|
+
const [settingsRow, clientWorkspaces, memberCanEnterTime] = await Promise.all([
|
|
26
|
+
db
|
|
27
|
+
.select({
|
|
28
|
+
workspaceEnabled: workspaceSettings.workspaceEnabled,
|
|
29
|
+
internalApprovalsEnabled: workspaceSettings.internalApprovalsEnabled,
|
|
30
|
+
currency: workspaceSettings.currency
|
|
31
|
+
})
|
|
32
|
+
.from(workspaceSettings)
|
|
33
|
+
.where(eq(workspaceSettings.organizationId, organizationId))
|
|
34
|
+
.limit(1),
|
|
35
|
+
listClientWorkspaces(organizationId, session.user.id, isAdmin),
|
|
36
|
+
canMemberEnterTime(organizationId, session.user.id)
|
|
37
|
+
])
|
|
38
|
+
const settings = settingsRow[0]
|
|
39
|
+
const workspaceEnabled = settings?.workspaceEnabled ?? false
|
|
40
|
+
const canEnterTime = workspaceEnabled && memberCanEnterTime
|
|
41
|
+
const reviewWorkspaces = clientWorkspaces.filter((item) => item.accessMode === 'REVIEW')
|
|
42
|
+
const canViewSupplierTime =
|
|
43
|
+
organizationType === 'CLIENT' && clientWorkspaces.some((item) => item.accessMode === 'VIEW')
|
|
44
|
+
const hasInternalApprovals =
|
|
45
|
+
workspaceEnabled &&
|
|
46
|
+
(settings?.internalApprovalsEnabled ?? true) &&
|
|
47
|
+
(await hasInternalApprovalAssignment(organizationId, session.user.id))
|
|
48
|
+
|
|
49
|
+
const [bootstrap, approvalQueue, clientApprovals, reviewerSuppliers, supplierTimesheets] = await Promise.all([
|
|
50
|
+
canEnterTime ? getBootstrap(organizationId, session.user.id) : Promise.resolve(null),
|
|
51
|
+
hasInternalApprovals ? listApprovalQueue(organizationId, session.user.id) : Promise.resolve([]),
|
|
52
|
+
reviewWorkspaces.length ? listClientApprovals(organizationId, session.user.id, isAdmin) : Promise.resolve(null),
|
|
53
|
+
isAdmin && reviewWorkspaces.length
|
|
54
|
+
? listClientReviewerSuppliers(organizationId, session.user.id)
|
|
55
|
+
: Promise.resolve([]),
|
|
56
|
+
canViewSupplierTime ? listClientSupplierTimesheets(organizationId, session.user.id, isAdmin) : Promise.resolve([])
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
const pendingInternal = approvalQueue.filter((item) => item.status === 'SUBMITTED')
|
|
60
|
+
const clientDashboard = selectClientDashboardApprovals(clientApprovals?.items ?? [], session.user.id)
|
|
61
|
+
const unsubmittedEntries = bootstrap?.week.entries.filter((entry) => !entry.submissionId) ?? []
|
|
62
|
+
const unsubmittedDates = unsubmittedEntries.map((entry) => entry.entryDate).sort()
|
|
63
|
+
|
|
64
|
+
const previousSubmissions = bootstrap
|
|
65
|
+
? await db
|
|
66
|
+
.select({
|
|
67
|
+
id: timesheetSubmission.id,
|
|
68
|
+
weekStartsOn: weeklyTimesheet.weekStartsOn,
|
|
69
|
+
periodStartsOn: timesheetSubmission.periodStartsOn,
|
|
70
|
+
periodEndsOn: timesheetSubmission.periodEndsOn,
|
|
71
|
+
status: timesheetSubmission.status,
|
|
72
|
+
totalMinutes: sql<number>`coalesce(sum(${timeEntry.durationMinutes}), 0)`.mapWith(Number)
|
|
73
|
+
})
|
|
74
|
+
.from(timesheetSubmission)
|
|
75
|
+
.innerJoin(weeklyTimesheet, eq(weeklyTimesheet.id, timesheetSubmission.weeklyTimesheetId))
|
|
76
|
+
.leftJoin(timeEntry, eq(timeEntry.submissionId, timesheetSubmission.id))
|
|
77
|
+
.where(
|
|
78
|
+
and(
|
|
79
|
+
eq(timesheetSubmission.organizationId, organizationId),
|
|
80
|
+
eq(timesheetSubmission.userId, session.user.id),
|
|
81
|
+
inArray(timesheetSubmission.status, ['SUBMITTED', 'REJECTED', 'APPROVED'])
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
.groupBy(timesheetSubmission.id, weeklyTimesheet.weekStartsOn)
|
|
85
|
+
.orderBy(
|
|
86
|
+
desc(weeklyTimesheet.weekStartsOn),
|
|
87
|
+
desc(timesheetSubmission.periodEndsOn),
|
|
88
|
+
desc(timesheetSubmission.id)
|
|
89
|
+
)
|
|
90
|
+
.limit(5)
|
|
91
|
+
: []
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...(bootstrap && {
|
|
95
|
+
myWeek: {
|
|
96
|
+
previousSubmissions,
|
|
97
|
+
projects: [...new Set(bootstrap.week.entries.map((entry) => entry.projectId))].map((id) => {
|
|
98
|
+
const project = bootstrap.projects.find((item) => item.id === id)!
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
name: project.name,
|
|
102
|
+
clientName: project.clientName,
|
|
103
|
+
totalMinutes: bootstrap.week.entries
|
|
104
|
+
.filter((entry) => entry.projectId === id)
|
|
105
|
+
.reduce((sum, entry) => sum + entry.durationMinutes, 0)
|
|
106
|
+
}
|
|
107
|
+
}),
|
|
108
|
+
weekStartsOn: bootstrap.week.weekStartsOn,
|
|
109
|
+
status: bootstrap.week.submissions.some((item) => item.status === 'REJECTED')
|
|
110
|
+
? ('REJECTED' as const)
|
|
111
|
+
: bootstrap.week.submissions.some((item) => item.status === 'SUBMITTED')
|
|
112
|
+
? ('SUBMITTED' as const)
|
|
113
|
+
: bootstrap.week.entries.length > 0 &&
|
|
114
|
+
bootstrap.week.entries.every((item) => item.submissionStatus === 'APPROVED')
|
|
115
|
+
? ('APPROVED' as const)
|
|
116
|
+
: ('DRAFT' as const),
|
|
117
|
+
totalMinutes: bootstrap.week.entries.reduce((sum, entry) => sum + entry.durationMinutes, 0),
|
|
118
|
+
rejectionComment:
|
|
119
|
+
bootstrap.week.submissions.find((item) => item.status === 'REJECTED')?.rejectionComment ?? null,
|
|
120
|
+
hasRunningTimer: bootstrap.week.entries.some((entry) => Boolean(entry.timerStartedAt)),
|
|
121
|
+
batches: bootstrap.week.submissions.map((submission) => ({
|
|
122
|
+
id: submission.id,
|
|
123
|
+
status: submission.status,
|
|
124
|
+
totalMinutes: bootstrap.week.entries
|
|
125
|
+
.filter((entry) => entry.submissionId === submission.id)
|
|
126
|
+
.reduce((sum, entry) => sum + entry.durationMinutes, 0),
|
|
127
|
+
periodStartsOn: submission.periodStartsOn,
|
|
128
|
+
periodEndsOn: submission.periodEndsOn
|
|
129
|
+
})),
|
|
130
|
+
unsubmitted: unsubmittedEntries.length
|
|
131
|
+
? {
|
|
132
|
+
totalMinutes: unsubmittedEntries.reduce((sum, entry) => sum + entry.durationMinutes, 0),
|
|
133
|
+
periodStartsOn: unsubmittedDates[0]!,
|
|
134
|
+
periodEndsOn: unsubmittedDates.at(-1)!
|
|
135
|
+
}
|
|
136
|
+
: null
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
...(hasInternalApprovals && {
|
|
140
|
+
internalApprovals: {
|
|
141
|
+
pendingCount: pendingInternal.length,
|
|
142
|
+
items: approvalQueue
|
|
143
|
+
.slice(0, 5)
|
|
144
|
+
.map(({ id, userName, weekStartsOn, periodStartsOn, periodEndsOn, totalMinutes, submittedAt, status }) => ({
|
|
145
|
+
id,
|
|
146
|
+
userName,
|
|
147
|
+
weekStartsOn,
|
|
148
|
+
periodStartsOn,
|
|
149
|
+
periodEndsOn,
|
|
150
|
+
totalMinutes,
|
|
151
|
+
submittedAt,
|
|
152
|
+
status
|
|
153
|
+
}))
|
|
154
|
+
}
|
|
155
|
+
}),
|
|
156
|
+
...(reviewWorkspaces.length && {
|
|
157
|
+
clientApprovals: {
|
|
158
|
+
pendingCount: clientDashboard.pendingCount,
|
|
159
|
+
hasHistory: clientDashboard.hasHistory,
|
|
160
|
+
unassignedSupplierCount: reviewerSuppliers.filter((item) => item.reviewerCount === 0).length,
|
|
161
|
+
items: clientDashboard.items.map(
|
|
162
|
+
({ id, supplierName, person, weekStartsOn, periodStartsOn, periodEndsOn, totalMinutes, status }) => ({
|
|
163
|
+
status,
|
|
164
|
+
id,
|
|
165
|
+
supplierName,
|
|
166
|
+
person,
|
|
167
|
+
weekStartsOn,
|
|
168
|
+
periodStartsOn,
|
|
169
|
+
periodEndsOn,
|
|
170
|
+
totalMinutes
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}),
|
|
175
|
+
...(canViewSupplierTime && {
|
|
176
|
+
supplierTimesheets: {
|
|
177
|
+
items: supplierTimesheets
|
|
178
|
+
.slice(0, 5)
|
|
179
|
+
.map(({ id, supplierName, person, weekStartsOn, periodStartsOn, periodEndsOn, totalMinutes }) => ({
|
|
180
|
+
id,
|
|
181
|
+
supplierName,
|
|
182
|
+
person,
|
|
183
|
+
weekStartsOn,
|
|
184
|
+
periodStartsOn,
|
|
185
|
+
periodEndsOn,
|
|
186
|
+
totalMinutes
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
})
|
|
@@ -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 { deleteEntry } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
4
|
+
|
|
5
|
+
defineRouteMeta({
|
|
6
|
+
openAPI: {
|
|
7
|
+
tags: ['Timesheets'],
|
|
8
|
+
operationId: 'timesheetsEntriesByIdDelete',
|
|
9
|
+
summary: 'Delete a time entry',
|
|
10
|
+
description: 'Delete a time entry. Scoped to the active organization and the applicable Timesheets permission.'
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default defineEventHandler(async (event) => {
|
|
15
|
+
const { session, organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'update')
|
|
16
|
+
await deleteEntry(organizationId, session.user.id, getRouterParam(event, 'id')!)
|
|
17
|
+
return { success: true }
|
|
18
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { requireFeatureAccess } from '@nuxt-customer-portal/core/server/portal'
|
|
2
|
+
import { timesheetsFeature } from '@nuxt-customer-portal/timesheets/shared/feature'
|
|
3
|
+
import { updateEntry } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
|
|
4
|
+
import { entryUpdateSchema } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-validation'
|
|
5
|
+
|
|
6
|
+
defineRouteMeta({
|
|
7
|
+
openAPI: {
|
|
8
|
+
tags: ['Timesheets'],
|
|
9
|
+
operationId: 'timesheetsEntriesByIdPatch',
|
|
10
|
+
summary: 'Update a time entry',
|
|
11
|
+
description: 'Update a time entry. Scoped to the active organization and the applicable Timesheets permission.'
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export default defineEventHandler(async (event) => {
|
|
16
|
+
const { session, organizationId } = await requireFeatureAccess(event, timesheetsFeature.policy, 'update')
|
|
17
|
+
return updateEntry(
|
|
18
|
+
organizationId,
|
|
19
|
+
session.user.id,
|
|
20
|
+
getRouterParam(event, 'id')!,
|
|
21
|
+
entryUpdateSchema.parse(await readBody(event))
|
|
22
|
+
)
|
|
23
|
+
})
|