@nuxt-customer-portal/invoice-timesheets 0.4.9 → 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,37 @@
|
|
|
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
|
+
|
|
27
|
+
## 0.5.0
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- @nuxt-customer-portal/invoices@0.5.0
|
|
32
|
+
- @nuxt-customer-portal/timesheets@0.5.0
|
|
33
|
+
- @nuxt-customer-portal/core@0.5.0
|
|
34
|
+
|
|
3
35
|
## 0.4.9
|
|
4
36
|
|
|
5
37
|
### 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) =>
|
|
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.
|
|
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/core": "^0.
|
|
40
|
-
"@nuxt-customer-portal/
|
|
41
|
-
"@nuxt-customer-portal/
|
|
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",
|
package/portal.manifest.mjs
CHANGED
|
@@ -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
|
)
|