@spaceinvoices/react-ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/cli/dist/index.js +922 -0
- package/package.json +87 -0
- package/registry.json +600 -0
- package/spaceinvoices.schema.json +47 -0
- package/src/app.tsx +25 -0
- package/src/common/autocomplete.tsx +135 -0
- package/src/components/activities/activity-timeline.tsx +160 -0
- package/src/components/activities/index.ts +1 -0
- package/src/components/activities/locales/de.ts +30 -0
- package/src/components/activities/locales/sl.ts +30 -0
- package/src/components/advance-invoices/advance-invoices.hooks.ts +75 -0
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +702 -0
- package/src/components/advance-invoices/create/locales/de.ts +29 -0
- package/src/components/advance-invoices/create/locales/sl.ts +25 -0
- package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +74 -0
- package/src/components/advance-invoices/index.ts +5 -0
- package/src/components/advance-invoices/list/index.ts +3 -0
- package/src/components/advance-invoices/list/list-row-actions.tsx +119 -0
- package/src/components/advance-invoices/list/list-table.tsx +178 -0
- package/src/components/advance-invoices/list/locales/de.ts +32 -0
- package/src/components/advance-invoices/list/locales/sl.ts +32 -0
- package/src/components/advance-invoices/list/use-advance-invoice-download.ts +63 -0
- package/src/components/button-loader.tsx +11 -0
- package/src/components/combobox.tsx +96 -0
- package/src/components/company-registry/company-registry-autocomplete.tsx +151 -0
- package/src/components/company-registry/company-registry.hooks.ts +67 -0
- package/src/components/company-registry/index.ts +7 -0
- package/src/components/credit-notes/create/create-credit-note-form.tsx +332 -0
- package/src/components/credit-notes/create/index.ts +1 -0
- package/src/components/credit-notes/create/locales/de.ts +69 -0
- package/src/components/credit-notes/create/locales/sl.ts +67 -0
- package/src/components/credit-notes/credit-notes.hooks.ts +22 -0
- package/src/components/credit-notes/index.ts +10 -0
- package/src/components/credit-notes/list/index.ts +3 -0
- package/src/components/credit-notes/list/list-row-actions.tsx +116 -0
- package/src/components/credit-notes/list/list-table.tsx +183 -0
- package/src/components/credit-notes/list/locales/de.ts +33 -0
- package/src/components/credit-notes/list/locales/sl.ts +33 -0
- package/src/components/credit-notes/list/use-credit-note-download.ts +65 -0
- package/src/components/customers/create-customer-form/create-customer-form.tsx +134 -0
- package/src/components/customers/create-customer-form/locales/de.ts +20 -0
- package/src/components/customers/create-customer-form/locales/sl.ts +20 -0
- package/src/components/customers/customer-autocomplete.tsx +173 -0
- package/src/components/customers/customer-combobox.tsx +130 -0
- package/src/components/customers/customer-list-table/customer-list-row-actions.tsx +48 -0
- package/src/components/customers/customer-list-table/customer-list-table.tsx +124 -0
- package/src/components/customers/customer-list-table/index.ts +2 -0
- package/src/components/customers/customer-list-table/locales/de.ts +16 -0
- package/src/components/customers/customer-list-table/locales/sl.ts +16 -0
- package/src/components/customers/customers.hooks.test.ts +348 -0
- package/src/components/customers/customers.hooks.ts +57 -0
- package/src/components/customers/index.ts +5 -0
- package/src/components/dashboard/chart-empty-state.tsx +29 -0
- package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +80 -0
- package/src/components/dashboard/collection-rate-card/index.ts +4 -0
- package/src/components/dashboard/collection-rate-card/locales/sl.ts +3 -0
- package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +74 -0
- package/src/components/dashboard/index.ts +54 -0
- package/src/components/dashboard/invoice-status-chart/index.ts +4 -0
- package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +130 -0
- package/src/components/dashboard/invoice-status-chart/locales/sl.ts +9 -0
- package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +105 -0
- package/src/components/dashboard/loading-card.tsx +19 -0
- package/src/components/dashboard/payment-methods-chart/index.ts +4 -0
- package/src/components/dashboard/payment-methods-chart/locales/sl.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +152 -0
- package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +50 -0
- package/src/components/dashboard/payment-trend-chart/index.ts +4 -0
- package/src/components/dashboard/payment-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +137 -0
- package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +92 -0
- package/src/components/dashboard/revenue-card.tsx +49 -0
- package/src/components/dashboard/revenue-trend-chart/index.ts +4 -0
- package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +137 -0
- package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +93 -0
- package/src/components/dashboard/shared/index.ts +5 -0
- package/src/components/dashboard/shared/use-revenue-data.ts +160 -0
- package/src/components/dashboard/shared/use-stats-counts.ts +89 -0
- package/src/components/dashboard/shared/use-stats-query.ts +38 -0
- package/src/components/dashboard/stat-card.tsx +41 -0
- package/src/components/dashboard/tax-collected-card/index.ts +2 -0
- package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +77 -0
- package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +145 -0
- package/src/components/dashboard/top-customers-chart/index.ts +4 -0
- package/src/components/dashboard/top-customers-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +130 -0
- package/src/components/dashboard/top-customers-chart/use-top-customers.ts +72 -0
- package/src/components/documents/create/document-add-item-form.tsx +379 -0
- package/src/components/documents/create/document-add-item-tax-rate-field.tsx +120 -0
- package/src/components/documents/create/document-details-section.tsx +597 -0
- package/src/components/documents/create/document-items-section.tsx +133 -0
- package/src/components/documents/create/document-recipient-section.tsx +101 -0
- package/src/components/documents/create/form-types.ts +36 -0
- package/src/components/documents/create/index.ts +9 -0
- package/src/components/documents/create/live-preview.tsx +235 -0
- package/src/components/documents/create/mark-as-paid-section.tsx +82 -0
- package/src/components/documents/create/prepare-document-submission.test.ts +132 -0
- package/src/components/documents/create/prepare-document-submission.ts +187 -0
- package/src/components/documents/create/prepare-preview-data.test.ts +155 -0
- package/src/components/documents/create/prepare-preview-data.ts +16 -0
- package/src/components/documents/create/smart-code-insert-button.tsx +139 -0
- package/src/components/documents/create/use-document-customer-form.ts +161 -0
- package/src/components/documents/document-preview.tsx +13 -0
- package/src/components/documents/documents.hooks.ts +146 -0
- package/src/components/documents/index.ts +23 -0
- package/src/components/documents/shared/document-preview-display.tsx +172 -0
- package/src/components/documents/shared/index.ts +3 -0
- package/src/components/documents/shared/scaled-document-preview.tsx +70 -0
- package/src/components/documents/shared/use-a4-scaling.ts +62 -0
- package/src/components/documents/types.ts +61 -0
- package/src/components/documents/view/document-actions-bar.tsx +328 -0
- package/src/components/documents/view/document-details-card.tsx +179 -0
- package/src/components/documents/view/document-payments-list.tsx +256 -0
- package/src/components/documents/view/index.ts +4 -0
- package/src/components/documents/view/locales/de.ts +85 -0
- package/src/components/documents/view/locales/sl.ts +84 -0
- package/src/components/documents/view/use-document-download.ts +125 -0
- package/src/components/entities/create-entity-form.tsx +105 -0
- package/src/components/entities/entities.hooks.ts +50 -0
- package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +103 -0
- package/src/components/entities/entity-settings-form/entity-settings-form.tsx +1326 -0
- package/src/components/entities/entity-settings-form/image-upload-with-crop.tsx +222 -0
- package/src/components/entities/entity-settings-form/index.ts +2 -0
- package/src/components/entities/entity-settings-form/input-with-preview.tsx +190 -0
- package/src/components/entities/entity-settings-form/locales/de.ts +192 -0
- package/src/components/entities/entity-settings-form/locales/sl.ts +188 -0
- package/src/components/entities/furs-settings-form/furs-settings-form.tsx +410 -0
- package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +320 -0
- package/src/components/entities/furs-settings-form/index.ts +3 -0
- package/src/components/entities/furs-settings-form/locales/de.ts +233 -0
- package/src/components/entities/furs-settings-form/locales/en.ts +194 -0
- package/src/components/entities/furs-settings-form/locales/sl.ts +196 -0
- package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +242 -0
- package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +139 -0
- package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +252 -0
- package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +370 -0
- package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +420 -0
- package/src/components/entities/keys.ts +2 -0
- package/src/components/entities/settings/branding-settings-form.tsx +274 -0
- package/src/components/entities/settings/company-settings-form.tsx +256 -0
- package/src/components/entities/settings/defaults-settings-form.tsx +501 -0
- package/src/components/entities/settings/email-settings-form.tsx +288 -0
- package/src/components/entities/settings/eslog-settings-form.tsx +113 -0
- package/src/components/entities/settings/index.ts +8 -0
- package/src/components/entities/settings/number-format-settings-form.tsx +244 -0
- package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +164 -0
- package/src/components/entities/settings/pdf-template-selector/index.ts +2 -0
- package/src/components/entities/settings/pdf-template-selector/locales/de.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/locales/sl.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/pdf-template-cards.tsx +49 -0
- package/src/components/entities/settings/settings-footer.tsx +16 -0
- package/src/components/entities/settings/tax-rules-settings-form.tsx +346 -0
- package/src/components/estimates/create/create-estimate-form.tsx +384 -0
- package/src/components/estimates/create/locales/de.ts +64 -0
- package/src/components/estimates/create/locales/sl.ts +63 -0
- package/src/components/estimates/create/prepare-estimate-submission.ts +39 -0
- package/src/components/estimates/create/use-estimate-customer-form.ts +5 -0
- package/src/components/estimates/estimates.hooks.ts +15 -0
- package/src/components/estimates/index.ts +6 -0
- package/src/components/estimates/list/index.ts +3 -0
- package/src/components/estimates/list/list-row-actions.tsx +103 -0
- package/src/components/estimates/list/list-table.tsx +171 -0
- package/src/components/estimates/list/locales/de.ts +26 -0
- package/src/components/estimates/list/locales/sl.ts +26 -0
- package/src/components/estimates/list/use-estimate-download.ts +63 -0
- package/src/components/export/document-export-form.tsx +288 -0
- package/src/components/export/index.ts +2 -0
- package/src/components/form/form-input.tsx +89 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/invoices/create/create-invoice-form.tsx +852 -0
- package/src/components/invoices/create/eslog-validation.test.ts +242 -0
- package/src/components/invoices/create/eslog-validation.ts +208 -0
- package/src/components/invoices/create/locales/de.ts +118 -0
- package/src/components/invoices/create/locales/sl.ts +114 -0
- package/src/components/invoices/create/prepare-invoice-submission.test.ts +777 -0
- package/src/components/invoices/create/prepare-invoice-submission.ts +79 -0
- package/src/components/invoices/create/use-invoice-customer-form.ts +5 -0
- package/src/components/invoices/index.ts +9 -0
- package/src/components/invoices/invoices-furs.hooks.ts +28 -0
- package/src/components/invoices/invoices.hooks.ts +110 -0
- package/src/components/invoices/list/index.ts +3 -0
- package/src/components/invoices/list/list-row-actions.tsx +132 -0
- package/src/components/invoices/list/list-table.tsx +165 -0
- package/src/components/invoices/list/locales/de.ts +33 -0
- package/src/components/invoices/list/locales/sl.ts +33 -0
- package/src/components/invoices/list/use-invoice-download.ts +62 -0
- package/src/components/invoices/send-email-dialog/index.ts +1 -0
- package/src/components/invoices/send-email-dialog/locales/de.ts +18 -0
- package/src/components/invoices/send-email-dialog/locales/sl.ts +17 -0
- package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +289 -0
- package/src/components/invoices/send-email-dialog.tsx +2 -0
- package/src/components/invoices/shared/index.ts +2 -0
- package/src/components/invoices/shared/scaled-document-preview.tsx +32 -0
- package/src/components/invoices/shared/use-a4-scaling.tsx +39 -0
- package/src/components/invoices/view/eslog-info-display.tsx +160 -0
- package/src/components/invoices/view/furs-info-display.tsx +213 -0
- package/src/components/items/create-item-form/create-item-form.tsx +155 -0
- package/src/components/items/create-item-form/locales/de.ts +14 -0
- package/src/components/items/create-item-form/locales/en.ts +9 -0
- package/src/components/items/create-item-form/locales/sl.ts +14 -0
- package/src/components/items/item-combobox.tsx +147 -0
- package/src/components/items/item-list-table/item-list-header.tsx +33 -0
- package/src/components/items/item-list-table/item-list-row-actions.tsx +48 -0
- package/src/components/items/item-list-table/item-list-row.tsx +32 -0
- package/src/components/items/item-list-table/item-list-table.tsx +76 -0
- package/src/components/items/item-list-table/locales/de.ts +10 -0
- package/src/components/items/item-list-table/locales/en.ts +10 -0
- package/src/components/items/item-list-table/locales/sl.ts +10 -0
- package/src/components/items/items.hooks.ts +63 -0
- package/src/components/loading-spinner.tsx +24 -0
- package/src/components/payments/create-payment-form/create-payment-form.tsx +222 -0
- package/src/components/payments/create-payment-form/locales/de.ts +20 -0
- package/src/components/payments/create-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/edit-payment-form/edit-payment-form.tsx +230 -0
- package/src/components/payments/edit-payment-form/index.ts +1 -0
- package/src/components/payments/edit-payment-form/locales/de.ts +20 -0
- package/src/components/payments/edit-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/index.ts +4 -0
- package/src/components/payments/list/index.ts +2 -0
- package/src/components/payments/list/list-row-actions.tsx +98 -0
- package/src/components/payments/list/list-table.tsx +186 -0
- package/src/components/payments/list/locales/de.ts +19 -0
- package/src/components/payments/list/locales/sl.ts +19 -0
- package/src/components/payments/payments.hooks.ts +15 -0
- package/src/components/request-logs/index.ts +3 -0
- package/src/components/request-logs/request-log-detail.tsx +242 -0
- package/src/components/request-logs/request-log-list-table.tsx +266 -0
- package/src/components/request-logs/request-logs-page.tsx +10 -0
- package/src/components/table/README.md +410 -0
- package/src/components/table/data-table.tsx +251 -0
- package/src/components/table/date-cell.tsx +35 -0
- package/src/components/table/filter-bar.tsx +114 -0
- package/src/components/table/filter-panel.tsx +407 -0
- package/src/components/table/hooks/use-table-fetch.ts +17 -0
- package/src/components/table/hooks/use-table-query.ts +36 -0
- package/src/components/table/hooks/use-table-state.ts +293 -0
- package/src/components/table/index.ts +35 -0
- package/src/components/table/search-input.tsx +85 -0
- package/src/components/table/sortable-header.tsx +56 -0
- package/src/components/table/table-empty-state.tsx +40 -0
- package/src/components/table/table-no-results.tsx +41 -0
- package/src/components/table/table-pagination.tsx +42 -0
- package/src/components/table/table-skeleton.tsx +54 -0
- package/src/components/table/types.ts +136 -0
- package/src/components/tax-reports/index.ts +1 -0
- package/src/components/tax-reports/kir-export-form.tsx +172 -0
- package/src/components/taxes/create-tax-form/create-tax-form.tsx +112 -0
- package/src/components/taxes/create-tax-form/locales/de.ts +8 -0
- package/src/components/taxes/create-tax-form/locales/en.ts +7 -0
- package/src/components/taxes/create-tax-form/locales/sl.ts +8 -0
- package/src/components/taxes/tax-list-table/locales/de.ts +11 -0
- package/src/components/taxes/tax-list-table/locales/en.ts +10 -0
- package/src/components/taxes/tax-list-table/locales/sl.ts +11 -0
- package/src/components/taxes/tax-list-table/tax-list-header.tsx +29 -0
- package/src/components/taxes/tax-list-table/tax-list-row-actions.tsx +43 -0
- package/src/components/taxes/tax-list-table/tax-list-row.tsx +46 -0
- package/src/components/taxes/tax-list-table/tax-list-table.tsx +59 -0
- package/src/components/taxes/taxes.hooks.ts +35 -0
- package/src/components/ui/alert-dialog.tsx +61 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +132 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +213 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/chart.tsx +380 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +56 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/dialog.tsx +187 -0
- package/src/components/ui/drawer.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/form.tsx +166 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/loading-spinner.tsx +16 -0
- package/src/components/ui/popover.tsx +108 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/select.tsx +200 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +145 -0
- package/src/components/ui/sidebar.tsx +771 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +60 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/sticky-form-footer.tsx +55 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +101 -0
- package/src/components/ui/tabs.tsx +80 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +89 -0
- package/src/components/wl-subscription/index.ts +2 -0
- package/src/components/wl-subscription/locked-feature.tsx +173 -0
- package/src/components/wl-subscription/upgrade-modal.tsx +209 -0
- package/src/frontend.tsx +28 -0
- package/src/generate-schemas.ts +265 -0
- package/src/generated/schemas/advanceinvoice.ts +177 -0
- package/src/generated/schemas/creditnote.ts +187 -0
- package/src/generated/schemas/customer.ts +29 -0
- package/src/generated/schemas/entity.ts +252 -0
- package/src/generated/schemas/estimate.ts +159 -0
- package/src/generated/schemas/furssettings.ts +25 -0
- package/src/generated/schemas/index.ts +24 -0
- package/src/generated/schemas/invoice.ts +167 -0
- package/src/generated/schemas/item.ts +38 -0
- package/src/generated/schemas/payment.ts +44 -0
- package/src/generated/schemas/previewadvanceinvoice_body.ts +354 -0
- package/src/generated/schemas/previewestimate_body.ts +309 -0
- package/src/generated/schemas/registerfursmovablepremise_body.ts +22 -0
- package/src/generated/schemas/registerfursrealestatepremise_body.ts +32 -0
- package/src/generated/schemas/renderdocument_body.ts +594 -0
- package/src/generated/schemas/sendemail_body.ts +26 -0
- package/src/generated/schemas/startpdfexport_body.ts +20 -0
- package/src/generated/schemas/tax.ts +48 -0
- package/src/generated/schemas/uploadfile_body.ts +23 -0
- package/src/generated/schemas/uploadfurscertificate_body.ts +20 -0
- package/src/generated/schemas/userfurssettings.ts +19 -0
- package/src/hooks/create-resource-hooks.test.ts +483 -0
- package/src/hooks/create-resource-hooks.ts +300 -0
- package/src/hooks/use-debounce.ts +12 -0
- package/src/hooks/use-duplicate-document.ts +185 -0
- package/src/hooks/use-media-query.tsx +19 -0
- package/src/hooks/use-mobile.ts +39 -0
- package/src/hooks/use-next-document-number.ts +57 -0
- package/src/hooks/use-resource-mutation.ts +118 -0
- package/src/hooks/use-vies-check.ts +130 -0
- package/src/index.css +11 -0
- package/src/index.html +13 -0
- package/src/index.tsx +12 -0
- package/src/lib/auth.ts +4 -0
- package/src/lib/browser-cookies.ts +70 -0
- package/src/lib/constants.ts +287 -0
- package/src/lib/cookies.ts +36 -0
- package/src/lib/schemas/advance-invoice.ts +43 -0
- package/src/lib/schemas/credit-note.ts +32 -0
- package/src/lib/schemas/estimate.ts +31 -0
- package/src/lib/schemas/index.ts +18 -0
- package/src/lib/schemas/invoice.ts +43 -0
- package/src/lib/schemas/shared.ts +79 -0
- package/src/lib/translation.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/entities-context.tsx +41 -0
- package/src/providers/entities-provider.tsx +201 -0
- package/src/providers/form-footer-context.tsx +72 -0
- package/src/providers/sdk-provider.tsx +164 -0
- package/src/providers/white-label-provider.tsx +91 -0
- package/src/providers/wl-subscription-provider.tsx +277 -0
- package/src/utils/string-helpers.ts +111 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
2
|
+
import type { CreateInvoiceRequest, Invoice } from "@spaceinvoices/js-sdk";
|
|
3
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { AlertCircle, Check, FileCode2, X } from "lucide-react";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import type { Resolver } from "react-hook-form";
|
|
8
|
+
import { useForm, useWatch } from "react-hook-form";
|
|
9
|
+
import type { z } from "zod";
|
|
10
|
+
import { Alert, AlertDescription, AlertTitle } from "@/ui/components/ui/alert";
|
|
11
|
+
import { Button } from "@/ui/components/ui/button";
|
|
12
|
+
import { Form } from "@/ui/components/ui/form";
|
|
13
|
+
import { Skeleton } from "@/ui/components/ui/skeleton";
|
|
14
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/ui/components/ui/tooltip";
|
|
15
|
+
import { createInvoiceSchema } from "@/ui/generated/schemas";
|
|
16
|
+
import { useViesCheck } from "@/ui/hooks/use-vies-check";
|
|
17
|
+
import type { ComponentTranslationProps } from "@/ui/lib/translation";
|
|
18
|
+
import { createTranslation } from "@/ui/lib/translation";
|
|
19
|
+
import { cn } from "@/ui/lib/utils";
|
|
20
|
+
import { useEntities } from "@/ui/providers/entities-context";
|
|
21
|
+
import { useFormFooterRegistration } from "@/ui/providers/form-footer-context";
|
|
22
|
+
import { CUSTOMERS_CACHE_KEY } from "../../customers/customers.hooks";
|
|
23
|
+
import {
|
|
24
|
+
DocumentDetailsSection,
|
|
25
|
+
DocumentNoteField,
|
|
26
|
+
DocumentPaymentTermsField,
|
|
27
|
+
} from "../../documents/create/document-details-section";
|
|
28
|
+
import { DocumentItemsSection, type PriceModesMap } from "../../documents/create/document-items-section";
|
|
29
|
+
import { DocumentRecipientSection } from "../../documents/create/document-recipient-section";
|
|
30
|
+
import { MarkAsPaidSection } from "../../documents/create/mark-as-paid-section";
|
|
31
|
+
import type { DocumentTypes } from "../../documents/types";
|
|
32
|
+
import { useFursPremises, useFursSettings } from "../../entities/furs-settings-form/furs-settings.hooks";
|
|
33
|
+
import {
|
|
34
|
+
getLastUsedFursCombo,
|
|
35
|
+
setLastUsedFursCombo,
|
|
36
|
+
useCreateInvoice,
|
|
37
|
+
useNextInvoiceNumber,
|
|
38
|
+
useUpdateInvoice,
|
|
39
|
+
} from "../invoices.hooks";
|
|
40
|
+
import { getEntityErrors, getFormFieldErrors, validateEslogForm } from "./eslog-validation";
|
|
41
|
+
import de from "./locales/de";
|
|
42
|
+
import sl from "./locales/sl";
|
|
43
|
+
import { prepareInvoiceSubmission } from "./prepare-invoice-submission";
|
|
44
|
+
import { useInvoiceCustomerForm } from "./use-invoice-customer-form";
|
|
45
|
+
|
|
46
|
+
const translations = {
|
|
47
|
+
sl,
|
|
48
|
+
de,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
// Form values: extend schema with local-only fields (number is for display, not sent to API)
|
|
52
|
+
type CreateInvoiceFormValues = z.infer<typeof createInvoiceSchema> & {
|
|
53
|
+
number?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Preview payload extends request with display-only fields */
|
|
57
|
+
type InvoicePreviewPayload = Partial<CreateInvoiceRequest> & { number?: string };
|
|
58
|
+
|
|
59
|
+
type DocumentAddFormProps = {
|
|
60
|
+
type: DocumentTypes;
|
|
61
|
+
entityId: string;
|
|
62
|
+
onSuccess?: (data: Invoice) => void;
|
|
63
|
+
onError?: (error: unknown) => void;
|
|
64
|
+
onChange?: (data: InvoicePreviewPayload) => void;
|
|
65
|
+
onAddNewTax?: () => void;
|
|
66
|
+
onHeaderActionChange?: (action: ReactNode) => void;
|
|
67
|
+
/** Initial values for form fields (used for document duplication or editing) */
|
|
68
|
+
initialValues?: Partial<CreateInvoiceRequest>;
|
|
69
|
+
/** Mode: create (default) or edit */
|
|
70
|
+
mode?: "create" | "edit";
|
|
71
|
+
/** Document ID for edit mode */
|
|
72
|
+
documentId?: string;
|
|
73
|
+
} & ComponentTranslationProps;
|
|
74
|
+
|
|
75
|
+
export default function CreateInvoiceForm({
|
|
76
|
+
type: _type,
|
|
77
|
+
entityId,
|
|
78
|
+
onSuccess,
|
|
79
|
+
onError,
|
|
80
|
+
onChange,
|
|
81
|
+
onAddNewTax,
|
|
82
|
+
onHeaderActionChange,
|
|
83
|
+
initialValues,
|
|
84
|
+
mode = "create",
|
|
85
|
+
documentId,
|
|
86
|
+
t: translateProp,
|
|
87
|
+
namespace,
|
|
88
|
+
locale,
|
|
89
|
+
}: DocumentAddFormProps) {
|
|
90
|
+
const t = createTranslation({
|
|
91
|
+
t: translateProp,
|
|
92
|
+
namespace,
|
|
93
|
+
locale,
|
|
94
|
+
translations,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const isEditMode = mode === "edit";
|
|
98
|
+
const { activeEntity } = useEntities();
|
|
99
|
+
const queryClient = useQueryClient();
|
|
100
|
+
|
|
101
|
+
// Get default invoice note and payment terms from entity settings
|
|
102
|
+
const defaultInvoiceNote = (activeEntity?.settings as any)?.default_invoice_note || "";
|
|
103
|
+
const defaultPaymentTerms = (activeEntity?.settings as any)?.default_invoice_payment_terms || "";
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// FURS Settings & Premises
|
|
107
|
+
// ============================================================================
|
|
108
|
+
const { data: fursSettings, isLoading: isFursSettingsLoading } = useFursSettings(entityId);
|
|
109
|
+
const { data: fursPremises, isLoading: isFursPremisesLoading } = useFursPremises(entityId, {
|
|
110
|
+
enabled: fursSettings?.enabled === true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Loading state for FURS - don't render form until we know if FURS is active
|
|
114
|
+
const isFursLoading = isFursSettingsLoading || (fursSettings?.enabled && isFursPremisesLoading);
|
|
115
|
+
|
|
116
|
+
// Check if FURS is enabled and has active premises
|
|
117
|
+
const isFursEnabled = fursSettings?.enabled === true;
|
|
118
|
+
const activePremises = useMemo(() => fursPremises?.filter((p) => p.is_active) || [], [fursPremises]);
|
|
119
|
+
const hasFursPremises = activePremises.length > 0;
|
|
120
|
+
|
|
121
|
+
// FURS premise/device selection state
|
|
122
|
+
const [selectedPremiseName, setSelectedPremiseName] = useState<string | undefined>();
|
|
123
|
+
const [selectedDeviceName, setSelectedDeviceName] = useState<string | undefined>();
|
|
124
|
+
const [skipFiscalization, setSkipFiscalization] = useState(false);
|
|
125
|
+
|
|
126
|
+
// UI-only state (not part of API schema)
|
|
127
|
+
const [markAsPaid, setMarkAsPaid] = useState(false);
|
|
128
|
+
const [paymentType, setPaymentType] = useState("bank_transfer");
|
|
129
|
+
const [isDraftPending, setIsDraftPending] = useState(false);
|
|
130
|
+
|
|
131
|
+
// Service date type state (single date or range)
|
|
132
|
+
const [serviceDateType, setServiceDateType] = useState<"single" | "range">("single");
|
|
133
|
+
|
|
134
|
+
// Price modes per item (gross vs net) - collected from component state at submit
|
|
135
|
+
// Initialize from initialValues for duplicated documents
|
|
136
|
+
const initialPriceModes = useMemo(() => {
|
|
137
|
+
if (!initialValues?.items) return {};
|
|
138
|
+
return initialValues.items.reduce((acc, item, index) => {
|
|
139
|
+
acc[index] = item.gross_price != null;
|
|
140
|
+
return acc;
|
|
141
|
+
}, {} as PriceModesMap);
|
|
142
|
+
}, [initialValues?.items]);
|
|
143
|
+
const priceModesRef = useRef<PriceModesMap>(initialPriceModes);
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// e-SLOG Settings (Slovenian e-Invoice)
|
|
147
|
+
// ============================================================================
|
|
148
|
+
const isSloenianEntity = activeEntity?.country_code === "SI";
|
|
149
|
+
const entityEslogEnabled = !!(activeEntity?.settings as any)?.eslog_validation_enabled;
|
|
150
|
+
const isEslogAvailable = isSloenianEntity && entityEslogEnabled;
|
|
151
|
+
|
|
152
|
+
// e-SLOG validation state - defaults to entity setting
|
|
153
|
+
const [eslogValidationEnabled, setEslogValidationEnabled] = useState<boolean | undefined>(undefined);
|
|
154
|
+
// e-SLOG entity-level errors (require settings update, can't be fixed in form)
|
|
155
|
+
const [eslogEntityErrors, setEslogEntityErrors] = useState<Array<{ field: string; message: string }>>([]);
|
|
156
|
+
|
|
157
|
+
// Initialize e-SLOG state from entity settings
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (isEslogAvailable && eslogValidationEnabled === undefined) {
|
|
160
|
+
setEslogValidationEnabled(true);
|
|
161
|
+
}
|
|
162
|
+
}, [isEslogAvailable, eslogValidationEnabled]);
|
|
163
|
+
|
|
164
|
+
// Clear entity errors when eslog validation is disabled
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!eslogValidationEnabled) {
|
|
167
|
+
setEslogEntityErrors([]);
|
|
168
|
+
}
|
|
169
|
+
}, [eslogValidationEnabled]);
|
|
170
|
+
|
|
171
|
+
// Get active devices for selected premise
|
|
172
|
+
const activeDevices = useMemo(() => {
|
|
173
|
+
if (!selectedPremiseName) return [];
|
|
174
|
+
const premise = activePremises.find((p) => p.business_premise_name === selectedPremiseName);
|
|
175
|
+
return premise?.Devices?.filter((d) => d.is_active) || [];
|
|
176
|
+
}, [activePremises, selectedPremiseName]);
|
|
177
|
+
|
|
178
|
+
// Initialize FURS selection from localStorage or first active combo
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!isFursEnabled || !hasFursPremises || selectedPremiseName) return;
|
|
181
|
+
|
|
182
|
+
const lastUsed = getLastUsedFursCombo(entityId);
|
|
183
|
+
if (lastUsed) {
|
|
184
|
+
// Verify the last-used combo is still valid (premise/device still exist and active)
|
|
185
|
+
const premise = activePremises.find((p) => p.business_premise_name === lastUsed.business_premise_name);
|
|
186
|
+
const device = premise?.Devices?.find(
|
|
187
|
+
(d) => d.electronic_device_name === lastUsed.electronic_device_name && d.is_active,
|
|
188
|
+
);
|
|
189
|
+
if (premise && device) {
|
|
190
|
+
setSelectedPremiseName(lastUsed.business_premise_name);
|
|
191
|
+
setSelectedDeviceName(lastUsed.electronic_device_name);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fall back to first active premise/device
|
|
197
|
+
const firstPremise = activePremises[0];
|
|
198
|
+
const firstDevice = firstPremise?.Devices?.find((d) => d.is_active);
|
|
199
|
+
if (firstPremise && firstDevice) {
|
|
200
|
+
setSelectedPremiseName(firstPremise.business_premise_name);
|
|
201
|
+
setSelectedDeviceName(firstDevice.electronic_device_name);
|
|
202
|
+
}
|
|
203
|
+
}, [isFursEnabled, hasFursPremises, activePremises, entityId, selectedPremiseName]);
|
|
204
|
+
|
|
205
|
+
// When premise changes, select first active device
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!selectedPremiseName) return;
|
|
208
|
+
const premise = activePremises.find((p) => p.business_premise_name === selectedPremiseName);
|
|
209
|
+
const firstDevice = premise?.Devices?.find((d) => d.is_active);
|
|
210
|
+
if (firstDevice && selectedDeviceName !== firstDevice.electronic_device_name) {
|
|
211
|
+
// Only update if the current device is not in this premise
|
|
212
|
+
const currentDeviceInPremise = premise?.Devices?.find(
|
|
213
|
+
(d) => d.electronic_device_name === selectedDeviceName && d.is_active,
|
|
214
|
+
);
|
|
215
|
+
if (!currentDeviceInPremise) {
|
|
216
|
+
setSelectedDeviceName(firstDevice.electronic_device_name);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}, [selectedPremiseName, activePremises, selectedDeviceName]);
|
|
220
|
+
|
|
221
|
+
const form = useForm<CreateInvoiceFormValues>({
|
|
222
|
+
// Cast resolver to accept extended form type (includes UI-only fields)
|
|
223
|
+
resolver: zodResolver(createInvoiceSchema) as Resolver<CreateInvoiceFormValues>,
|
|
224
|
+
defaultValues: {
|
|
225
|
+
number: initialValues?.number || "", // Edit mode uses initialValues, create mode uses useNextInvoiceNumber
|
|
226
|
+
date: initialValues?.date || new Date().toISOString(),
|
|
227
|
+
customer_id: initialValues?.customer_id ?? undefined,
|
|
228
|
+
// Cast customer to form schema type (API type may have additional fields)
|
|
229
|
+
customer: (initialValues?.customer as CreateInvoiceFormValues["customer"]) ?? undefined,
|
|
230
|
+
items: initialValues?.items?.length
|
|
231
|
+
? initialValues.items.map((item) => ({
|
|
232
|
+
name: item.name || "",
|
|
233
|
+
description: item.description || "",
|
|
234
|
+
quantity: item.quantity ?? 1,
|
|
235
|
+
// Use gross_price if set, otherwise use price
|
|
236
|
+
price: item.gross_price ?? item.price,
|
|
237
|
+
taxes: item.taxes || [],
|
|
238
|
+
}))
|
|
239
|
+
: [
|
|
240
|
+
{
|
|
241
|
+
name: "",
|
|
242
|
+
description: "",
|
|
243
|
+
quantity: 1,
|
|
244
|
+
price: undefined,
|
|
245
|
+
taxes: [],
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
currency_code: initialValues?.currency_code || activeEntity?.currency_code || "EUR",
|
|
249
|
+
note: initialValues?.note ?? defaultInvoiceNote,
|
|
250
|
+
payment_terms: initialValues?.payment_terms ?? defaultPaymentTerms,
|
|
251
|
+
date_service: new Date().toISOString(),
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Skip fiscalization is only allowed for bank transfers or unpaid invoices
|
|
256
|
+
const canSkipFiscalization = !markAsPaid || paymentType === "bank_transfer";
|
|
257
|
+
|
|
258
|
+
// Auto-disable skip when it becomes invalid (e.g., user changes payment type to cash)
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!canSkipFiscalization && skipFiscalization) {
|
|
261
|
+
setSkipFiscalization(false);
|
|
262
|
+
}
|
|
263
|
+
}, [canSkipFiscalization, skipFiscalization]);
|
|
264
|
+
|
|
265
|
+
// Clear date_service_to when switching from range to single
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (serviceDateType === "single") {
|
|
268
|
+
form.setValue("date_service_to", undefined);
|
|
269
|
+
}
|
|
270
|
+
}, [serviceDateType, form]);
|
|
271
|
+
|
|
272
|
+
// Check if FURS selection is ready (needed to prevent number flashing)
|
|
273
|
+
// Selection is ready when: FURS not enabled, OR no premises, OR we have a valid selection
|
|
274
|
+
const isFursSelectionReady = !isFursEnabled || !hasFursPremises || (!!selectedPremiseName && !!selectedDeviceName);
|
|
275
|
+
|
|
276
|
+
// FURS is "active" for this invoice if enabled and we have a valid selection (and not skipped)
|
|
277
|
+
const isFursActive =
|
|
278
|
+
isFursEnabled && hasFursPremises && selectedPremiseName && selectedDeviceName && !skipFiscalization;
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// Next Invoice Number Preview
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Wait for FURS selection to be ready before querying to prevent number flashing
|
|
284
|
+
// Skip in edit mode - we use the existing document number
|
|
285
|
+
const { data: nextNumberData, isLoading: isNextNumberLoading } = useNextInvoiceNumber(entityId, {
|
|
286
|
+
business_premise_name: isFursActive ? selectedPremiseName : undefined,
|
|
287
|
+
electronic_device_name: isFursActive ? selectedDeviceName : undefined,
|
|
288
|
+
enabled: !!entityId && !isFursLoading && isFursSelectionReady && !isEditMode,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Overall loading state - wait until we have FURS data, selection ready, and next number (only in create mode)
|
|
292
|
+
const isFormDataLoading = isEditMode
|
|
293
|
+
? false // In edit mode, don't wait for next number
|
|
294
|
+
: isFursLoading || !isFursSelectionReady || isNextNumberLoading;
|
|
295
|
+
|
|
296
|
+
// Update header action with FURS and e-SLOG toggle buttons
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!onHeaderActionChange) return;
|
|
299
|
+
|
|
300
|
+
// Don't set header action while loading or in edit mode (FURS/e-SLOG not editable)
|
|
301
|
+
if (isFursLoading || isEditMode) {
|
|
302
|
+
onHeaderActionChange(null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const showFursToggle = isFursEnabled && hasFursPremises;
|
|
307
|
+
const showEslogToggle = isEslogAvailable;
|
|
308
|
+
|
|
309
|
+
if (showFursToggle || showEslogToggle) {
|
|
310
|
+
const isFursChecked = !skipFiscalization;
|
|
311
|
+
const isEslogChecked = eslogValidationEnabled === true;
|
|
312
|
+
|
|
313
|
+
onHeaderActionChange(
|
|
314
|
+
<div className="flex items-center gap-2">
|
|
315
|
+
{/* e-SLOG toggle */}
|
|
316
|
+
{showEslogToggle && (
|
|
317
|
+
<TooltipProvider>
|
|
318
|
+
<Tooltip>
|
|
319
|
+
<TooltipTrigger asChild>
|
|
320
|
+
<Button
|
|
321
|
+
type="button"
|
|
322
|
+
variant={isEslogChecked ? "outline" : "ghost"}
|
|
323
|
+
size="sm"
|
|
324
|
+
className={cn("h-8 cursor-pointer gap-2", !isEslogChecked && "text-muted-foreground")}
|
|
325
|
+
onClick={() => setEslogValidationEnabled(!eslogValidationEnabled)}
|
|
326
|
+
>
|
|
327
|
+
<div
|
|
328
|
+
className={cn(
|
|
329
|
+
"flex size-4 items-center justify-center rounded border",
|
|
330
|
+
isEslogChecked
|
|
331
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
332
|
+
: "border-muted-foreground bg-background text-muted-foreground",
|
|
333
|
+
)}
|
|
334
|
+
>
|
|
335
|
+
{isEslogChecked ? <Check className="size-3" /> : <FileCode2 className="size-3" />}
|
|
336
|
+
</div>
|
|
337
|
+
<span>{t("e-SLOG")}</span>
|
|
338
|
+
</Button>
|
|
339
|
+
</TooltipTrigger>
|
|
340
|
+
<TooltipContent side="bottom" className="max-w-xs">
|
|
341
|
+
{isEslogChecked
|
|
342
|
+
? t("Click to skip e-SLOG validation for this invoice")
|
|
343
|
+
: t("Click to enable e-SLOG validation")}
|
|
344
|
+
</TooltipContent>
|
|
345
|
+
</Tooltip>
|
|
346
|
+
</TooltipProvider>
|
|
347
|
+
)}
|
|
348
|
+
|
|
349
|
+
{/* FURS toggle */}
|
|
350
|
+
{showFursToggle && (
|
|
351
|
+
<TooltipProvider>
|
|
352
|
+
<Tooltip>
|
|
353
|
+
<TooltipTrigger asChild>
|
|
354
|
+
<Button
|
|
355
|
+
type="button"
|
|
356
|
+
variant={isFursChecked ? "outline" : "ghost"}
|
|
357
|
+
size="sm"
|
|
358
|
+
className={cn(
|
|
359
|
+
"h-8 cursor-pointer gap-2",
|
|
360
|
+
!canSkipFiscalization && "cursor-not-allowed opacity-50",
|
|
361
|
+
!isFursChecked && "text-destructive hover:text-destructive",
|
|
362
|
+
)}
|
|
363
|
+
onClick={() => canSkipFiscalization && setSkipFiscalization(!skipFiscalization)}
|
|
364
|
+
>
|
|
365
|
+
<div
|
|
366
|
+
className={cn(
|
|
367
|
+
"flex size-4 items-center justify-center rounded border",
|
|
368
|
+
isFursChecked
|
|
369
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
370
|
+
: "border-destructive bg-destructive text-destructive-foreground",
|
|
371
|
+
)}
|
|
372
|
+
>
|
|
373
|
+
{isFursChecked ? <Check className="size-3" /> : <X className="size-3" />}
|
|
374
|
+
</div>
|
|
375
|
+
<span>{t("Fiscally verify")}</span>
|
|
376
|
+
</Button>
|
|
377
|
+
</TooltipTrigger>
|
|
378
|
+
<TooltipContent side="bottom" className="max-w-xs">
|
|
379
|
+
{canSkipFiscalization
|
|
380
|
+
? isFursChecked
|
|
381
|
+
? t("Click to skip fiscalization for this invoice")
|
|
382
|
+
: t("Click to enable fiscalization")
|
|
383
|
+
: t("Cannot skip fiscalization for cash payments")}
|
|
384
|
+
</TooltipContent>
|
|
385
|
+
</Tooltip>
|
|
386
|
+
</TooltipProvider>
|
|
387
|
+
)}
|
|
388
|
+
</div>,
|
|
389
|
+
);
|
|
390
|
+
} else {
|
|
391
|
+
onHeaderActionChange(null);
|
|
392
|
+
}
|
|
393
|
+
}, [
|
|
394
|
+
isFursLoading,
|
|
395
|
+
isFursEnabled,
|
|
396
|
+
hasFursPremises,
|
|
397
|
+
skipFiscalization,
|
|
398
|
+
canSkipFiscalization,
|
|
399
|
+
isEslogAvailable,
|
|
400
|
+
eslogValidationEnabled,
|
|
401
|
+
isEditMode,
|
|
402
|
+
onHeaderActionChange,
|
|
403
|
+
t,
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
// Pre-fill invoice number from preview
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
if (nextNumberData?.number) {
|
|
409
|
+
form.setValue("number", nextNumberData.number);
|
|
410
|
+
}
|
|
411
|
+
}, [nextNumberData?.number, form]);
|
|
412
|
+
|
|
413
|
+
// Watch specific fields for VIES check (stable references)
|
|
414
|
+
const customerCountry = useWatch({ control: form.control, name: "customer.country" });
|
|
415
|
+
const customerCountryCode = useWatch({ control: form.control, name: "customer.country_code" });
|
|
416
|
+
const customerTaxNumber = useWatch({ control: form.control, name: "customer.tax_number" });
|
|
417
|
+
|
|
418
|
+
// Watch fields needed for document note/payment terms preview
|
|
419
|
+
const watchedNumber = useWatch({ control: form.control, name: "number" });
|
|
420
|
+
const watchedDate = useWatch({ control: form.control, name: "date" });
|
|
421
|
+
const watchedDateDue = useWatch({ control: form.control, name: "date_due" });
|
|
422
|
+
const watchedCurrencyCode = useWatch({ control: form.control, name: "currency_code" });
|
|
423
|
+
const watchedCustomer = useWatch({ control: form.control, name: "customer" });
|
|
424
|
+
|
|
425
|
+
// ============================================================================
|
|
426
|
+
// VIES Check - determine if reverse charge applies
|
|
427
|
+
// ============================================================================
|
|
428
|
+
const { reverseChargeApplies, warning: viesWarning } = useViesCheck({
|
|
429
|
+
issuerCountryCode: activeEntity?.country_code,
|
|
430
|
+
isTaxSubject: activeEntity?.is_tax_subject ?? true,
|
|
431
|
+
customerCountry,
|
|
432
|
+
customerCountryCode,
|
|
433
|
+
customerTaxNumber,
|
|
434
|
+
enabled: !!activeEntity,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Extract customer management logic into a custom hook
|
|
438
|
+
const {
|
|
439
|
+
originalCustomer,
|
|
440
|
+
showCustomerForm,
|
|
441
|
+
shouldFocusName,
|
|
442
|
+
selectedCustomerId,
|
|
443
|
+
initialCustomerName,
|
|
444
|
+
handleCustomerSelect,
|
|
445
|
+
handleCustomerClear,
|
|
446
|
+
} = useInvoiceCustomerForm(form as any);
|
|
447
|
+
|
|
448
|
+
const { mutate: createInvoice, isPending: isCreatePending } = useCreateInvoice({
|
|
449
|
+
entityId,
|
|
450
|
+
onSuccess: (data) => {
|
|
451
|
+
// Save FURS combo to localStorage on successful creation
|
|
452
|
+
if (isFursActive && selectedPremiseName && selectedDeviceName) {
|
|
453
|
+
setLastUsedFursCombo(entityId, {
|
|
454
|
+
business_premise_name: selectedPremiseName,
|
|
455
|
+
electronic_device_name: selectedDeviceName,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
// Invalidate customers cache when a customer was created/linked
|
|
459
|
+
// This ensures the new customer appears in autocomplete for future documents
|
|
460
|
+
if (data.customer_id) {
|
|
461
|
+
queryClient.invalidateQueries({ queryKey: [CUSTOMERS_CACHE_KEY] });
|
|
462
|
+
}
|
|
463
|
+
onSuccess?.(data);
|
|
464
|
+
},
|
|
465
|
+
onError,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const { mutate: updateInvoice, isPending: isUpdatePending } = useUpdateInvoice({
|
|
469
|
+
entityId,
|
|
470
|
+
onSuccess: (data) => {
|
|
471
|
+
// Invalidate customers cache when a customer was created/linked
|
|
472
|
+
if (data.customer_id) {
|
|
473
|
+
queryClient.invalidateQueries({ queryKey: [CUSTOMERS_CACHE_KEY] });
|
|
474
|
+
}
|
|
475
|
+
// Invalidate document queries to refresh the view
|
|
476
|
+
queryClient.invalidateQueries({ queryKey: ["documents", "invoice", documentId] });
|
|
477
|
+
onSuccess?.(data);
|
|
478
|
+
},
|
|
479
|
+
onError,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const isPending = isCreatePending || isUpdatePending;
|
|
483
|
+
|
|
484
|
+
// Shared submit logic for both regular save and save as draft
|
|
485
|
+
const submitInvoice = useCallback(
|
|
486
|
+
(values: CreateInvoiceFormValues, isDraft: boolean) => {
|
|
487
|
+
// Skip e-SLOG validation for drafts and edit mode
|
|
488
|
+
if (!isDraft && !isEditMode && eslogValidationEnabled) {
|
|
489
|
+
const validationErrors = validateEslogForm(values, activeEntity);
|
|
490
|
+
|
|
491
|
+
if (validationErrors.length > 0) {
|
|
492
|
+
const entityErrors = getEntityErrors(validationErrors);
|
|
493
|
+
const formErrors = getFormFieldErrors(validationErrors);
|
|
494
|
+
setEslogEntityErrors(entityErrors);
|
|
495
|
+
for (const error of formErrors) {
|
|
496
|
+
form.setError(error.field as any, {
|
|
497
|
+
type: "eslog",
|
|
498
|
+
message: error.message,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
setEslogEntityErrors([]);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Build FURS options (skip for drafts and edit mode)
|
|
507
|
+
const fursOptions =
|
|
508
|
+
!isDraft && !isEditMode && isFursEnabled
|
|
509
|
+
? skipFiscalization
|
|
510
|
+
? { skip: true }
|
|
511
|
+
: selectedPremiseName && selectedDeviceName
|
|
512
|
+
? { business_premise_name: selectedPremiseName, electronic_device_name: selectedDeviceName }
|
|
513
|
+
: undefined
|
|
514
|
+
: undefined;
|
|
515
|
+
|
|
516
|
+
// Build e-SLOG options (skip for drafts and edit mode)
|
|
517
|
+
const eslogOptions =
|
|
518
|
+
!isDraft && !isEditMode && isEslogAvailable
|
|
519
|
+
? { validation_enabled: eslogValidationEnabled === true }
|
|
520
|
+
: undefined;
|
|
521
|
+
|
|
522
|
+
const payload = prepareInvoiceSubmission(values, {
|
|
523
|
+
originalCustomer,
|
|
524
|
+
wasCustomerFormShown: showCustomerForm,
|
|
525
|
+
markAsPaid: isDraft || isEditMode ? false : markAsPaid,
|
|
526
|
+
paymentType,
|
|
527
|
+
furs: fursOptions,
|
|
528
|
+
eslog: eslogOptions,
|
|
529
|
+
priceModes: priceModesRef.current,
|
|
530
|
+
isDraft,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (isEditMode && documentId) {
|
|
534
|
+
// In edit mode, use updateInvoice
|
|
535
|
+
// Remove number from payload as it's not editable
|
|
536
|
+
const { number, ...updatePayload } = payload;
|
|
537
|
+
updateInvoice({ id: documentId, data: updatePayload });
|
|
538
|
+
} else {
|
|
539
|
+
createInvoice(payload);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
[
|
|
543
|
+
activeEntity,
|
|
544
|
+
createInvoice,
|
|
545
|
+
updateInvoice,
|
|
546
|
+
documentId,
|
|
547
|
+
eslogValidationEnabled,
|
|
548
|
+
form,
|
|
549
|
+
isEditMode,
|
|
550
|
+
isEslogAvailable,
|
|
551
|
+
isFursEnabled,
|
|
552
|
+
markAsPaid,
|
|
553
|
+
originalCustomer,
|
|
554
|
+
paymentType,
|
|
555
|
+
selectedDeviceName,
|
|
556
|
+
selectedPremiseName,
|
|
557
|
+
showCustomerForm,
|
|
558
|
+
skipFiscalization,
|
|
559
|
+
],
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// Handle save as draft - triggers form validation then submits with isDraft=true
|
|
563
|
+
const handleSaveAsDraft = useCallback(async () => {
|
|
564
|
+
setIsDraftPending(true);
|
|
565
|
+
try {
|
|
566
|
+
const isValid = await form.trigger();
|
|
567
|
+
if (isValid) {
|
|
568
|
+
const values = form.getValues();
|
|
569
|
+
submitInvoice(values, true);
|
|
570
|
+
}
|
|
571
|
+
} finally {
|
|
572
|
+
setIsDraftPending(false);
|
|
573
|
+
}
|
|
574
|
+
}, [form, submitInvoice]);
|
|
575
|
+
|
|
576
|
+
// Memoize secondary action to prevent infinite loops in useFormFooterRegistration
|
|
577
|
+
// Don't show "Save as Draft" in edit mode
|
|
578
|
+
const draftLabel = t("Save as Draft");
|
|
579
|
+
const saveLabel = isEditMode ? t("Update") : t("Save");
|
|
580
|
+
const secondaryAction = useMemo(
|
|
581
|
+
() =>
|
|
582
|
+
isEditMode
|
|
583
|
+
? undefined
|
|
584
|
+
: {
|
|
585
|
+
label: draftLabel,
|
|
586
|
+
onClick: handleSaveAsDraft,
|
|
587
|
+
isPending: isDraftPending,
|
|
588
|
+
},
|
|
589
|
+
[draftLabel, handleSaveAsDraft, isDraftPending, isEditMode],
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// Watch isDirty to get stable reference
|
|
593
|
+
const isDirty = form.formState.isDirty;
|
|
594
|
+
|
|
595
|
+
useFormFooterRegistration({
|
|
596
|
+
formId: "create-invoice-form",
|
|
597
|
+
isPending,
|
|
598
|
+
isDirty,
|
|
599
|
+
label: saveLabel,
|
|
600
|
+
secondaryAction,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Track if initial setup has been done
|
|
604
|
+
const initialSetupDoneRef = useRef(false);
|
|
605
|
+
|
|
606
|
+
// Set default note and payment terms from entity settings when entity data is available
|
|
607
|
+
// This handles the case where activeEntity loads asynchronously
|
|
608
|
+
useEffect(() => {
|
|
609
|
+
if (initialSetupDoneRef.current) return;
|
|
610
|
+
if (!activeEntity) return;
|
|
611
|
+
|
|
612
|
+
const entityDefaultNote = (activeEntity.settings as any)?.default_invoice_note;
|
|
613
|
+
if (entityDefaultNote && !form.getValues("note")) {
|
|
614
|
+
form.setValue("note", entityDefaultNote);
|
|
615
|
+
}
|
|
616
|
+
const entityDefaultPaymentTerms = (activeEntity.settings as any)?.default_invoice_payment_terms;
|
|
617
|
+
if (entityDefaultPaymentTerms && !form.getValues("payment_terms")) {
|
|
618
|
+
form.setValue("payment_terms", entityDefaultPaymentTerms);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Auto-add tax field for tax subject entities
|
|
622
|
+
if (activeEntity.is_tax_subject) {
|
|
623
|
+
const items = form.getValues("items") || [];
|
|
624
|
+
if (items.length > 0 && (!items[0].taxes || items[0].taxes.length === 0)) {
|
|
625
|
+
form.setValue("items.0.taxes", [{ tax_id: undefined }]);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
initialSetupDoneRef.current = true;
|
|
630
|
+
}, [activeEntity, form]);
|
|
631
|
+
|
|
632
|
+
// Use form.watch subscription for onChange callback (avoids re-render loops)
|
|
633
|
+
const prevPayloadRef = useRef<string>("");
|
|
634
|
+
|
|
635
|
+
useEffect(() => {
|
|
636
|
+
if (!onChange) return;
|
|
637
|
+
|
|
638
|
+
const buildPayload = (formValues: any): InvoicePreviewPayload => {
|
|
639
|
+
const currentItems = formValues.items || [];
|
|
640
|
+
const transformedItems = currentItems.map((item: any, index: number) => {
|
|
641
|
+
const { price, ...rest } = item;
|
|
642
|
+
const isGross = priceModesRef.current[index] ?? false;
|
|
643
|
+
return isGross ? { ...rest, gross_price: price } : { ...rest, price };
|
|
644
|
+
});
|
|
645
|
+
return {
|
|
646
|
+
number: formValues.number,
|
|
647
|
+
date: formValues.date,
|
|
648
|
+
customer_id: formValues.customer_id,
|
|
649
|
+
customer: formValues.customer,
|
|
650
|
+
items: transformedItems,
|
|
651
|
+
currency_code: formValues.currency_code,
|
|
652
|
+
note: formValues.note,
|
|
653
|
+
payment_terms: formValues.payment_terms,
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Initial call
|
|
658
|
+
const initialPayload = buildPayload(form.getValues());
|
|
659
|
+
prevPayloadRef.current = JSON.stringify(initialPayload);
|
|
660
|
+
onChange(initialPayload);
|
|
661
|
+
|
|
662
|
+
// Subscribe to changes
|
|
663
|
+
const subscription = form.watch((formValues) => {
|
|
664
|
+
const payload = buildPayload(formValues);
|
|
665
|
+
const payloadStr = JSON.stringify(payload);
|
|
666
|
+
if (payloadStr !== prevPayloadRef.current) {
|
|
667
|
+
prevPayloadRef.current = payloadStr;
|
|
668
|
+
onChange(payload);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
return () => subscription.unsubscribe();
|
|
673
|
+
}, [onChange, form]);
|
|
674
|
+
|
|
675
|
+
const onSubmit = (values: CreateInvoiceFormValues) => {
|
|
676
|
+
submitInvoice(values, false);
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// Show skeleton while loading FURS data and next number
|
|
680
|
+
if (isFormDataLoading) {
|
|
681
|
+
return (
|
|
682
|
+
<div className="space-y-8">
|
|
683
|
+
{/* Recipient + Details columns */}
|
|
684
|
+
<div className="flex w-full flex-col md:flex-row md:gap-6">
|
|
685
|
+
{/* Recipient section skeleton */}
|
|
686
|
+
<div className="flex-1 space-y-4">
|
|
687
|
+
<Skeleton className="h-7 w-24" /> {/* "Recipient" title */}
|
|
688
|
+
<Skeleton className="h-10 w-full" /> {/* Customer autocomplete */}
|
|
689
|
+
</div>
|
|
690
|
+
{/* Details section skeleton */}
|
|
691
|
+
<div className="flex-1 space-y-4">
|
|
692
|
+
<Skeleton className="h-7 w-20" /> {/* "Details" title */}
|
|
693
|
+
<Skeleton className="h-5 w-16" /> {/* "Number *" label */}
|
|
694
|
+
<Skeleton className="h-10 w-full" /> {/* Number field */}
|
|
695
|
+
<Skeleton className="h-5 w-12" /> {/* "Date *" label */}
|
|
696
|
+
<Skeleton className="h-10 w-full" /> {/* Date picker */}
|
|
697
|
+
<Skeleton className="h-5 w-16" /> {/* "Due Date" label */}
|
|
698
|
+
<Skeleton className="h-10 w-full" /> {/* Due date picker */}
|
|
699
|
+
<Skeleton className="h-5 w-20" /> {/* "Currency *" label */}
|
|
700
|
+
<Skeleton className="h-10 w-full" /> {/* Currency select */}
|
|
701
|
+
{/* Mark as paid section */}
|
|
702
|
+
<div className="space-y-3 rounded-md border p-4">
|
|
703
|
+
<div className="flex items-center gap-3">
|
|
704
|
+
<Skeleton className="h-4 w-4 rounded" /> {/* Checkbox */}
|
|
705
|
+
<Skeleton className="h-5 w-28" /> {/* "Mark as Paid" */}
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
{/* Items section skeleton */}
|
|
712
|
+
<div className="space-y-4">
|
|
713
|
+
<Skeleton className="h-7 w-16" /> {/* "Items" title */}
|
|
714
|
+
<div className="space-y-4 rounded-lg border p-4">
|
|
715
|
+
<Skeleton className="h-10 w-full" /> {/* Item name */}
|
|
716
|
+
<div className="flex gap-4">
|
|
717
|
+
<Skeleton className="h-10 w-24" /> {/* Quantity */}
|
|
718
|
+
<Skeleton className="h-10 flex-1" /> {/* Price */}
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
<Skeleton className="h-9 w-24" /> {/* Add item button */}
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
{/* Note field skeleton */}
|
|
725
|
+
<div className="space-y-2">
|
|
726
|
+
<Skeleton className="h-5 w-12" /> {/* "Note" label */}
|
|
727
|
+
<Skeleton className="h-24 w-full" /> {/* Textarea */}
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
{/* Save button skeleton */}
|
|
731
|
+
<Skeleton className="h-10 w-24" />
|
|
732
|
+
</div>
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return (
|
|
737
|
+
<Form {...form}>
|
|
738
|
+
<form id="create-invoice-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
|
739
|
+
{/* e-SLOG entity-level validation errors */}
|
|
740
|
+
{eslogEntityErrors.length > 0 && (
|
|
741
|
+
<Alert variant="destructive">
|
|
742
|
+
<AlertCircle className="h-4 w-4" />
|
|
743
|
+
<AlertTitle>{t("e-SLOG Validation Failed")}</AlertTitle>
|
|
744
|
+
<AlertDescription>
|
|
745
|
+
<p className="mb-2">{t("The following entity settings need to be updated:")}</p>
|
|
746
|
+
<ul className="list-disc space-y-1 pl-4">
|
|
747
|
+
{eslogEntityErrors.map((error) => (
|
|
748
|
+
<li key={error.field} className="text-sm">
|
|
749
|
+
{error.message}
|
|
750
|
+
</li>
|
|
751
|
+
))}
|
|
752
|
+
</ul>
|
|
753
|
+
</AlertDescription>
|
|
754
|
+
</Alert>
|
|
755
|
+
)}
|
|
756
|
+
|
|
757
|
+
<div className="flex w-full flex-col md:flex-row md:gap-6">
|
|
758
|
+
<DocumentRecipientSection
|
|
759
|
+
control={form.control}
|
|
760
|
+
entityId={entityId}
|
|
761
|
+
onCustomerSelect={handleCustomerSelect}
|
|
762
|
+
onCustomerClear={handleCustomerClear}
|
|
763
|
+
showCustomerForm={showCustomerForm}
|
|
764
|
+
shouldFocusName={shouldFocusName}
|
|
765
|
+
selectedCustomerId={selectedCustomerId}
|
|
766
|
+
initialCustomerName={initialCustomerName}
|
|
767
|
+
t={t}
|
|
768
|
+
/>
|
|
769
|
+
<DocumentDetailsSection
|
|
770
|
+
control={form.control}
|
|
771
|
+
documentType={_type}
|
|
772
|
+
t={t}
|
|
773
|
+
fursInline={
|
|
774
|
+
// Hide FURS selector in edit mode - fiscalization is set at creation only
|
|
775
|
+
!isEditMode && isFursEnabled && hasFursPremises
|
|
776
|
+
? {
|
|
777
|
+
premises: activePremises.map((p) => ({ id: p.id, business_premise_name: p.business_premise_name })),
|
|
778
|
+
devices: activeDevices.map((d) => ({ id: d.id, electronic_device_name: d.electronic_device_name })),
|
|
779
|
+
selectedPremise: selectedPremiseName,
|
|
780
|
+
selectedDevice: selectedDeviceName,
|
|
781
|
+
onPremiseChange: setSelectedPremiseName,
|
|
782
|
+
onDeviceChange: setSelectedDeviceName,
|
|
783
|
+
isSkipped: skipFiscalization,
|
|
784
|
+
}
|
|
785
|
+
: undefined
|
|
786
|
+
}
|
|
787
|
+
serviceDate={{
|
|
788
|
+
dateType: serviceDateType,
|
|
789
|
+
onDateTypeChange: setServiceDateType,
|
|
790
|
+
}}
|
|
791
|
+
>
|
|
792
|
+
{/* Invoice-specific: Mark as paid section (UI-only state, not in form schema) */}
|
|
793
|
+
{/* Hide in edit mode - payments are managed separately */}
|
|
794
|
+
{!isEditMode && (
|
|
795
|
+
<MarkAsPaidSection
|
|
796
|
+
checked={markAsPaid}
|
|
797
|
+
onCheckedChange={setMarkAsPaid}
|
|
798
|
+
paymentType={paymentType}
|
|
799
|
+
onPaymentTypeChange={setPaymentType}
|
|
800
|
+
t={t}
|
|
801
|
+
/>
|
|
802
|
+
)}
|
|
803
|
+
</DocumentDetailsSection>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<DocumentItemsSection
|
|
807
|
+
control={form.control}
|
|
808
|
+
watch={form.watch}
|
|
809
|
+
setValue={form.setValue}
|
|
810
|
+
getValues={form.getValues}
|
|
811
|
+
entityId={entityId}
|
|
812
|
+
currencyCode={activeEntity?.currency_code ?? undefined}
|
|
813
|
+
onAddNewTax={onAddNewTax}
|
|
814
|
+
t={t}
|
|
815
|
+
taxesDisabled={reverseChargeApplies}
|
|
816
|
+
taxesDisabledMessage={
|
|
817
|
+
reverseChargeApplies ? t("Reverse charge - tax exempt EU B2B sale") : viesWarning ? viesWarning : undefined
|
|
818
|
+
}
|
|
819
|
+
maxTaxesPerItem={activeEntity?.country_rules?.max_taxes_per_item}
|
|
820
|
+
priceModesRef={priceModesRef}
|
|
821
|
+
initialPriceModes={initialPriceModes}
|
|
822
|
+
/>
|
|
823
|
+
|
|
824
|
+
<DocumentNoteField
|
|
825
|
+
control={form.control}
|
|
826
|
+
t={t}
|
|
827
|
+
entity={activeEntity}
|
|
828
|
+
document={{
|
|
829
|
+
number: watchedNumber,
|
|
830
|
+
date: watchedDate,
|
|
831
|
+
date_due: watchedDateDue,
|
|
832
|
+
currency_code: watchedCurrencyCode,
|
|
833
|
+
customer: watchedCustomer as any,
|
|
834
|
+
}}
|
|
835
|
+
/>
|
|
836
|
+
|
|
837
|
+
<DocumentPaymentTermsField
|
|
838
|
+
control={form.control}
|
|
839
|
+
t={t}
|
|
840
|
+
entity={activeEntity}
|
|
841
|
+
document={{
|
|
842
|
+
number: watchedNumber,
|
|
843
|
+
date: watchedDate,
|
|
844
|
+
date_due: watchedDateDue,
|
|
845
|
+
currency_code: watchedCurrencyCode,
|
|
846
|
+
customer: watchedCustomer as any,
|
|
847
|
+
}}
|
|
848
|
+
/>
|
|
849
|
+
</form>
|
|
850
|
+
</Form>
|
|
851
|
+
);
|
|
852
|
+
}
|