@nuxt-customer-portal/invoice-timesheets 0.5.0 → 0.6.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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @nuxt-customer-portal/invoice-timesheets
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cff4891: Add weekly, two-weekly, and monthly client review periods with provider-controlled combined requests, entry snapshots, withdrawal and correction history, supplementary requests, and English/Dutch notifications. Keep weekly internal approvals and historical reviews intact; interval changes start next month.
8
+
9
+ Require client approval before invoicing review-enabled client time from the persisted next-month rollout date. Run package migrations before deploying the new application code.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [cff4891]
14
+ - @nuxt-customer-portal/timesheets@0.6.0
15
+ - @nuxt-customer-portal/core@0.6.0
16
+ - @nuxt-customer-portal/invoices@0.6.0
17
+
18
+ ## 0.5.1
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [1528005]
23
+ - @nuxt-customer-portal/timesheets@0.5.1
24
+ - @nuxt-customer-portal/core@0.5.1
25
+ - @nuxt-customer-portal/invoices@0.5.1
26
+
3
27
  ## 0.5.0
4
28
 
5
29
  ### Patch Changes
@@ -1,9 +1,12 @@
1
+ import type { InvoiceSourceEntryDto } from '@nuxt-customer-portal/invoices/shared/types/invoice'
2
+
1
3
  export default defineNuxtPlugin(() => {
2
4
  useInvoiceSources().register({
3
5
  id: 'timesheets',
4
6
  labelKey: 'features.invoices.admin.approvedTime',
5
7
  descriptionKey: 'features.invoices.admin.approvedTimeDescription',
6
- load: (query) => $fetch('/api/invoice-timesheets/sources', { query }),
8
+ load: (query) =>
9
+ $fetch<{ enabled: boolean; entries: InvoiceSourceEntryDto[] }>('/api/invoice-timesheets/sources', { query }),
7
10
  create: (input) => $fetch<{ id: string }>('/api/invoice-timesheets/invoices', { method: 'POST', body: input })
8
11
  })
9
12
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxt-customer-portal/invoice-timesheets",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://nuxt-customer-portal.com",
6
6
  "repository": {
@@ -36,9 +36,9 @@
36
36
  "nanoid": "^5.1.6",
37
37
  "pg": "^8.22.0",
38
38
  "zod": "^4.4.3",
39
- "@nuxt-customer-portal/invoices": "^0.5.0",
40
- "@nuxt-customer-portal/core": "^0.5.0",
41
- "@nuxt-customer-portal/timesheets": "^0.5.0"
39
+ "@nuxt-customer-portal/core": "^0.6.0",
40
+ "@nuxt-customer-portal/invoices": "^0.6.0",
41
+ "@nuxt-customer-portal/timesheets": "^0.6.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "nuxt": "^4.5.1",
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  id: 'invoice-timesheets',
3
- version: '0.5.0',
3
+ version: '0.6.0',
4
4
  source: '@nuxt-customer-portal/invoice-timesheets',
5
5
  dependsOn: ['core', 'invoices', 'timesheets'],
6
6
  schema: './server/db/schema/invoice-timesheets.ts',
@@ -1,3 +1,4 @@
1
+ import { reviewCutover } from '@nuxt-customer-portal/timesheets/server/utils/client-review-requests'
1
2
  import { and, eq, inArray, sql } from 'drizzle-orm'
2
3
  import { nanoid } from 'nanoid'
3
4
  import { db } from '@nuxt-customer-portal/core/server/portal'
@@ -13,6 +14,14 @@ import {
13
14
  } from '@nuxt-customer-portal/timesheets/server/utils/timesheet-repository'
14
15
  import { invoiceTimeEntry } from '@nuxt-customer-portal/invoice-timesheets/server/db/schema/invoice-timesheets'
15
16
 
17
+ const clientPeriodApproved = sql`(
18
+ ${timeEntry.entryDate} < (select starts_on from timesheets.client_review_rollout)
19
+ or not exists (select 1 from timesheets.workspace_client wc where wc.workspace_organization_id = ${timeEntry.organizationId} and wc.client_organization_id = ${timeEntry.clientOrganizationId} and wc.access_mode = 'REVIEW')
20
+ or exists (select 1 from timesheets.client_review_request_entry re
21
+ join timesheets.client_review_request r on r.id = re.request_id
22
+ where re.entry_id = ${timeEntry.id} and r.status = 'APPROVED')
23
+ )`
24
+
16
25
  export const listTimesheetInvoiceSources = async (organizationId: string, from?: string, to?: string) => {
17
26
  try {
18
27
  await requireTimesheetWorkspace(organizationId)
@@ -33,7 +42,7 @@ export const listTimesheetInvoiceSources = async (organizationId: string, from?:
33
42
  projectId: timeEntry.projectId
34
43
  })
35
44
  .from(timeEntry)
36
- .where(and(eq(timeEntry.organizationId, organizationId), inArray(timeEntry.id, ids))),
45
+ .where(and(eq(timeEntry.organizationId, organizationId), inArray(timeEntry.id, ids), clientPeriodApproved)),
37
46
  db
38
47
  .select({ timeEntryId: invoiceTimeEntry.timeEntryId })
39
48
  .from(invoiceTimeEntry)
@@ -50,6 +59,7 @@ export const listTimesheetInvoiceSources = async (organizationId: string, from?:
50
59
  ])
51
60
  const context = new Map(contexts.map((item) => [item.entryId, item]))
52
61
  const used = new Set(linked.map((item) => item.timeEntryId))
62
+ const cutover = await reviewCutover()
53
63
  const disputes = new Set(disputed.map((item) => `${item.submissionId}:${item.clientOrganizationId}`))
54
64
  return {
55
65
  enabled: true,
@@ -58,7 +68,7 @@ export const listTimesheetInvoiceSources = async (organizationId: string, from?:
58
68
  return !item ||
59
69
  item.clientOrganizationId === organizationId ||
60
70
  used.has(row.entryId) ||
61
- disputes.has(`${item.submissionId}:${item.clientOrganizationId}`)
71
+ (row.date < cutover && disputes.has(`${item.submissionId}:${item.clientOrganizationId}`))
62
72
  ? []
63
73
  : [
64
74
  {
@@ -109,6 +119,9 @@ export const createInvoiceFromTimesheets = async (
109
119
  throw createError({ statusCode: 400, message: 'Internal time entries cannot be invoiced' })
110
120
  }
111
121
  return db.transaction(async (tx) => {
122
+ await tx.execute(
123
+ sql`select pg_advisory_xact_lock(hashtext(${'client-review:' + organizationId + ':' + input.clientOrganizationId}))`
124
+ )
112
125
  const sourceIds = input.lines.flatMap((line) => line.timeEntryIds)
113
126
  if (new Set(sourceIds).size !== sourceIds.length) {
114
127
  throw createError({ statusCode: 400, message: 'A time entry can only occur once on an invoice' })
@@ -125,7 +138,8 @@ export const createInvoiceFromTimesheets = async (
125
138
  and(
126
139
  eq(timeEntry.organizationId, organizationId),
127
140
  eq(timeEntry.clientOrganizationId, input.clientOrganizationId),
128
- inArray(timeEntry.id, sourceIds)
141
+ inArray(timeEntry.id, sourceIds),
142
+ clientPeriodApproved
129
143
  )
130
144
  ),
131
145
  tx
@@ -147,6 +161,7 @@ export const createInvoiceFromTimesheets = async (
147
161
  )
148
162
  )
149
163
  ])
164
+ const cutover = await reviewCutover(tx)
150
165
  const disputedSubmissions = new Set(disputed.map((item) => item.submissionId))
151
166
  if (
152
167
  entries.length !== sourceIds.length ||
@@ -154,7 +169,7 @@ export const createInvoiceFromTimesheets = async (
154
169
  entries.some(
155
170
  (row) =>
156
171
  row.status !== 'APPROVED' ||
157
- disputedSubmissions.has(row.entry.submissionId) ||
172
+ (row.entry.entryDate < cutover && disputedSubmissions.has(row.entry.submissionId)) ||
158
173
  !row.entry.billableSnapshot ||
159
174
  row.entry.currencySnapshot !== input.currency
160
175
  )