@nuxt-customer-portal/invoices 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 (82) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +4 -0
  4. package/app/assets/invoices.css +332 -0
  5. package/app/components/InvoicesAdminEmptyState.vue +28 -0
  6. package/app/components/InvoicesAdminInvoices.vue +950 -0
  7. package/app/components/InvoicesAdminListToolbar.vue +19 -0
  8. package/app/components/InvoicesAdminPaginatedList.vue +95 -0
  9. package/app/components/InvoicesClientAccessPanel.vue +63 -0
  10. package/app/components/InvoicesClientSettingsPanel.vue +150 -0
  11. package/app/components/InvoicesDashboardCard.vue +29 -0
  12. package/app/components/InvoicesDashboardReceivedInvoices.vue +1 -0
  13. package/app/components/InvoicesDashboardSalesInvoices.vue +1 -0
  14. package/app/components/InvoicesInvoiceDetail.vue +1109 -0
  15. package/app/components/InvoicesInvoiceEmailModal.vue +223 -0
  16. package/app/components/InvoicesPageShell.vue +7 -0
  17. package/app/components/InvoicesSenderDetailsForm.vue +229 -0
  18. package/app/composables/useInvoiceSources.ts +9 -0
  19. package/app/composables/useInvoices.ts +68 -0
  20. package/app/composables/useInvoicesAdminList.ts +113 -0
  21. package/app/pages/admin/invoices/[id].vue +36 -0
  22. package/app/pages/admin/invoices/index.vue +61 -0
  23. package/app/pages/admin/invoices/new.vue +18 -0
  24. package/app/pages/admin/invoices/settings.vue +86 -0
  25. package/app/pages/invoices/[id].vue +37 -0
  26. package/app/pages/invoices/index.vue +131 -0
  27. package/app/pages/invoices/viewers.vue +115 -0
  28. package/app/plugins/invoices-feature.ts +60 -0
  29. package/i18n/locales/en.json +306 -0
  30. package/i18n/locales/nl.json +306 -0
  31. package/migrations/0000_baseline.sql +235 -0
  32. package/migrations/meta/0000_snapshot.json +1318 -0
  33. package/migrations/meta/_journal.json +13 -0
  34. package/nuxt.config.ts +15 -0
  35. package/package.json +60 -0
  36. package/portal.manifest.mjs +9 -0
  37. package/server/api/invoices/admin/bootstrap.get.ts +17 -0
  38. package/server/api/invoices/admin/clients/[clientId]/access.get.ts +11 -0
  39. package/server/api/invoices/admin/clients/[clientId]/contacts/[id].delete.ts +8 -0
  40. package/server/api/invoices/admin/clients/[clientId]/contacts/[id].patch.ts +13 -0
  41. package/server/api/invoices/admin/clients/[clientId]/contacts/index.get.ts +8 -0
  42. package/server/api/invoices/admin/clients/[clientId]/contacts/index.post.ts +12 -0
  43. package/server/api/invoices/admin/email-domain.get.ts +17 -0
  44. package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].delete.ts +23 -0
  45. package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].get.ts +30 -0
  46. package/server/api/invoices/admin/invoices/[id]/attachments.post.ts +27 -0
  47. package/server/api/invoices/admin/invoices/[id]/email-preview.get.ts +22 -0
  48. package/server/api/invoices/admin/invoices/[id]/email-status.post.ts +18 -0
  49. package/server/api/invoices/admin/invoices/[id]/email.post.ts +24 -0
  50. package/server/api/invoices/admin/invoices/[id]/index.get.ts +17 -0
  51. package/server/api/invoices/admin/invoices/[id]/issue.post.ts +24 -0
  52. package/server/api/invoices/admin/invoices/[id]/payments.post.ts +24 -0
  53. package/server/api/invoices/admin/invoices/[id]/pdf.get.ts +29 -0
  54. package/server/api/invoices/admin/invoices/[id]/reminder-preview.get.ts +24 -0
  55. package/server/api/invoices/admin/invoices/[id]/reminder.post.ts +25 -0
  56. package/server/api/invoices/admin/invoices/[id].patch.ts +24 -0
  57. package/server/api/invoices/admin/invoices/index.get.ts +20 -0
  58. package/server/api/invoices/admin/invoices/index.post.ts +19 -0
  59. package/server/api/invoices/admin/invoices/next-number.get.ts +16 -0
  60. package/server/api/invoices/admin/settings.put.ts +9 -0
  61. package/server/api/invoices/capabilities.get.ts +25 -0
  62. package/server/api/invoices/client/[accessId]/viewers.get.ts +10 -0
  63. package/server/api/invoices/client/[accessId]/viewers.put.ts +17 -0
  64. package/server/api/invoices/client/invoices/[id]/attachments/[attachmentId].get.ts +20 -0
  65. package/server/api/invoices/client/invoices/[id]/index.get.ts +12 -0
  66. package/server/api/invoices/client/invoices/[id]/pdf.get.ts +23 -0
  67. package/server/api/invoices/client/invoices/index.get.ts +14 -0
  68. package/server/api/invoices/client/suppliers.get.ts +8 -0
  69. package/server/db/schema/invoices.ts +251 -0
  70. package/server/plugins/openapi-contracts.ts +55 -0
  71. package/server/utils/invoice-email-status.ts +64 -0
  72. package/server/utils/invoice-email.ts +240 -0
  73. package/server/utils/invoice-listing.ts +71 -0
  74. package/server/utils/invoice-pdf.ts +358 -0
  75. package/server/utils/invoice-repository.ts +821 -0
  76. package/server/utils/invoice-validation.ts +112 -0
  77. package/shared/email-delivery-status.ts +23 -0
  78. package/shared/feature.ts +148 -0
  79. package/shared/invoice-introduction.ts +21 -0
  80. package/shared/invoice-number.ts +24 -0
  81. package/shared/types/invoice.ts +251 -0
  82. package/tokens.css +25 -0
