@fast-white-cat/integration-ksef-direct 0.1.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 (140) hide show
  1. package/README.md +36 -0
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/modules/integration_ksef_direct/acl.js +13 -0
  5. package/dist/modules/integration_ksef_direct/acl.js.map +7 -0
  6. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].js +92 -0
  7. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].js.map +7 -0
  8. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.js +105 -0
  9. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.js.map +7 -0
  10. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.js +158 -0
  11. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.js.map +7 -0
  12. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].js +86 -0
  13. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].js.map +7 -0
  14. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.js +112 -0
  15. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.js.map +7 -0
  16. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.js +54 -0
  17. package/dist/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.js.map +7 -0
  18. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.js +64 -0
  19. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.js.map +7 -0
  20. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.js +104 -0
  21. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.js.map +7 -0
  22. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.js +41 -0
  23. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.js.map +7 -0
  24. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.js +172 -0
  25. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.js.map +7 -0
  26. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.js +80 -0
  27. package/dist/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.js.map +7 -0
  28. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.js +441 -0
  29. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.js.map +7 -0
  30. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.js +8 -0
  31. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.js.map +7 -0
  32. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.js +193 -0
  33. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.js.map +7 -0
  34. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.js +314 -0
  35. package/dist/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.js.map +7 -0
  36. package/dist/modules/integration_ksef_direct/backend/page.js +154 -0
  37. package/dist/modules/integration_ksef_direct/backend/page.js.map +7 -0
  38. package/dist/modules/integration_ksef_direct/commands/create-ksef-direct-document.js +80 -0
  39. package/dist/modules/integration_ksef_direct/commands/create-ksef-direct-document.js.map +7 -0
  40. package/dist/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.js +43 -0
  41. package/dist/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.js.map +7 -0
  42. package/dist/modules/integration_ksef_direct/data/entities.js +224 -0
  43. package/dist/modules/integration_ksef_direct/data/entities.js.map +7 -0
  44. package/dist/modules/integration_ksef_direct/data/validators.js +103 -0
  45. package/dist/modules/integration_ksef_direct/data/validators.js.map +7 -0
  46. package/dist/modules/integration_ksef_direct/di.js +11 -0
  47. package/dist/modules/integration_ksef_direct/di.js.map +7 -0
  48. package/dist/modules/integration_ksef_direct/events.js +21 -0
  49. package/dist/modules/integration_ksef_direct/events.js.map +7 -0
  50. package/dist/modules/integration_ksef_direct/index.js +10 -0
  51. package/dist/modules/integration_ksef_direct/index.js.map +7 -0
  52. package/dist/modules/integration_ksef_direct/integration.js +56 -0
  53. package/dist/modules/integration_ksef_direct/integration.js.map +7 -0
  54. package/dist/modules/integration_ksef_direct/lib/health.js +32 -0
  55. package/dist/modules/integration_ksef_direct/lib/health.js.map +7 -0
  56. package/dist/modules/integration_ksef_direct/lib/invoiceNumberFormat.js +23 -0
  57. package/dist/modules/integration_ksef_direct/lib/invoiceNumberFormat.js.map +7 -0
  58. package/dist/modules/integration_ksef_direct/lib/ksefClient.js +523 -0
  59. package/dist/modules/integration_ksef_direct/lib/ksefClient.js.map +7 -0
  60. package/dist/modules/integration_ksef_direct/lib/ksefCrypto.js +103 -0
  61. package/dist/modules/integration_ksef_direct/lib/ksefCrypto.js.map +7 -0
  62. package/dist/modules/integration_ksef_direct/lib/ksefFa2Xml.js +123 -0
  63. package/dist/modules/integration_ksef_direct/lib/ksefFa2Xml.js.map +7 -0
  64. package/dist/modules/integration_ksef_direct/lib/ksefXmlParser.js +76 -0
  65. package/dist/modules/integration_ksef_direct/lib/ksefXmlParser.js.map +7 -0
  66. package/dist/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.js +15 -0
  67. package/dist/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.js.map +7 -0
  68. package/dist/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.js +17 -0
  69. package/dist/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.js.map +7 -0
  70. package/dist/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.js +15 -0
  71. package/dist/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.js.map +7 -0
  72. package/dist/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.js +17 -0
  73. package/dist/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.js.map +7 -0
  74. package/dist/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.js +16 -0
  75. package/dist/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.js.map +7 -0
  76. package/dist/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.js +15 -0
  77. package/dist/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.js.map +7 -0
  78. package/dist/modules/integration_ksef_direct/setup.js +11 -0
  79. package/dist/modules/integration_ksef_direct/setup.js.map +7 -0
  80. package/dist/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.js +19 -0
  81. package/dist/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.js.map +7 -0
  82. package/dist/modules/integration_ksef_direct/workers/check-ksef-document-status.js +103 -0
  83. package/dist/modules/integration_ksef_direct/workers/check-ksef-document-status.js.map +7 -0
  84. package/dist/modules/integration_ksef_direct/workers/send-ksef-document.js +104 -0
  85. package/dist/modules/integration_ksef_direct/workers/send-ksef-document.js.map +7 -0
  86. package/dist/modules/integration_ksef_direct/workers/sync-received-documents.js +137 -0
  87. package/dist/modules/integration_ksef_direct/workers/sync-received-documents.js.map +7 -0
  88. package/dist/types/declarations.d.js +1 -0
  89. package/dist/types/declarations.d.js.map +7 -0
  90. package/package.json +98 -0
  91. package/src/index.ts +1 -0
  92. package/src/modules/integration_ksef_direct/__tests__/invoiceNumberFormat.test.ts +42 -0
  93. package/src/modules/integration_ksef_direct/__tests__/ksefFa2Xml.test.ts +407 -0
  94. package/src/modules/integration_ksef_direct/__tests__/ksefXmlParser.test.ts +230 -0
  95. package/src/modules/integration_ksef_direct/acl.ts +9 -0
  96. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents/[id].ts +94 -0
  97. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/documents.ts +111 -0
  98. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/health.ts +194 -0
  99. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents/[id].ts +88 -0
  100. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/received-documents.ts +119 -0
  101. package/src/modules/integration_ksef_direct/api/get/integration-ksef-direct/seller-info.ts +62 -0
  102. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents/[id]/send.ts +64 -0
  103. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/documents.ts +109 -0
  104. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/invoice-numbers.ts +40 -0
  105. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/fetch.ts +185 -0
  106. package/src/modules/integration_ksef_direct/api/post/integration-ksef-direct/received-documents/sync.ts +86 -0
  107. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.meta.ts +4 -0
  108. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/new/page.tsx +470 -0
  109. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/documents/page.tsx +233 -0
  110. package/src/modules/integration_ksef_direct/backend/integration-ksef-direct/received-documents/page.tsx +415 -0
  111. package/src/modules/integration_ksef_direct/backend/page.tsx +183 -0
  112. package/src/modules/integration_ksef_direct/commands/create-ksef-direct-document.ts +93 -0
  113. package/src/modules/integration_ksef_direct/commands/enqueue-ksef-direct-document.ts +57 -0
  114. package/src/modules/integration_ksef_direct/data/entities.ts +195 -0
  115. package/src/modules/integration_ksef_direct/data/validators.ts +115 -0
  116. package/src/modules/integration_ksef_direct/di.ts +9 -0
  117. package/src/modules/integration_ksef_direct/events.ts +18 -0
  118. package/src/modules/integration_ksef_direct/i18n/en.json +115 -0
  119. package/src/modules/integration_ksef_direct/i18n/pl.json +115 -0
  120. package/src/modules/integration_ksef_direct/index.ts +6 -0
  121. package/src/modules/integration_ksef_direct/integration.ts +54 -0
  122. package/src/modules/integration_ksef_direct/lib/health.ts +43 -0
  123. package/src/modules/integration_ksef_direct/lib/invoiceNumberFormat.ts +23 -0
  124. package/src/modules/integration_ksef_direct/lib/ksefClient.ts +668 -0
  125. package/src/modules/integration_ksef_direct/lib/ksefCrypto.ts +138 -0
  126. package/src/modules/integration_ksef_direct/lib/ksefFa2Xml.ts +147 -0
  127. package/src/modules/integration_ksef_direct/lib/ksefXmlParser.ts +97 -0
  128. package/src/modules/integration_ksef_direct/migrations/.snapshot-open-mercato.json +1028 -0
  129. package/src/modules/integration_ksef_direct/migrations/Migration20260519210000_integration_ksef_direct.ts +15 -0
  130. package/src/modules/integration_ksef_direct/migrations/Migration20260520120000_ksef_direct_documents.ts +17 -0
  131. package/src/modules/integration_ksef_direct/migrations/Migration20260520220000_ksef_direct_send_queue.ts +15 -0
  132. package/src/modules/integration_ksef_direct/migrations/Migration20260520230000_ksef_direct_seller_per_document.ts +17 -0
  133. package/src/modules/integration_ksef_direct/migrations/Migration20260521120000_ksef_direct_received_documents.ts +16 -0
  134. package/src/modules/integration_ksef_direct/migrations/Migration20260521130000_ksef_received_download_urls.ts +15 -0
  135. package/src/modules/integration_ksef_direct/setup.ts +9 -0
  136. package/src/modules/integration_ksef_direct/subscribers/auto-enqueue-ksef-document.ts +21 -0
  137. package/src/modules/integration_ksef_direct/workers/check-ksef-document-status.ts +129 -0
  138. package/src/modules/integration_ksef_direct/workers/send-ksef-document.ts +137 -0
  139. package/src/modules/integration_ksef_direct/workers/sync-received-documents.ts +171 -0
  140. package/src/types/declarations.d.ts +1 -0
