@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,470 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { Plus, RefreshCw, Trash2 } from 'lucide-react'
6
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
+ import { SectionHeader } from '@open-mercato/ui/backend/SectionHeader'
8
+ import { FormField } from '@open-mercato/ui/primitives/form-field'
9
+ import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
11
+ import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
12
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
13
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
15
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
16
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
17
+
18
+ export const pageMetadata = { features: ['integration_ksef_direct.documents.create'], navHidden: true }
19
+
20
+ const VAT_RATES = ['0', '5', '8', '23', 'ZW', 'NP'] as const
21
+ const CURRENCIES = ['PLN', 'EUR', 'USD', 'GBP'] as const
22
+ const VAT_RATE_MULTIPLIERS: Record<string, number> = { '0': 0, '5': 5, '8': 8, '23': 23, ZW: 0, NP: 0 }
23
+
24
+ type LineItemState = {
25
+ description: string
26
+ quantity: string
27
+ unit: string
28
+ unitNetPrice: string
29
+ vatRate: string
30
+ }
31
+
32
+ type Totals = { net: number; vat: number; gross: number }
33
+
34
+ function round2(v: number) { return Math.round(v * 100) / 100 }
35
+
36
+ function computeTotals(items: LineItemState[]): Totals {
37
+ let net = 0, vat = 0
38
+ for (const item of items) {
39
+ const qty = parseFloat(item.quantity) || 0
40
+ const price = parseFloat(item.unitNetPrice) || 0
41
+ const lineNet = round2(qty * price)
42
+ const rate = VAT_RATE_MULTIPLIERS[item.vatRate] ?? 0
43
+ const lineVat = round2(lineNet * rate / 100)
44
+ net += lineNet
45
+ vat += lineVat
46
+ }
47
+ net = round2(net)
48
+ vat = round2(vat)
49
+ return { net, vat, gross: round2(net + vat) }
50
+ }
51
+
52
+ function emptyLine(): LineItemState {
53
+ return { description: '', quantity: '1', unit: 'szt', unitNetPrice: '0.00', vatRate: '23' }
54
+ }
55
+
56
+ export default function NewKsefDirectDocumentPage() {
57
+ const t = useT()
58
+ const router = useRouter()
59
+
60
+ const [buyerNip, setBuyerNip] = React.useState('')
61
+ const [buyerName, setBuyerName] = React.useState('')
62
+ const [invoiceNumber, setInvoiceNumber] = React.useState('')
63
+ const today = new Date().toISOString().split('T')[0]
64
+ const [issueDate, setIssueDate] = React.useState(today)
65
+ const [saleDate, setSaleDate] = React.useState(today)
66
+ const [currency, setCurrency] = React.useState('PLN')
67
+ const [lineItems, setLineItems] = React.useState<LineItemState[]>([emptyLine()])
68
+ const [notes, setNotes] = React.useState('')
69
+ const [sellerNip, setSellerNip] = React.useState<string | null>(null)
70
+ const [sellerName, setSellerName] = React.useState('')
71
+ const [isGeneratingNumber, setIsGeneratingNumber] = React.useState(false)
72
+ const [sellerAddressL1, setSellerAddressL1] = React.useState('')
73
+ const [sellerCity, setSellerCity] = React.useState('')
74
+ const [sellerCountry, setSellerCountry] = React.useState('PL')
75
+ const [submitError, setSubmitError] = React.useState<string | null>(null)
76
+ const [isMutating, setIsMutating] = React.useState(false)
77
+
78
+ const { runMutation } = useGuardedMutation<{ entityType: string }>({
79
+ contextId: 'integration_ksef_direct:new-document',
80
+ })
81
+
82
+ React.useEffect(() => {
83
+ apiCall('/api/integration-ksef-direct/seller-info')
84
+ .then((res) => {
85
+ if (!res.ok) return
86
+ const data = res.result as { sellerNip: string | null; sellerName: string | null }
87
+ if (data.sellerNip !== undefined) setSellerNip(data.sellerNip)
88
+ if (data.sellerName && !sellerName) setSellerName(data.sellerName)
89
+ })
90
+ .catch(() => {
91
+ flash(t('integration_ksef_direct.documents.form.seller_info_error', 'Failed to load seller info'), 'error')
92
+ })
93
+ // eslint-disable-next-line react-hooks/exhaustive-deps
94
+ }, [])
95
+
96
+ const totals = React.useMemo(() => computeTotals(lineItems), [lineItems])
97
+
98
+ React.useEffect(() => {
99
+ function onKeyDown(e: KeyboardEvent) {
100
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
101
+ e.preventDefault()
102
+ void handleSubmit()
103
+ }
104
+ if (e.key === 'Escape') router.back()
105
+ }
106
+ window.addEventListener('keydown', onKeyDown)
107
+ return () => window.removeEventListener('keydown', onKeyDown)
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
109
+ }, [router, buyerNip, buyerName, invoiceNumber, issueDate, saleDate, currency, lineItems, notes, sellerName, sellerAddressL1, sellerCity, sellerCountry])
110
+
111
+ function addLine() {
112
+ setLineItems((prev) => [...prev, emptyLine()])
113
+ }
114
+
115
+ function removeLine(index: number) {
116
+ if (lineItems.length <= 1) return
117
+ setLineItems((prev) => prev.filter((_, i) => i !== index))
118
+ }
119
+
120
+ function updateLine(index: number, field: keyof LineItemState, value: string) {
121
+ setLineItems((prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item))
122
+ }
123
+
124
+ async function handleGenerateNumber() {
125
+ setIsGeneratingNumber(true)
126
+ try {
127
+ const result = await apiCall('/api/integration-ksef-direct/invoice-numbers', { method: 'POST' })
128
+ if (result.ok) {
129
+ const data = result.result as { number: string }
130
+ setInvoiceNumber(data.number)
131
+ } else {
132
+ flash(t('integration_ksef_direct.documents.form.generate_number_error', 'Failed to generate invoice number'), 'error')
133
+ }
134
+ } catch {
135
+ flash(t('integration_ksef_direct.documents.form.generate_number_error', 'Failed to generate invoice number'), 'error')
136
+ } finally {
137
+ setIsGeneratingNumber(false)
138
+ }
139
+ }
140
+
141
+ async function handleSubmit() {
142
+ setSubmitError(null)
143
+ if (!sellerAddressL1.trim() || !sellerCity.trim()) {
144
+ setSubmitError(t('integration_ksef_direct.documents.form.error_seller_address_required', 'Seller street and city are required.'))
145
+ return
146
+ }
147
+ setIsMutating(true)
148
+ const body = {
149
+ buyerNip,
150
+ buyerName: buyerName || undefined,
151
+ invoiceNumber,
152
+ issueDate,
153
+ saleDate: saleDate || undefined,
154
+ currency,
155
+ lineItems: lineItems.map((item) => ({
156
+ description: item.description,
157
+ quantity: parseFloat(item.quantity) || 0,
158
+ unit: item.unit,
159
+ unitNetPrice: parseFloat(item.unitNetPrice) || 0,
160
+ vatRate: item.vatRate,
161
+ })),
162
+ notes: notes || undefined,
163
+ sellerName: sellerName || undefined,
164
+ sellerAddressL1,
165
+ sellerCity,
166
+ sellerCountry: sellerCountry || undefined,
167
+ }
168
+
169
+ try {
170
+ await runMutation({
171
+ operation: async () => {
172
+ const result = await apiCall('/api/integration-ksef-direct/documents', {
173
+ method: 'POST',
174
+ body: JSON.stringify(body),
175
+ headers: { 'Content-Type': 'application/json' },
176
+ })
177
+ if (!result.ok) {
178
+ const err = (result.result as Record<string, unknown>)?.error
179
+ throw new Error(typeof err === 'string' ? err : t('integration_ksef_direct.documents.form.error', 'Submission failed'))
180
+ }
181
+ return result.result
182
+ },
183
+ context: { entityType: 'integration_ksef_direct.document' },
184
+ mutationPayload: { entityType: 'integration_ksef_direct.document' },
185
+ })
186
+ flash(t('integration_ksef_direct.documents.form.success', 'Document created successfully.'), 'success')
187
+ router.push('/backend/integration-ksef-direct/documents')
188
+ } catch (err) {
189
+ setSubmitError(err instanceof Error ? err.message : t('integration_ksef_direct.documents.form.error', 'Submission failed'))
190
+ } finally {
191
+ setIsMutating(false)
192
+ }
193
+ }
194
+
195
+ return (
196
+ <Page>
197
+ <PageBody>
198
+ <h1 className="text-xl font-semibold mb-6">
199
+ {t('integration_ksef_direct.documents.form.title_new', 'New KSeF Document')}
200
+ </h1>
201
+
202
+ {submitError && (
203
+ <Alert variant="destructive" className="mb-6">
204
+ <AlertDescription>{submitError}</AlertDescription>
205
+ </Alert>
206
+ )}
207
+
208
+ <SectionHeader title={t('integration_ksef_direct.documents.form.section.buyer', 'Buyer')} />
209
+ <div className="grid grid-cols-2 gap-4 mb-6">
210
+ <FormField label={t('integration_ksef_direct.documents.form.buyer_nip', 'Buyer NIP')} required>
211
+ <input
212
+ type="text"
213
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
214
+ value={buyerNip}
215
+ onChange={(e) => setBuyerNip(e.target.value)}
216
+ placeholder="1234567890"
217
+ maxLength={10}
218
+ />
219
+ </FormField>
220
+ <FormField label={t('integration_ksef_direct.documents.form.buyer_name', 'Buyer Name')}>
221
+ <input
222
+ type="text"
223
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
224
+ value={buyerName}
225
+ onChange={(e) => setBuyerName(e.target.value)}
226
+ />
227
+ </FormField>
228
+ </div>
229
+
230
+ <SectionHeader title={t('integration_ksef_direct.documents.form.section.invoice', 'Invoice Details')} />
231
+ <div className="grid grid-cols-2 gap-4 mb-6">
232
+ <FormField label={t('integration_ksef_direct.documents.form.invoice_number', 'Invoice Number')} required>
233
+ <div className="flex gap-2 items-center">
234
+ <input
235
+ type="text"
236
+ className="flex-1 border border-input rounded-md px-3 py-2 text-sm"
237
+ value={invoiceNumber}
238
+ onChange={(e) => setInvoiceNumber(e.target.value)}
239
+ />
240
+ <Button
241
+ type="button"
242
+ variant="outline"
243
+ size="sm"
244
+ onClick={() => void handleGenerateNumber()}
245
+ disabled={isGeneratingNumber}
246
+ className="shrink-0"
247
+ >
248
+ {isGeneratingNumber
249
+ ? <><Spinner className="size-4 mr-2" />{t('integration_ksef_direct.documents.form.generating', 'Generating...')}</>
250
+ : <><RefreshCw className="size-4 mr-2" aria-hidden />{t('integration_ksef_direct.documents.form.generate_number', 'Generate number')}</>
251
+ }
252
+ </Button>
253
+ </div>
254
+ </FormField>
255
+ <FormField label={t('integration_ksef_direct.documents.form.currency', 'Currency')}>
256
+ <select
257
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
258
+ value={currency}
259
+ onChange={(e) => setCurrency(e.target.value)}
260
+ >
261
+ {CURRENCIES.map((c) => <option key={c} value={c}>{c}</option>)}
262
+ </select>
263
+ </FormField>
264
+ <FormField label={t('integration_ksef_direct.documents.form.issue_date', 'Issue Date')} required>
265
+ <input
266
+ type="date"
267
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
268
+ value={issueDate}
269
+ onChange={(e) => setIssueDate(e.target.value)}
270
+ />
271
+ </FormField>
272
+ <FormField label={t('integration_ksef_direct.documents.form.sale_date', 'Sale Date')}>
273
+ <input
274
+ type="date"
275
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
276
+ value={saleDate}
277
+ onChange={(e) => setSaleDate(e.target.value)}
278
+ />
279
+ </FormField>
280
+ </div>
281
+
282
+ <SectionHeader
283
+ title={`${t('integration_ksef_direct.documents.form.line_items', 'Line Items')} (${lineItems.length})`}
284
+ className="mt-6"
285
+ />
286
+ <div className="space-y-3 mb-4">
287
+ {lineItems.map((item, index) => (
288
+ <div key={index} className="grid grid-cols-12 gap-2 items-end border border-border rounded-md p-3">
289
+ <div className="col-span-4">
290
+ <FormField label={t('integration_ksef_direct.documents.form.line_items.description', 'Description')} required>
291
+ <input
292
+ type="text"
293
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
294
+ value={item.description}
295
+ onChange={(e) => updateLine(index, 'description', e.target.value)}
296
+ />
297
+ </FormField>
298
+ </div>
299
+ <div className="col-span-2">
300
+ <FormField label={t('integration_ksef_direct.documents.form.line_items.quantity', 'Qty')}>
301
+ <input
302
+ type="number"
303
+ min="0.001"
304
+ step="any"
305
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
306
+ value={item.quantity}
307
+ onChange={(e) => updateLine(index, 'quantity', e.target.value)}
308
+ />
309
+ </FormField>
310
+ </div>
311
+ <div className="col-span-1">
312
+ <FormField label={t('integration_ksef_direct.documents.form.line_items.unit', 'Unit')}>
313
+ <input
314
+ type="text"
315
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
316
+ value={item.unit}
317
+ onChange={(e) => updateLine(index, 'unit', e.target.value)}
318
+ />
319
+ </FormField>
320
+ </div>
321
+ <div className="col-span-2">
322
+ <FormField label={t('integration_ksef_direct.documents.form.line_items.unit_net_price', 'Net Price')}>
323
+ <input
324
+ type="number"
325
+ min="0"
326
+ step="0.01"
327
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
328
+ value={item.unitNetPrice}
329
+ onChange={(e) => updateLine(index, 'unitNetPrice', e.target.value)}
330
+ />
331
+ </FormField>
332
+ </div>
333
+ <div className="col-span-2">
334
+ <FormField label={t('integration_ksef_direct.documents.form.line_items.vat_rate', 'VAT')}>
335
+ <select
336
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
337
+ value={item.vatRate}
338
+ onChange={(e) => updateLine(index, 'vatRate', e.target.value)}
339
+ >
340
+ {VAT_RATES.map((r) => <option key={r} value={r}>{r}%</option>)}
341
+ </select>
342
+ </FormField>
343
+ </div>
344
+ <div className="col-span-1 flex justify-end pb-1">
345
+ <IconButton
346
+ type="button"
347
+ variant="ghost"
348
+ size="sm"
349
+ aria-label={t('integration_ksef_direct.documents.form.line_items.remove', 'Remove line')}
350
+ onClick={() => removeLine(index)}
351
+ disabled={lineItems.length <= 1}
352
+ >
353
+ <Trash2 className="size-4" />
354
+ </IconButton>
355
+ </div>
356
+ </div>
357
+ ))}
358
+ </div>
359
+ <Button type="button" variant="outline" size="sm" onClick={addLine} className="mb-6">
360
+ <Plus className="size-4 mr-2" aria-hidden />
361
+ {t('integration_ksef_direct.documents.form.line_items.add', 'Add Line')}
362
+ </Button>
363
+
364
+ <SectionHeader title={t('integration_ksef_direct.documents.form.section.summary', 'Summary')} />
365
+ <div className="grid grid-cols-3 gap-4 mb-6">
366
+ <FormField label={t('integration_ksef_direct.documents.form.net_amount', 'Net Amount')}>
367
+ <input
368
+ type="text"
369
+ readOnly
370
+ className="w-full border border-input rounded-md px-3 py-2 text-sm bg-muted"
371
+ value={`${totals.net.toFixed(2)} ${currency}`}
372
+ />
373
+ </FormField>
374
+ <FormField label={t('integration_ksef_direct.documents.form.vat_amount', 'VAT Amount')}>
375
+ <input
376
+ type="text"
377
+ readOnly
378
+ className="w-full border border-input rounded-md px-3 py-2 text-sm bg-muted"
379
+ value={`${totals.vat.toFixed(2)} ${currency}`}
380
+ />
381
+ </FormField>
382
+ <FormField label={t('integration_ksef_direct.documents.form.gross_amount', 'Gross Amount')}>
383
+ <input
384
+ type="text"
385
+ readOnly
386
+ className="w-full border border-input rounded-md px-3 py-2 text-sm bg-muted font-semibold"
387
+ value={`${totals.gross.toFixed(2)} ${currency}`}
388
+ />
389
+ </FormField>
390
+ </div>
391
+
392
+ <SectionHeader title={t('integration_ksef_direct.documents.form.section.seller', 'Seller')} className="mt-6" />
393
+ <div className="grid grid-cols-2 gap-4 mb-4">
394
+ <FormField label={t('integration_ksef_direct.documents.form.seller_nip', 'Seller NIP')}>
395
+ <input
396
+ type="text"
397
+ disabled
398
+ className="w-full border border-input rounded-md px-3 py-2 text-sm bg-muted text-muted-foreground cursor-not-allowed"
399
+ value={sellerNip ?? ''}
400
+ placeholder={sellerNip === null ? t('integration_ksef_direct.documents.form.seller_nip_placeholder', 'KSeF not configured') : ''}
401
+ />
402
+ </FormField>
403
+ <FormField label={t('integration_ksef_direct.documents.form.seller_name', 'Seller Name')} required>
404
+ <input
405
+ type="text"
406
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
407
+ value={sellerName}
408
+ onChange={(e) => setSellerName(e.target.value)}
409
+ maxLength={512}
410
+ />
411
+ </FormField>
412
+ </div>
413
+
414
+ <SectionHeader title={t('integration_ksef_direct.documents.form.section.seller_address', 'Seller Address')} className="mt-0" />
415
+ <div className="grid grid-cols-2 gap-4 mb-6">
416
+ <FormField label={t('integration_ksef_direct.documents.form.seller_address_l1', 'Street and number')} required>
417
+ <input
418
+ type="text"
419
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
420
+ value={sellerAddressL1}
421
+ onChange={(e) => setSellerAddressL1(e.target.value)}
422
+ maxLength={512}
423
+ />
424
+ </FormField>
425
+ <FormField label={t('integration_ksef_direct.documents.form.seller_city', 'City')} required>
426
+ <input
427
+ type="text"
428
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
429
+ value={sellerCity}
430
+ onChange={(e) => setSellerCity(e.target.value)}
431
+ maxLength={256}
432
+ />
433
+ </FormField>
434
+ <FormField label={t('integration_ksef_direct.documents.form.seller_country', 'Country')}>
435
+ <input
436
+ type="text"
437
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
438
+ value={sellerCountry}
439
+ onChange={(e) => setSellerCountry(e.target.value)}
440
+ maxLength={2}
441
+ placeholder="PL"
442
+ />
443
+ </FormField>
444
+ </div>
445
+
446
+ <FormField label={t('integration_ksef_direct.documents.form.notes', 'Notes')} className="mb-6">
447
+ <textarea
448
+ rows={3}
449
+ className="w-full border border-input rounded-md px-3 py-2 text-sm"
450
+ value={notes}
451
+ onChange={(e) => setNotes(e.target.value)}
452
+ />
453
+ </FormField>
454
+
455
+ <div className="flex gap-3">
456
+ <Button
457
+ type="button"
458
+ onClick={() => void handleSubmit()}
459
+ disabled={isMutating}
460
+ >
461
+ {t('integration_ksef_direct.documents.form.submit', 'Save Document')}
462
+ </Button>
463
+ <Button type="button" variant="outline" onClick={() => router.back()}>
464
+ {t('common.cancel', 'Cancel')}
465
+ </Button>
466
+ </div>
467
+ </PageBody>
468
+ </Page>
469
+ )
470
+ }