@@ -0,0 +1,112 @@
1
+ import { z } from 'zod'
2
+ import { hasNumericInvoiceSequence } from '@nuxt-customer-portal/invoices/shared/invoice-number'
3
+
4
+ const id = z.string().trim().min(1).max(200)
5
+ const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
6
+ const moneyMinor = z.number().int().min(0).max(1_000_000_000)
7
+ const invoiceNumber = z
8
+ .string()
9
+ .trim()
10
+ .min(1)
11
+ .max(60)
12
+ .refine(hasNumericInvoiceSequence, 'Invoice number must end with a numeric sequence')
13
+ export const invoiceLineSchema = z.object({
14
+ description: z.string().trim().min(1).max(500),
15
+ quantityMilli: z.number().int().positive().max(100_000_000),
16
+ unit: z.string().trim().min(1).max(30).default('item'),
17
+ unitPriceMinor: moneyMinor,
18
+ vatRateBasisPoints: z.number().int().min(0).max(10_000).default(2100)
19
+ })
20
+ export const invoiceCreateSchema = z
21
+ .object({
22
+ clientOrganizationId: id,
23
+ contactId: id.nullable().optional(),
24
+ number: invoiceNumber,
25
+ currency: z.string().length(3),
26
+ issueDate: isoDate,
27
+ dueDate: isoDate,
28
+ subject: z.string().trim().max(500).nullable().optional(),
29
+ notes: z.string().trim().max(5000).nullable().optional(),
30
+ lines: z.array(invoiceLineSchema).min(1).max(500)
31
+ })
32
+ .refine((value) => value.dueDate >= value.issueDate, {
33
+ path: ['dueDate'],
34
+ message: 'Due date must not precede invoice date'
35
+ })
36
+ export type InvoiceCreateInput = z.infer<typeof invoiceCreateSchema>
37
+ export const invoiceUpdateSchema = z
38
+ .object({
39
+ number: invoiceNumber,
40
+ issueDate: isoDate,
41
+ dueDate: isoDate,
42
+ subject: z.string().trim().max(500).nullable().optional(),
43
+ notes: z.string().trim().max(5000).nullable().optional()
44
+ })
45
+ .refine((value) => value.dueDate >= value.issueDate, {
46
+ path: ['dueDate'],
47
+ message: 'Due date must not precede invoice date'
48
+ })
49
+ export const invoiceIssueSchema = z.object({ action: z.enum(['VOID', 'UNVOID']) })
50
+ export const invoicePaymentSchema = z.object({
51
+ paidOn: isoDate,
52
+ amountMinor: moneyMinor.positive(),
53
+ reference: z.string().trim().max(200).nullable().optional(),
54
+ note: z.string().trim().max(1000).nullable().optional()
55
+ })
56
+ export const invoiceEmailDeliverySchema = z
57
+ .object({
58
+ to: z.string().trim().toLowerCase().email().max(320),
59
+ cc: z.array(z.string().trim().toLowerCase().email().max(320)).max(20).default([]),
60
+ locale: z.enum(['nl', 'en']),
61
+ subject: z.string().trim().min(1).max(500),
62
+ body: z.string().trim().min(1).max(10_000)
63
+ })
64
+ .transform((value) => ({ ...value, cc: [...new Set(value.cc.filter((email) => email !== value.to))] }))
65
+ export const invoiceSettingsSchema = z.object({
66
+ enabled: z.boolean(),
67
+ currency: z.string().trim().toUpperCase().length(3),
68
+ defaultVatRateBasisPoints: z.number().int().min(0).max(10_000),
69
+ address: z.string().trim().max(5000),
70
+ registrationNumber: z.string().trim().max(200).nullable(),
71
+ vatNumber: z.string().trim().max(200).nullable(),
72
+ iban: z.string().trim().max(100).nullable(),
73
+ bic: z.string().trim().max(100).nullable(),
74
+ invoiceEmail: z.string().trim().email().max(320).nullable(),
75
+ preferredLocale: z.enum(['nl', 'en'])
76
+ })
77
+ export const billingContactCreateSchema = z.object({
78
+ userId: id.nullable().optional(),
79
+ name: z.string().trim().min(1).max(200),
80
+ email: z.string().trim().toLowerCase().email().max(320),
81
+ phone: z.string().trim().max(80).nullable().optional(),
82
+ jobTitle: z.string().trim().max(160).nullable().optional()
83
+ })
84
+ export const billingContactUpdateSchema = billingContactCreateSchema
85
+ .partial()
86
+ .refine((value) => Object.keys(value).length > 0, 'At least one field is required')
87
+ const listBase = z.object({
88
+ search: z.string().trim().max(200).optional(),
89
+ page: z.coerce.number().int().positive().default(1),
90
+ pageSize: z.coerce.number().int().min(1).max(100).default(20),
91
+ sortDir: z.enum(['asc', 'desc']).default('desc')
92
+ })
93
+ export const invoiceListQuerySchema = listBase.extend({
94
+ status: z.enum(['DRAFT', 'ISSUED', 'PAID', 'VOID']).optional(),
95
+ clientOrganizationId: id.optional(),
96
+ overdue: z
97
+ .enum(['true', 'false'])
98
+ .transform((value) => value === 'true')
99
+ .optional(),
100
+ sortBy: z.enum(['issueDate', 'dueDate', 'number', 'totalMinor']).default('issueDate')
101
+ })
102
+ export const clientInvoiceListQuerySchema = listBase.extend({
103
+ status: z.enum(['ISSUED', 'PAID']).optional(),
104
+ accessId: id.optional(),
105
+ overdue: z
106
+ .enum(['true', 'false'])
107
+ .transform((value) => value === 'true')
108
+ .optional(),
109
+ sortBy: z.enum(['issueDate', 'dueDate', 'number', 'totalMinor']).default('issueDate')
110
+ })
111
+ export type InvoiceListQuery = z.infer<typeof invoiceListQuerySchema>
112
+ export type ClientInvoiceListQuery = z.infer<typeof clientInvoiceListQuerySchema>
@@ -0,0 +1,23 @@
1
+ export const knownEmailProviderEvents = [
2
+ 'sent',
3
+ 'delivered',
4
+ 'delivery_delayed',
5
+ 'bounced',
6
+ 'failed',
7
+ 'suppressed',
8
+ 'complained',
9
+ 'opened',
10
+ 'clicked'
11
+ ] as const
12
+
13
+ export type KnownEmailProviderEvent = (typeof knownEmailProviderEvents)[number]
14
+
15
+ export const normalizeEmailProviderEvent = (value: string): string =>
16
+ value
17
+ .trim()
18
+ .toLowerCase()
19
+ .replace(/^email\./, '')
20
+ .replaceAll('-', '_')
21
+
22
+ export const isKnownEmailProviderEvent = (value: string): value is KnownEmailProviderEvent =>
23
+ knownEmailProviderEvents.includes(value as KnownEmailProviderEvent)
@@ -0,0 +1,148 @@
1
+ import type { PortalFeatureDefinition } from '@nuxt-customer-portal/core/shared/types/feature'
2
+
3
+ export const invoiceActions = ['read', 'create', 'update', 'manage'] as const
4
+ export type InvoiceAction = (typeof invoiceActions)[number]
5
+
6
+ export const invoicesFeature: PortalFeatureDefinition<InvoiceAction> = {
7
+ id: 'invoices',
8
+ modules: [
9
+ {
10
+ id: 'invoices',
11
+ labelKey: 'features.invoices.title',
12
+ icon: 'i-lucide-receipt-text',
13
+ to: '/invoices',
14
+ routePrefixes: ['/invoices', '/admin/invoices'],
15
+ audiences: ['authenticated'],
16
+ order: 31,
17
+ menuItems: [
18
+ {
19
+ id: 'received-invoices',
20
+ labelKey: 'features.invoices.receivedInvoices',
21
+ icon: 'i-lucide-inbox',
22
+ to: '/invoices',
23
+ exact: true,
24
+ audiences: ['authenticated']
25
+ },
26
+ {
27
+ id: 'invoice-viewers',
28
+ labelKey: 'features.invoices.clientInvoices.viewersTitle',
29
+ icon: 'i-lucide-users-round',
30
+ to: '/invoices/viewers',
31
+ audiences: ['clientAdmin']
32
+ },
33
+ {
34
+ id: 'sales-invoices',
35
+ labelKey: 'features.invoices.salesInvoices',
36
+ icon: 'i-lucide-receipt-text',
37
+ to: '/admin/invoices',
38
+ exact: true,
39
+ audiences: ['providerAdmin']
40
+ },
41
+ {
42
+ id: 'invoice-settings',
43
+ labelKey: 'features.invoices.admin.workspaceSettings',
44
+ icon: 'i-lucide-settings-2',
45
+ to: '/admin/invoices/settings',
46
+ audiences: ['providerAdmin']
47
+ }
48
+ ]
49
+ }
50
+ ],
51
+ dashboardWidgets: [
52
+ { id: 'invoices-sales', component: 'InvoicesDashboardSalesInvoices', area: 'main', size: 'half', order: 15 },
53
+ { id: 'invoices-received', component: 'InvoicesDashboardReceivedInvoices', area: 'main', size: 'half', order: 40 }
54
+ ],
55
+ clientIntegration: {
56
+ moduleId: 'invoices',
57
+ labelKey: 'features.invoices.title',
58
+ detailComponent: 'InvoicesClientSettingsPanel'
59
+ },
60
+ emails: [
61
+ {
62
+ id: 'invoice',
63
+ labelKey: 'features.invoices.admin.emailMessages.invoice',
64
+ defaults: {
65
+ en: {
66
+ subject: 'Invoice {{invoice_number}} from {{sender_name}}',
67
+ body: 'Dear Sir or Madam,<br><br>Please find invoice {{invoice_number}} attached.<br><br>Kind regards,<br>{{sender_name}}'
68
+ },
69
+ nl: {
70
+ subject: 'Factuur {{invoice_number}} van {{sender_name}}',
71
+ body: 'Geachte heer/mevrouw,<br><br>In de bijlage vindt u factuur {{invoice_number}}.<br><br>Met vriendelijke groet,<br>{{sender_name}}'
72
+ }
73
+ },
74
+ placeholders: [
75
+ {
76
+ key: 'invoice_number',
77
+ labelKey: 'features.invoices.admin.emailPlaceholders.invoiceNumber',
78
+ example: '2026-001'
79
+ },
80
+ {
81
+ key: 'sender_name',
82
+ labelKey: 'features.invoices.admin.emailPlaceholders.senderName',
83
+ example: 'Example Company'
84
+ },
85
+ {
86
+ key: 'recipient_name',
87
+ labelKey: 'features.invoices.admin.emailPlaceholders.recipientName',
88
+ example: 'Example Client'
89
+ },
90
+ {
91
+ key: 'due_date',
92
+ labelKey: 'features.invoices.admin.emailPlaceholders.dueDate',
93
+ example: '30 September 2026'
94
+ },
95
+ {
96
+ key: 'outstanding_amount',
97
+ labelKey: 'features.invoices.admin.emailPlaceholders.outstandingAmount',
98
+ example: '€1,250.00'
99
+ }
100
+ ]
101
+ },
102
+ {
103
+ id: 'payment-reminder',
104
+ labelKey: 'features.invoices.admin.emailMessages.paymentReminder',
105
+ defaults: {
106
+ en: {
107
+ subject: 'Payment reminder for invoice {{invoice_number}} from {{sender_name}}',
108
+ body: 'Dear Sir or Madam,<br><br>Invoice {{invoice_number}}, due on {{due_date}}, remains outstanding for {{outstanding_amount}}. Please arrange payment.<br><br>Kind regards,<br>{{sender_name}}'
109
+ },
110
+ nl: {
111
+ subject: 'Betalingsherinnering factuur {{invoice_number}} van {{sender_name}}',
112
+ body: 'Geachte heer/mevrouw,<br><br>Factuur {{invoice_number}}, met vervaldatum {{due_date}}, staat nog open voor {{outstanding_amount}}. Wij verzoeken u vriendelijk te betalen.<br><br>Met vriendelijke groet,<br>{{sender_name}}'
113
+ }
114
+ },
115
+ placeholders: [
116
+ {
117
+ key: 'invoice_number',
118
+ labelKey: 'features.invoices.admin.emailPlaceholders.invoiceNumber',
119
+ example: '2026-001'
120
+ },
121
+ {
122
+ key: 'sender_name',
123
+ labelKey: 'features.invoices.admin.emailPlaceholders.senderName',
124
+ example: 'Example Company'
125
+ },
126
+ {
127
+ key: 'recipient_name',
128
+ labelKey: 'features.invoices.admin.emailPlaceholders.recipientName',
129
+ example: 'Example Client'
130
+ },
131
+ {
132
+ key: 'due_date',
133
+ labelKey: 'features.invoices.admin.emailPlaceholders.dueDate',
134
+ example: '30 September 2026'
135
+ },
136
+ {
137
+ key: 'outstanding_amount',
138
+ labelKey: 'features.invoices.admin.emailPlaceholders.outstandingAmount',
139
+ example: '€1,250.00'
140
+ }
141
+ ]
142
+ }
143
+ ],
144
+ policy: {
145
+ PROVIDER: { owner: invoiceActions, admin: invoiceActions, member: [] },
146
+ CLIENT: { owner: ['read', 'manage'], admin: ['read', 'manage'], member: ['read'] }
147
+ }
148
+ }
@@ -0,0 +1,21 @@
1
+ export const createHoursIntroduction = (entries: Array<{ person: string; date: string }>, locale: 'nl' | 'en') => {
2
+ if (!entries.length) {
3
+ return ''
4
+ }
5
+ const names = [...new Set(entries.map((entry) => entry.person))].sort()
6
+ const people = new Intl.ListFormat(locale, { style: 'long', type: 'conjunction' }).format(names)
7
+ const dates = entries.map((entry) => entry.date).sort()
8
+ const format = (date: string) =>
9
+ new Intl.DateTimeFormat(locale, {
10
+ day: 'numeric',
11
+ month: 'long',
12
+ year: 'numeric',
13
+ timeZone: 'UTC'
14
+ }).format(new Date(`${date}T12:00:00Z`))
15
+ const start = dates[0]!
16
+ const end = dates[dates.length - 1]!
17
+ const period = start === end ? format(start) : `${format(start)} ${locale === 'nl' ? 't/m' : 'to'} ${format(end)}`
18
+ return locale === 'nl'
19
+ ? `Hierbij factureren wij de uren van ${people} over de periode ${period}.`
20
+ : `We hereby invoice the hours worked by ${people} for the period ${period}.`
21
+ }
@@ -0,0 +1,24 @@
1
+ const trailingSequence = /\d+$/
2
+
3
+ export const hasNumericInvoiceSequence = (number: string) => trailingSequence.test(number)
4
+
5
+ export const incrementInvoiceNumber = (number: string): string => {
6
+ const match = number.match(trailingSequence)
7
+ if (!match || match.index === undefined) {
8
+ throw new Error('Invoice number must end with a numeric sequence')
9
+ }
10
+ const digits = match[0].split('')
11
+ let carry = 1
12
+ for (let index = digits.length - 1; index >= 0 && carry; index -= 1) {
13
+ const next = Number(digits[index]) + carry
14
+ digits[index] = String(next % 10)
15
+ carry = next > 9 ? 1 : 0
16
+ }
17
+ if (carry) {
18
+ digits.unshift('1')
19
+ }
20
+ const next = digits.join('').padStart(match[0].length, '0')
21
+ return `${number.slice(0, match.index)}${next}`
22
+ }
23
+
24
+ export const firstInvoiceNumber = (year = new Date().getFullYear()): string => `${year}.0001`
@@ -0,0 +1,251 @@
1
+ export type InvoiceStatus = 'DRAFT' | 'ISSUED' | 'PAID' | 'VOID'
2
+ export interface InvoiceLineDto {
3
+ id: string
4
+ description: string
5
+ quantityMilli: number
6
+ unit: string
7
+ unitPriceMinor: number
8
+ vatRateBasisPoints: number
9
+ amountMinor: number
10
+ }
11
+ export interface InvoicePaymentDto {
12
+ id: string
13
+ paidOn: string
14
+ amountMinor: number
15
+ reference: string | null
16
+ note: string | null
17
+ }
18
+ export interface InvoiceHistoryDto {
19
+ id: string
20
+ action: string
21
+ actorName: string
22
+ amountMinor: number | null
23
+ attachmentName: string | null
24
+ createdAt: string
25
+ }
26
+ export interface InvoiceAttachmentDto {
27
+ id: string
28
+ fileName: string
29
+ contentType: string
30
+ size: number
31
+ uploadedByName: string
32
+ createdAt: string
33
+ }
34
+ export interface InvoiceEmailDeliveryDto {
35
+ id: string
36
+ purpose: 'INVOICE' | 'REMINDER'
37
+ status: 'PENDING' | 'SENT' | 'FAILED'
38
+ recipientEmail: string
39
+ ccEmails: string[]
40
+ locale: string
41
+ subject: string
42
+ actorName: string
43
+ providerMessageId: string | null
44
+ providerLastEvent: string | null
45
+ providerStatusCheckedAt: string | null
46
+ errorMessage: string | null
47
+ createdAt: string
48
+ sentAt: string | null
49
+ }
50
+ export interface InvoiceDto {
51
+ id: string
52
+ organizationId: string
53
+ clientOrganizationId: string | null
54
+ number: string
55
+ status: InvoiceStatus
56
+ currency: string
57
+ issueDate: string
58
+ dueDate: string
59
+ subject: string | null
60
+ notes: string | null
61
+ senderName: string
62
+ senderLogo: string | null
63
+ senderAddress: string
64
+ senderRegistration: string | null
65
+ senderVatNumber: string | null
66
+ senderIban: string | null
67
+ senderBic: string | null
68
+ recipientName: string
69
+ recipientAddress: string
70
+ recipientContactName: string | null
71
+ recipientEmail: string | null
72
+ recipientLocale: string
73
+ issuedAt: string | null
74
+ createdAt: string
75
+ updatedAt: string
76
+ lines: InvoiceLineDto[]
77
+ payments: InvoicePaymentDto[]
78
+ history: InvoiceHistoryDto[]
79
+ attachments: InvoiceAttachmentDto[]
80
+ emailDeliveries: InvoiceEmailDeliveryDto[]
81
+ subtotalMinor: number
82
+ vatMinor: number
83
+ totalMinor: number
84
+ paidMinor: number
85
+ outstandingMinor: number
86
+ isOverdue: boolean
87
+ daysOverdue: number
88
+ reminderCount: number
89
+ lastReminderSentAt: string | null
90
+ }
91
+ export type ClientInvoiceSummaryDto = Pick<
92
+ InvoiceDto,
93
+ | 'id'
94
+ | 'number'
95
+ | 'status'
96
+ | 'currency'
97
+ | 'issueDate'
98
+ | 'dueDate'
99
+ | 'subject'
100
+ | 'totalMinor'
101
+ | 'outstandingMinor'
102
+ | 'isOverdue'
103
+ | 'daysOverdue'
104
+ > & { supplierName: string; accessId: string }
105
+ export type ClientInvoiceDto = Omit<
106
+ InvoiceDto,
107
+ 'payments' | 'reminderCount' | 'lastReminderSentAt' | 'history' | 'emailDeliveries'
108
+ > & {
109
+ supplierName: string
110
+ accessId: string
111
+ payments?: never
112
+ reminderCount?: never
113
+ lastReminderSentAt?: never
114
+ history?: never
115
+ emailDeliveries?: never
116
+ }
117
+ export type InvoiceHistoryAction =
118
+ | 'CREATED'
119
+ | 'EDITED'
120
+ | 'ISSUED'
121
+ | 'VOIDED'
122
+ | 'UNVOIDED'
123
+ | 'PAYMENT_REGISTERED'
124
+ | 'ATTACHMENT_ADDED'
125
+ | 'ATTACHMENT_REMOVED'
126
+ | 'EMAIL_SENT'
127
+ | 'REMINDER_SENT'
128
+ export type InvoiceEmailPurpose = 'INVOICE' | 'REMINDER'
129
+ export interface OrganizationInvoiceProfileDto extends InvoiceSettingsDto {
130
+ name: string
131
+ logo: string | null
132
+ }
133
+ export interface InvoiceSettingsDto {
134
+ organizationId: string
135
+ enabled: boolean
136
+ currency: string
137
+ defaultVatRateBasisPoints: number
138
+ address: string
139
+ registrationNumber: string | null
140
+ vatNumber: string | null
141
+ iban: string | null
142
+ bic: string | null
143
+ invoiceEmail: string | null
144
+ preferredLocale: 'nl' | 'en'
145
+ }
146
+ export interface InvoiceCapabilitiesDto {
147
+ canConfigureInvoices: boolean
148
+ canManageInvoices: boolean
149
+ canViewReceivedInvoices: boolean
150
+ canManageViewers: boolean
151
+ invoicesEnabled: boolean
152
+ }
153
+ export interface ClientInvoiceSupplierDto {
154
+ id: string
155
+ providerOrganizationId: string
156
+ providerName: string
157
+ canViewInvoices: boolean
158
+ }
159
+ export interface ClientInvoiceViewerDto {
160
+ id: string
161
+ name: string
162
+ email: string
163
+ role: string
164
+ assigned: boolean
165
+ fixedAccess: boolean
166
+ }
167
+ export interface InvoiceEmailPreviewDto {
168
+ to: string
169
+ cc: string[]
170
+ locale: string
171
+ subject: string
172
+ body: string
173
+ senderEmail: string
174
+ senderDomain: string
175
+ emailProviderConfigured: boolean
176
+ senderDomainVerified: boolean
177
+ attachments: Array<{ fileName: string; size: number }>
178
+ totalAttachmentSize: number
179
+ maximumAttachmentSize: number
180
+ }
181
+ export interface InvoiceEmailStatusRefreshDto {
182
+ deliveries: InvoiceEmailDeliveryDto[]
183
+ failures: Array<{ deliveryId: string; code: 'PROVIDER_NOT_CONFIGURED' | 'PROVIDER_LOOKUP_FAILED' }>
184
+ }
185
+ export interface InvoiceContactDto {
186
+ id: string
187
+ name: string
188
+ email: string
189
+ phone: string | null
190
+ jobTitle: string | null
191
+ }
192
+ export interface InvoiceClientDto {
193
+ id: string
194
+ organizationId: string
195
+ name: string
196
+ officialName: string
197
+ address: string
198
+ registrationNumber: string | null
199
+ vatNumber: string | null
200
+ invoiceEmail: string | null
201
+ preferredLocale: 'nl' | 'en'
202
+ contacts: InvoiceContactDto[]
203
+ }
204
+ export interface InvoiceSourceEntryDto {
205
+ entryId: string
206
+ date: string
207
+ client: string
208
+ clientOrganizationId: string
209
+ project: string
210
+ projectId: string
211
+ person: string
212
+ activity: string
213
+ minutes: number
214
+ hourlyRateMinor: number
215
+ amountMinor: number
216
+ currency: string
217
+ note: string | null
218
+ }
219
+ export type InvoiceableEntryDto = InvoiceSourceEntryDto
220
+ export interface InvoicesListPagination {
221
+ total: number
222
+ page: number
223
+ pageSize: number
224
+ pageCount: number
225
+ }
226
+ export interface InvoicesListResponse<T> {
227
+ items: T[]
228
+ pagination: InvoicesListPagination
229
+ }
230
+ export interface InvoiceSourceProvider {
231
+ id: string
232
+ labelKey: string
233
+ descriptionKey: string
234
+ load: (query: { from: string; to: string }) => Promise<{ enabled: boolean; entries: InvoiceSourceEntryDto[] }>
235
+ create: (input: Record<string, unknown>) => Promise<{ id: string }>
236
+ }
237
+ export interface InvoiceClientAccessOverviewDto {
238
+ configured: boolean
239
+ enabled: boolean
240
+ moduleEnabled: boolean
241
+ members: {
242
+ id: string
243
+ name: string
244
+ email: string
245
+ image: string | null
246
+ role: string
247
+ fixedAccess: boolean
248
+ assigned: boolean
249
+ canView: boolean
250
+ }[]
251
+ }
package/tokens.css ADDED
@@ -0,0 +1,25 @@
1
+ :root {
2
+ --invoices-color-running: var(--color-success-600);
3
+ --invoices-color-running-soft: var(--color-success-50);
4
+ --invoices-color-weekend: var(--color-neutral-50);
5
+ --invoices-rule: var(--ui-border);
6
+ --invoices-radius: var(--ui-radius);
7
+ --invoices-space-cell: 0.75rem;
8
+ --invoices-duration-fast: 120ms;
9
+ --invoices-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
10
+ }
11
+
12
+ .dark {
13
+ --invoices-color-running: var(--color-success-400);
14
+ --invoices-color-running-soft: var(--color-neutral-800);
15
+ --invoices-color-weekend: var(--color-neutral-900);
16
+ }
17
+
18
+ html[data-portal-theme='brutal'] {
19
+ --invoices-color-weekend: var(--color-paper-2);
20
+ }
21
+
22
+ html.dark[data-portal-theme='brutal'] {
23
+ --invoices-color-running-soft: var(--color-paper-2);
24
+ --invoices-color-weekend: var(--color-paper-2);
25
+ }