@@ -0,0 +1,233 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { Plus, Send, RefreshCw } from 'lucide-react'
6
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
8
+ import type { ColumnDef } from '@tanstack/react-table'
9
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
10
+ import { Button } from '@open-mercato/ui/primitives/button'
11
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
13
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
14
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
15
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
16
+ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
17
+
18
+ const PAGE_SIZE = 50
19
+
20
+ const STATUS_VARIANT: Record<string, 'success' | 'error' | 'warning' | 'neutral'> = {
21
+ draft: 'neutral',
22
+ queued: 'warning',
23
+ sending: 'warning',
24
+ sent: 'success',
25
+ failed: 'error',
26
+ }
27
+
28
+ export const pageMetadata = {
29
+ features: ['integration_ksef_direct.documents.view'],
30
+ }
31
+
32
+ type DocumentRow = {
33
+ id: string
34
+ source: string
35
+ status: string
36
+ invoiceNumber: string
37
+ buyerNip: string
38
+ buyerName: string | null
39
+ issueDate: string
40
+ grossAmount: string
41
+ currency: string
42
+ ksefReferenceNumber: string | null
43
+ createdAt: string
44
+ }
45
+
46
+ type ApiResponse = {
47
+ items: DocumentRow[]
48
+ total: number
49
+ page: number
50
+ pageSize: number
51
+ }
52
+
53
+ export default function KsefDirectDocumentsPage() {
54
+ const t = useT()
55
+ const scopeVersion = useOrganizationScopeVersion()
56
+ const [rows, setRows] = React.useState<DocumentRow[]>([])
57
+ const [page, setPage] = React.useState(1)
58
+ const [total, setTotal] = React.useState(0)
59
+ const [totalPages, setTotalPages] = React.useState(1)
60
+ const [isLoading, setLoading] = React.useState(false)
61
+ const [sendingId, setSendingId] = React.useState<string | null>(null)
62
+
63
+ const { runMutation } = useGuardedMutation<{ entityType: string }>({
64
+ contextId: 'integration_ksef_direct:send-document',
65
+ })
66
+
67
+ const fetchData = React.useCallback(async () => {
68
+ setLoading(true)
69
+ try {
70
+ const result = await apiCall<ApiResponse>(
71
+ `/api/integration-ksef-direct/documents?page=${page}&pageSize=${PAGE_SIZE}`,
72
+ )
73
+ if (result.ok && result.result) {
74
+ setRows(result.result.items)
75
+ setTotal(result.result.total)
76
+ setTotalPages(Math.max(1, Math.ceil(result.result.total / PAGE_SIZE)))
77
+ } else {
78
+ setRows([])
79
+ setTotal(0)
80
+ setTotalPages(1)
81
+ }
82
+ } catch {
83
+ setRows([])
84
+ setTotal(0)
85
+ setTotalPages(1)
86
+ } finally {
87
+ setLoading(false)
88
+ }
89
+ }, [page])
90
+
91
+ React.useEffect(() => { void fetchData() }, [fetchData, scopeVersion])
92
+
93
+ useAppEvent('ksef_direct.document.*', () => { void fetchData() }, [fetchData])
94
+
95
+ const handleSend = React.useCallback(async (documentId: string) => {
96
+ setSendingId(documentId)
97
+ try {
98
+ await runMutation({
99
+ operation: async () => {
100
+ const result = await apiCall(`/api/integration-ksef-direct/documents/${documentId}/send`, {
101
+ method: 'POST',
102
+ })
103
+ if (!result.ok) {
104
+ const err = (result.result as Record<string, unknown>)?.error
105
+ throw new Error(typeof err === 'string' ? err : t('integration_ksef_direct.documents.errors.send_failed', 'Send failed'))
106
+ }
107
+ return result.result
108
+ },
109
+ context: { entityType: 'integration_ksef_direct.document' },
110
+ mutationPayload: { entityType: 'integration_ksef_direct.document' },
111
+ })
112
+ flash(t('integration_ksef_direct.documents.send_success', 'Document queued for sending'), 'success')
113
+ void fetchData()
114
+ } catch (err) {
115
+ flash(err instanceof Error ? err.message : t('integration_ksef_direct.documents.errors.send_failed', 'Send failed'), 'error')
116
+ } finally {
117
+ setSendingId(null)
118
+ }
119
+ }, [runMutation, t, fetchData])
120
+
121
+ const columns = React.useMemo<ColumnDef<DocumentRow, unknown>[]>(() => [
122
+ {
123
+ id: 'invoiceNumber',
124
+ accessorKey: 'invoiceNumber',
125
+ header: t('integration_ksef_direct.documents.columns.invoiceNumber', 'Invoice Number'),
126
+ cell: ({ row }) => <span className="font-medium">{row.original.invoiceNumber}</span>,
127
+ },
128
+ {
129
+ id: 'buyer',
130
+ header: t('integration_ksef_direct.documents.columns.buyer', 'Buyer'),
131
+ cell: ({ row }) => (
132
+ <span>
133
+ {row.original.buyerName
134
+ ? `${row.original.buyerName} (${row.original.buyerNip})`
135
+ : row.original.buyerNip}
136
+ </span>
137
+ ),
138
+ },
139
+ {
140
+ id: 'issueDate',
141
+ accessorKey: 'issueDate',
142
+ header: t('integration_ksef_direct.documents.columns.issueDate', 'Issue Date'),
143
+ cell: ({ row }) => new Date(row.original.issueDate).toLocaleDateString(),
144
+ },
145
+ {
146
+ id: 'grossAmount',
147
+ accessorKey: 'grossAmount',
148
+ header: t('integration_ksef_direct.documents.columns.grossAmount', 'Gross Amount'),
149
+ cell: ({ row }) => (
150
+ <span className="tabular-nums">
151
+ {parseFloat(row.original.grossAmount).toLocaleString('pl-PL', { minimumFractionDigits: 2 })}{' '}
152
+ {row.original.currency}
153
+ </span>
154
+ ),
155
+ },
156
+ {
157
+ id: 'status',
158
+ accessorKey: 'status',
159
+ header: t('integration_ksef_direct.documents.columns.status', 'Status'),
160
+ cell: ({ row }) => (
161
+ <StatusBadge variant={STATUS_VARIANT[row.original.status] ?? 'neutral'}>
162
+ {t(`integration_ksef_direct.documents.status.${row.original.status}`, row.original.status)}
163
+ </StatusBadge>
164
+ ),
165
+ },
166
+ {
167
+ id: 'ksefReferenceNumber',
168
+ header: t('integration_ksef_direct.documents.ksef_reference_number', 'KSeF Number'),
169
+ cell: ({ row }) => row.original.ksefReferenceNumber
170
+ ? <span className="font-mono text-sm">{row.original.ksefReferenceNumber}</span>
171
+ : null,
172
+ },
173
+ {
174
+ id: 'actions',
175
+ header: '',
176
+ cell: ({ row }) => {
177
+ const { id, status } = row.original
178
+ const isSending = sendingId === id
179
+ if (status === 'draft') {
180
+ return (
181
+ <Button
182
+ size="sm"
183
+ variant="ghost"
184
+ disabled={isSending}
185
+ onClick={() => void handleSend(id)}
186
+ aria-label={t('integration_ksef_direct.documents.send_button', 'Send to KSeF')}
187
+ >
188
+ <Send className="h-4 w-4 mr-1" />
189
+ {t('integration_ksef_direct.documents.send_button', 'Send to KSeF')}
190
+ </Button>
191
+ )
192
+ }
193
+ if (status === 'failed') {
194
+ return (
195
+ <Button
196
+ size="sm"
197
+ variant="ghost"
198
+ disabled={isSending}
199
+ onClick={() => void handleSend(id)}
200
+ aria-label={t('integration_ksef_direct.documents.retry_button', 'Retry send')}
201
+ >
202
+ <RefreshCw className="h-4 w-4 mr-1" />
203
+ {t('integration_ksef_direct.documents.retry_button', 'Retry')}
204
+ </Button>
205
+ )
206
+ }
207
+ return null
208
+ },
209
+ },
210
+ ], [t, sendingId, handleSend])
211
+
212
+ return (
213
+ <Page>
214
+ <PageBody>
215
+ <DataTable<DocumentRow>
216
+ columns={columns}
217
+ data={rows}
218
+ isLoading={isLoading}
219
+ actions={(
220
+ <Button asChild>
221
+ <Link href="/backend/integration-ksef-direct/documents/new">
222
+ <Plus className="h-4 w-4 mr-2" />
223
+ {t('integration_ksef_direct.documents.list.actions.new', 'New Document')}
224
+ </Link>
225
+ </Button>
226
+ )}
227
+ emptyState={t('integration_ksef_direct.documents.empty.title', 'No KSeF documents yet')}
228
+ pagination={{ page, pageSize: PAGE_SIZE, total, totalPages, onPageChange: setPage }}
229
+ />
230
+ </PageBody>
231
+ </Page>
232
+ )
233
+ }
@@ -0,0 +1,415 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Copy, Search } from 'lucide-react'
5
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
7
+ import type { ColumnDef } from '@tanstack/react-table'
8
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
9
+ import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { Input } from '@open-mercato/ui/primitives/input'
11
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@open-mercato/ui/primitives/dialog'
12
+ import { FormField } from '@open-mercato/ui/primitives/form-field'
13
+ import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
14
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
17
+
18
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
20
+
21
+ export const pageMetadata = {
22
+ requireAuth: true,
23
+ requireFeatures: ['integration_ksef_direct.received_documents.view'],
24
+ pageTitleKey: 'integration_ksef_direct.received_documents.title',
25
+ }
26
+
27
+ const PAGE_SIZE = 50
28
+
29
+ type ReceivedDocumentRow = {
30
+ id: string
31
+ ksefReferenceNumber: string
32
+ invoiceNumber: string | null
33
+ sellerNip: string | null
34
+ sellerName: string | null
35
+ issueDate: string | null
36
+ currency: string | null
37
+ grossAmount: string | null
38
+ status: string
39
+ errorMessage: string | null
40
+ syncedAt: string | null
41
+ createdAt: string
42
+ }
43
+
44
+ type ReceivedDocumentDetail = ReceivedDocumentRow & {
45
+ netAmount: string | null
46
+ vatAmount: string | null
47
+ rawXml: string | null
48
+ updatedAt: string
49
+ }
50
+
51
+ type ApiResponse = {
52
+ items: ReceivedDocumentRow[]
53
+ total: number
54
+ page: number
55
+ pageSize: number
56
+ }
57
+
58
+ const STATUS_VARIANT: Record<string, 'warning' | 'success' | 'error'> = {
59
+ pending_download: 'warning',
60
+ downloaded: 'success',
61
+ failed: 'error',
62
+ }
63
+
64
+ // ─── Detail Modal ────────────────────────────────────────────────────────────
65
+
66
+ function DetailModal({
67
+ documentId,
68
+ onClose,
69
+ }: {
70
+ documentId: string
71
+ onClose: () => void
72
+ }) {
73
+ const t = useT()
74
+ const [doc, setDoc] = React.useState<ReceivedDocumentDetail | null>(null)
75
+ const [loading, setLoading] = React.useState(true)
76
+ const [copied, setCopied] = React.useState(false)
77
+
78
+ React.useEffect(() => {
79
+ let cancelled = false
80
+ setLoading(true)
81
+ apiCall<ReceivedDocumentDetail>(`/api/integration-ksef-direct/received-documents/${documentId}`)
82
+ .then((res) => {
83
+ if (!cancelled && res.ok && res.result) setDoc(res.result)
84
+ })
85
+ .catch(() => {})
86
+ .finally(() => { if (!cancelled) setLoading(false) })
87
+ return () => { cancelled = true }
88
+ }, [documentId])
89
+
90
+ React.useEffect(() => {
91
+ const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
92
+ window.addEventListener('keydown', handler)
93
+ return () => window.removeEventListener('keydown', handler)
94
+ }, [onClose])
95
+
96
+ async function handleCopy() {
97
+ if (!doc?.rawXml) return
98
+ try {
99
+ await navigator.clipboard.writeText(doc.rawXml)
100
+ setCopied(true)
101
+ setTimeout(() => setCopied(false), 2000)
102
+ } catch {}
103
+ }
104
+
105
+ return (
106
+ <Dialog open onOpenChange={(open: boolean) => { if (!open) onClose() }}>
107
+ <DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
108
+ <DialogHeader>
109
+ <DialogTitle>
110
+ {doc?.invoiceNumber ?? doc?.ksefReferenceNumber ?? t('integration_ksef_direct.received_documents.title', 'Received Document')}
111
+ </DialogTitle>
112
+ </DialogHeader>
113
+
114
+ {loading && (
115
+ <p className="text-sm text-muted-foreground py-4">
116
+ {t('integration_ksef_direct.received_documents.loading', 'Loading...')}
117
+ </p>
118
+ )}
119
+
120
+ {doc && !loading && (
121
+ <div className="flex flex-col gap-4 overflow-y-auto">
122
+ <dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
123
+ <div>
124
+ <dt className="text-muted-foreground">{t('integration_ksef_direct.received_documents.column.ksef_reference', 'KSeF Ref.')}</dt>
125
+ <dd className="font-mono break-all">{doc.ksefReferenceNumber}</dd>
126
+ </div>
127
+ <div>
128
+ <dt className="text-muted-foreground">{t('integration_ksef_direct.received_documents.column.invoice_number', 'Invoice No.')}</dt>
129
+ <dd>{doc.invoiceNumber ?? '—'}</dd>
130
+ </div>
131
+ <div>
132
+ <dt className="text-muted-foreground">{t('integration_ksef_direct.received_documents.column.seller_nip', 'Seller NIP')}</dt>
133
+ <dd>{doc.sellerNip ?? '—'}</dd>
134
+ </div>
135
+ <div>
136
+ <dt className="text-muted-foreground">{t('integration_ksef_direct.received_documents.column.seller_name', 'Seller')}</dt>
137
+ <dd>{doc.sellerName ?? '—'}</dd>
138
+ </div>
139
+ <div>
140
+ <dt className="text-muted-foreground">{t('integration_ksef_direct.received_documents.column.issue_date', 'Issue Date')}</dt>
141
+ <dd>{doc.issueDate ?? '—'}</dd>
142
+ </div>
143
+ <div>
144
+ <dt className="text-muted-foreground">{t('integration_ksef_direct.received_documents.column.gross_amount', 'Gross Amount')}</dt>
145
+ <dd>
146
+ {doc.grossAmount != null
147
+ ? `${parseFloat(doc.grossAmount).toLocaleString('pl-PL', { minimumFractionDigits: 2 })} ${doc.currency ?? 'PLN'}`
148
+ : '—'}
149
+ </dd>
150
+ </div>
151
+ <div>
152
+ <dt className="text-muted-foreground">{t('integration_ksef_direct.received_documents.column.status', 'Status')}</dt>
153
+ <dd>
154
+ <StatusBadge variant={STATUS_VARIANT[doc.status] ?? 'warning'}>
155
+ {t(`integration_ksef_direct.received_documents.status.${doc.status}`, doc.status)}
156
+ </StatusBadge>
157
+ </dd>
158
+ </div>
159
+ {doc.errorMessage && (
160
+ <div className="col-span-2">
161
+ <dt className="text-muted-foreground">Error</dt>
162
+ <dd className="text-status-destructive-text">{doc.errorMessage}</dd>
163
+ </div>
164
+ )}
165
+ </dl>
166
+
167
+ {doc.rawXml && (
168
+ <div className="flex flex-col gap-1">
169
+ <div className="flex items-center justify-between">
170
+ <span className="text-sm font-medium">XML</span>
171
+ <Button size="sm" variant="ghost" onClick={handleCopy} aria-label="Copy XML">
172
+ <Copy className="size-4 mr-1" />
173
+ {copied
174
+ ? t('integration_ksef_direct.received_documents.xml_copied', 'Copied')
175
+ : t('integration_ksef_direct.received_documents.copy_xml', 'Copy XML')}
176
+ </Button>
177
+ </div>
178
+ <pre className="text-xs font-mono bg-muted rounded p-3 overflow-auto max-h-64 whitespace-pre-wrap break-all">
179
+ {doc.rawXml}
180
+ </pre>
181
+ </div>
182
+ )}
183
+ </div>
184
+ )}
185
+ </DialogContent>
186
+ </Dialog>
187
+ )
188
+ }
189
+
190
+ // ─── Fetch Dialog ─────────────────────────────────────────────────────────────
191
+
192
+ function FetchDialog({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
193
+ const t = useT()
194
+ const [reference, setReference] = React.useState('')
195
+ const [error, setError] = React.useState<string | null>(null)
196
+ const [isSubmitting, setSubmitting] = React.useState(false)
197
+
198
+ const { runMutation } = useGuardedMutation<{ entityType: string }>({
199
+ contextId: 'integration_ksef_direct:fetch-received-document',
200
+ })
201
+
202
+ React.useEffect(() => {
203
+ const handler = (e: KeyboardEvent) => {
204
+ if (e.key === 'Escape') onClose()
205
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') void handleSubmit()
206
+ }
207
+ window.addEventListener('keydown', handler)
208
+ return () => window.removeEventListener('keydown', handler)
209
+ })
210
+
211
+ async function handleSubmit() {
212
+ setError(null)
213
+ setSubmitting(true)
214
+ try {
215
+ await runMutation({
216
+ operation: async () => {
217
+ const result = await apiCall('/api/integration-ksef-direct/received-documents/fetch', {
218
+ method: 'POST',
219
+ body: JSON.stringify({ ksefReferenceNumber: reference }),
220
+ })
221
+ if (!result.ok) {
222
+ const body = result.result as Record<string, unknown>
223
+ throw new Error(typeof body?.error === 'string' ? body.error : 'Error')
224
+ }
225
+ return result.result
226
+ },
227
+ context: { entityType: 'integration_ksef_direct.received_document' },
228
+ mutationPayload: { entityType: 'integration_ksef_direct.received_document' },
229
+ })
230
+ flash(t('integration_ksef_direct.received_documents.fetch_success', 'Document fetched successfully.'), 'success')
231
+ onClose()
232
+ onSuccess()
233
+ } catch (err) {
234
+ setError(err instanceof Error ? err.message : 'Error')
235
+ } finally {
236
+ setSubmitting(false)
237
+ }
238
+ }
239
+
240
+ return (
241
+ <Dialog open onOpenChange={(open: boolean) => { if (!open) onClose() }}>
242
+ <DialogContent className="max-w-md">
243
+ <DialogHeader>
244
+ <DialogTitle>{t('integration_ksef_direct.received_documents.dialog_fetch.title', 'Fetch Document by KSeF Reference')}</DialogTitle>
245
+ </DialogHeader>
246
+
247
+ <div className="flex flex-col gap-4">
248
+ {error && (
249
+ <Alert variant="destructive">
250
+ <AlertDescription>{error}</AlertDescription>
251
+ </Alert>
252
+ )}
253
+ <FormField label={t('integration_ksef_direct.received_documents.column.ksef_reference', 'KSeF Reference Number')} required>
254
+ <Input
255
+ value={reference}
256
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setReference(e.target.value)}
257
+ placeholder="e.g. 1234567890..."
258
+ className="font-mono"
259
+ />
260
+ </FormField>
261
+ </div>
262
+
263
+ <DialogFooter>
264
+ <Button variant="outline" onClick={onClose} type="button">
265
+ {t('integration_ksef_direct.received_documents.cancel', 'Cancel')}
266
+ </Button>
267
+ <Button onClick={handleSubmit} disabled={isSubmitting || !reference.trim()} type="button">
268
+ {t('integration_ksef_direct.received_documents.dialog_fetch.submit', 'Fetch')}
269
+ </Button>
270
+ </DialogFooter>
271
+ </DialogContent>
272
+ </Dialog>
273
+ )
274
+ }
275
+
276
+ // ─── Main Page ────────────────────────────────────────────────────────────────
277
+
278
+ export default function ReceivedDocumentsPage() {
279
+ const t = useT()
280
+ const scopeVersion = useOrganizationScopeVersion()
281
+ const [rows, setRows] = React.useState<ReceivedDocumentRow[]>([])
282
+ const [page, setPage] = React.useState(1)
283
+ const [total, setTotal] = React.useState(0)
284
+ const [totalPages, setTotalPages] = React.useState(1)
285
+ const [isLoading, setLoading] = React.useState(false)
286
+ const [fetchDialogOpen, setFetchDialogOpen] = React.useState(false)
287
+ const [detailId, setDetailId] = React.useState<string | null>(null)
288
+
289
+ const fetchData = React.useCallback(async () => {
290
+ setLoading(true)
291
+ try {
292
+ const result = await apiCall<ApiResponse>(
293
+ `/api/integration-ksef-direct/received-documents?page=${page}&pageSize=${PAGE_SIZE}`,
294
+ )
295
+ if (result.ok && result.result) {
296
+ setRows(result.result.items)
297
+ setTotal(result.result.total)
298
+ setTotalPages(Math.max(1, Math.ceil(result.result.total / PAGE_SIZE)))
299
+ } else {
300
+ setRows([])
301
+ setTotal(0)
302
+ setTotalPages(1)
303
+ }
304
+ } catch {
305
+ setRows([])
306
+ setTotal(0)
307
+ setTotalPages(1)
308
+ } finally {
309
+ setLoading(false)
310
+ }
311
+ }, [page])
312
+
313
+ React.useEffect(() => { void fetchData() }, [fetchData, scopeVersion])
314
+
315
+ const columns = React.useMemo<ColumnDef<ReceivedDocumentRow, unknown>[]>(() => [
316
+ {
317
+ id: 'sellerNip',
318
+ accessorKey: 'sellerNip',
319
+ header: t('integration_ksef_direct.received_documents.column.seller_nip', 'Seller NIP'),
320
+ cell: ({ row }) => row.original.sellerNip ?? '—',
321
+ },
322
+ {
323
+ id: 'sellerName',
324
+ accessorKey: 'sellerName',
325
+ header: t('integration_ksef_direct.received_documents.column.seller_name', 'Seller'),
326
+ cell: ({ row }) => row.original.sellerName ?? '—',
327
+ },
328
+ {
329
+ id: 'invoiceNumber',
330
+ accessorKey: 'invoiceNumber',
331
+ header: t('integration_ksef_direct.received_documents.column.invoice_number', 'Invoice No.'),
332
+ cell: ({ row }) => row.original.invoiceNumber ?? '—',
333
+ },
334
+ {
335
+ id: 'issueDate',
336
+ accessorKey: 'issueDate',
337
+ header: t('integration_ksef_direct.received_documents.column.issue_date', 'Issue Date'),
338
+ cell: ({ row }) => row.original.issueDate ?? '—',
339
+ },
340
+ {
341
+ id: 'grossAmount',
342
+ header: t('integration_ksef_direct.received_documents.column.gross_amount', 'Gross Amount'),
343
+ cell: ({ row }) => row.original.grossAmount != null
344
+ ? (
345
+ <span className="tabular-nums">
346
+ {parseFloat(row.original.grossAmount).toLocaleString('pl-PL', { minimumFractionDigits: 2 })}{' '}
347
+ {row.original.currency ?? 'PLN'}
348
+ </span>
349
+ )
350
+ : '—',
351
+ },
352
+ {
353
+ id: 'status',
354
+ accessorKey: 'status',
355
+ header: t('integration_ksef_direct.received_documents.column.status', 'Status'),
356
+ cell: ({ row }) => (
357
+ <StatusBadge variant={STATUS_VARIANT[row.original.status] ?? 'warning'}>
358
+ {t(`integration_ksef_direct.received_documents.status.${row.original.status}`, row.original.status)}
359
+ </StatusBadge>
360
+ ),
361
+ },
362
+ {
363
+ id: 'ksefReferenceNumber',
364
+ accessorKey: 'ksefReferenceNumber',
365
+ header: t('integration_ksef_direct.received_documents.column.ksef_reference', 'KSeF Ref.'),
366
+ cell: ({ row }) => (
367
+ <span className="font-mono text-sm truncate max-w-48 block" title={row.original.ksefReferenceNumber}>
368
+ {row.original.ksefReferenceNumber}
369
+ </span>
370
+ ),
371
+ },
372
+ ], [t])
373
+
374
+ const toolbar = (
375
+ <Button
376
+ variant="outline"
377
+ size="sm"
378
+ onClick={() => setFetchDialogOpen(true)}
379
+ >
380
+ <Search className="size-4 mr-2" />
381
+ {t('integration_ksef_direct.received_documents.fetch_button', 'Fetch by reference')}
382
+ </Button>
383
+ )
384
+
385
+ return (
386
+ <Page>
387
+ <PageBody>
388
+ <DataTable<ReceivedDocumentRow>
389
+ entityId="ksef_direct_received_document"
390
+ columns={columns}
391
+ data={rows}
392
+ isLoading={isLoading}
393
+ actions={toolbar}
394
+ emptyState={t('integration_ksef_direct.received_documents.empty', 'No received documents found.')}
395
+ pagination={{ page, pageSize: PAGE_SIZE, total, totalPages, onPageChange: setPage }}
396
+ onRowClick={(row) => setDetailId(row.id)}
397
+ />
398
+ </PageBody>
399
+
400
+ {fetchDialogOpen && (
401
+ <FetchDialog
402
+ onClose={() => setFetchDialogOpen(false)}
403
+ onSuccess={() => void fetchData()}
404
+ />
405
+ )}
406
+
407
+ {detailId && (
408
+ <DetailModal
409
+ documentId={detailId}
410
+ onClose={() => setDetailId(null)}
411
+ />
412
+ )}
413
+ </Page>
414
+ )
415
+ }