@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,161 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Customer data structure used in document forms.
|
|
6
|
+
* All fields are optional to support partial customer data.
|
|
7
|
+
*/
|
|
8
|
+
export type CustomerData = {
|
|
9
|
+
name?: string | null;
|
|
10
|
+
address?: string | null;
|
|
11
|
+
address_2?: string | null;
|
|
12
|
+
post_code?: string | null;
|
|
13
|
+
city?: string | null;
|
|
14
|
+
state?: string | null;
|
|
15
|
+
country?: string | null;
|
|
16
|
+
tax_number?: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Form schema requirements for document customer handling.
|
|
21
|
+
* Documents must have customer_id and customer fields.
|
|
22
|
+
*/
|
|
23
|
+
export type DocumentFormWithCustomer = FieldValues & {
|
|
24
|
+
customer_id?: string | null;
|
|
25
|
+
customer?: CustomerData | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Type-safe setValue wrapper for document forms.
|
|
30
|
+
*/
|
|
31
|
+
function _setFormValue<TForm extends FieldValues>(
|
|
32
|
+
form: UseFormReturn<TForm>,
|
|
33
|
+
name: Path<TForm>,
|
|
34
|
+
value: PathValue<TForm, Path<TForm>>,
|
|
35
|
+
) {
|
|
36
|
+
form.setValue(name, value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shared hook for managing customer selection and form state in document forms.
|
|
41
|
+
* Used by invoices, estimates, credit notes, and advance invoices.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const {
|
|
46
|
+
* originalCustomer,
|
|
47
|
+
* showCustomerForm,
|
|
48
|
+
* shouldFocusName,
|
|
49
|
+
* selectedCustomerId,
|
|
50
|
+
* handleCustomerSelect,
|
|
51
|
+
* handleCustomerClear,
|
|
52
|
+
* } = useDocumentCustomerForm(form);
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(form: UseFormReturn<TForm>) {
|
|
56
|
+
// Initialize states based on form's default values (for duplication scenarios)
|
|
57
|
+
const formDefaults = form.formState.defaultValues;
|
|
58
|
+
const initialCustomerId = formDefaults?.customer_id as string | undefined;
|
|
59
|
+
const initialCustomer = formDefaults?.customer as CustomerData | undefined;
|
|
60
|
+
const hasInitialCustomer = !!(initialCustomerId || initialCustomer?.name);
|
|
61
|
+
|
|
62
|
+
const [originalCustomer, setOriginalCustomer] = useState<CustomerData | null>(
|
|
63
|
+
hasInitialCustomer && initialCustomer ? initialCustomer : null,
|
|
64
|
+
);
|
|
65
|
+
const [showCustomerForm, setShowCustomerForm] = useState(hasInitialCustomer);
|
|
66
|
+
const [shouldFocusName, setShouldFocusName] = useState(false);
|
|
67
|
+
const [selectedCustomerId, setSelectedCustomerId] = useState<string | undefined>(initialCustomerId);
|
|
68
|
+
|
|
69
|
+
// Type-safe setValue that works with the generic form type
|
|
70
|
+
const setValue = <K extends Path<TForm>>(name: K, value: PathValue<TForm, K>) => {
|
|
71
|
+
form.setValue(name, value);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleCustomerSelect = (customerId: string, customer: CustomerData) => {
|
|
75
|
+
const isNewCustomer = !customerId || customerId === "";
|
|
76
|
+
|
|
77
|
+
// Helper to convert empty/null to undefined for optional fields,
|
|
78
|
+
// but keep empty string for required fields (name) so form shows them
|
|
79
|
+
const toFormValue = (value: string | null | undefined, isRequired = false): string | undefined => {
|
|
80
|
+
if (value === null || value === undefined || value === "") {
|
|
81
|
+
return isRequired ? "" : undefined;
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (isNewCustomer) {
|
|
87
|
+
// New customer - clear customer_id and set customer data
|
|
88
|
+
setValue("customer_id" as Path<TForm>, undefined as PathValue<TForm, Path<TForm>>);
|
|
89
|
+
setValue(
|
|
90
|
+
"customer" as Path<TForm>,
|
|
91
|
+
{
|
|
92
|
+
name: toFormValue(customer.name, true),
|
|
93
|
+
address: toFormValue(customer.address),
|
|
94
|
+
address_2: toFormValue(customer.address_2),
|
|
95
|
+
post_code: toFormValue(customer.post_code),
|
|
96
|
+
city: toFormValue(customer.city),
|
|
97
|
+
state: toFormValue(customer.state),
|
|
98
|
+
country: toFormValue(customer.country),
|
|
99
|
+
tax_number: toFormValue(customer.tax_number),
|
|
100
|
+
} as PathValue<TForm, Path<TForm>>,
|
|
101
|
+
);
|
|
102
|
+
setOriginalCustomer(null);
|
|
103
|
+
setSelectedCustomerId(undefined);
|
|
104
|
+
setShouldFocusName(!customer.name);
|
|
105
|
+
} else {
|
|
106
|
+
// Existing customer - set customer_id and populate fields
|
|
107
|
+
setValue("customer_id" as Path<TForm>, customerId as PathValue<TForm, Path<TForm>>);
|
|
108
|
+
|
|
109
|
+
const customerData: CustomerData = {
|
|
110
|
+
name: toFormValue(customer.name, true),
|
|
111
|
+
address: toFormValue(customer.address),
|
|
112
|
+
address_2: toFormValue(customer.address_2),
|
|
113
|
+
post_code: toFormValue(customer.post_code),
|
|
114
|
+
city: toFormValue(customer.city),
|
|
115
|
+
state: toFormValue(customer.state),
|
|
116
|
+
country: toFormValue(customer.country),
|
|
117
|
+
tax_number: toFormValue(customer.tax_number),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
setValue("customer" as Path<TForm>, customerData as PathValue<TForm, Path<TForm>>);
|
|
121
|
+
setOriginalCustomer(customerData);
|
|
122
|
+
setSelectedCustomerId(customerId);
|
|
123
|
+
setShouldFocusName(false);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setShowCustomerForm(true);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleCustomerClear = () => {
|
|
130
|
+
setValue("customer_id" as Path<TForm>, undefined as PathValue<TForm, Path<TForm>>);
|
|
131
|
+
// Clear customer object entirely - use undefined for optional fields
|
|
132
|
+
setValue(
|
|
133
|
+
"customer" as Path<TForm>,
|
|
134
|
+
{
|
|
135
|
+
name: "",
|
|
136
|
+
address: undefined,
|
|
137
|
+
address_2: undefined,
|
|
138
|
+
post_code: undefined,
|
|
139
|
+
city: undefined,
|
|
140
|
+
state: undefined,
|
|
141
|
+
country: undefined,
|
|
142
|
+
tax_number: undefined,
|
|
143
|
+
} as PathValue<TForm, Path<TForm>>,
|
|
144
|
+
);
|
|
145
|
+
setOriginalCustomer(null);
|
|
146
|
+
setSelectedCustomerId(undefined);
|
|
147
|
+
setShouldFocusName(false);
|
|
148
|
+
setShowCustomerForm(false);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
originalCustomer,
|
|
153
|
+
showCustomerForm,
|
|
154
|
+
shouldFocusName,
|
|
155
|
+
selectedCustomerId,
|
|
156
|
+
/** Initial customer name from form defaults (for duplication display) */
|
|
157
|
+
initialCustomerName: initialCustomer?.name ?? undefined,
|
|
158
|
+
handleCustomerSelect,
|
|
159
|
+
handleCustomerClear,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CreateInvoiceRequest } from "@spaceinvoices/js-sdk";
|
|
2
|
+
|
|
3
|
+
type DocumentPreviewProps = {
|
|
4
|
+
data: Partial<CreateInvoiceRequest>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function DocumentPreview({ data }: DocumentPreviewProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="rounded-lg border p-4">
|
|
10
|
+
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { AUTH_COOKIES } from "@/ui/lib/auth";
|
|
3
|
+
import { getCookie } from "@/ui/lib/browser-cookies";
|
|
4
|
+
|
|
5
|
+
// Document type union for API calls
|
|
6
|
+
export type DocumentType = "invoice" | "estimate" | "credit_note" | "advance_invoice";
|
|
7
|
+
|
|
8
|
+
// Cache key map for invalidation
|
|
9
|
+
const CACHE_KEYS: Record<DocumentType, string> = {
|
|
10
|
+
invoice: "invoices",
|
|
11
|
+
estimate: "estimates",
|
|
12
|
+
credit_note: "credit-notes",
|
|
13
|
+
advance_invoice: "advance-invoices",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get API base URL from environment
|
|
18
|
+
*/
|
|
19
|
+
function getApiBaseUrl(): string {
|
|
20
|
+
if (typeof window === "undefined") return "";
|
|
21
|
+
return (import.meta.env?.VITE_API_URL || import.meta.env?.BUN_PUBLIC_API_URL || "") as string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Make authenticated API request
|
|
26
|
+
*/
|
|
27
|
+
async function apiRequest<T>(path: string, options: RequestInit & { entityId: string }): Promise<T> {
|
|
28
|
+
const token = getCookie(AUTH_COOKIES.TOKEN);
|
|
29
|
+
const baseUrl = getApiBaseUrl();
|
|
30
|
+
|
|
31
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
32
|
+
...options,
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
Authorization: `Bearer ${token}`,
|
|
36
|
+
"x-entity-id": options.entityId,
|
|
37
|
+
...options.headers,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const errorData = await response.json().catch(() => ({}));
|
|
43
|
+
throw new Error(errorData.message || `Request failed with status ${response.status}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// DELETE returns 204 No Content
|
|
47
|
+
if (response.status === 204) {
|
|
48
|
+
return undefined as T;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return response.json();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Finalize Document Hook
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
type FinalizeDocumentOptions = {
|
|
59
|
+
entityId: string;
|
|
60
|
+
onSuccess?: (data: unknown) => void;
|
|
61
|
+
onError?: (error: Error) => void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type FinalizeDocumentVariables = {
|
|
65
|
+
documentId: string;
|
|
66
|
+
documentType: DocumentType;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to finalize a draft document
|
|
71
|
+
* Assigns a document number and runs fiscalization (if applicable)
|
|
72
|
+
*/
|
|
73
|
+
export function useFinalizeDocument(options: FinalizeDocumentOptions) {
|
|
74
|
+
const queryClient = useQueryClient();
|
|
75
|
+
|
|
76
|
+
return useMutation({
|
|
77
|
+
mutationFn: async ({ documentId, documentType }: FinalizeDocumentVariables) => {
|
|
78
|
+
return apiRequest(`/documents/${documentId}/finalize?type=${documentType}`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
entityId: options.entityId,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
onSuccess: (data, variables) => {
|
|
84
|
+
// Invalidate list cache
|
|
85
|
+
const cacheKey = CACHE_KEYS[variables.documentType];
|
|
86
|
+
queryClient.invalidateQueries({ queryKey: [cacheKey] });
|
|
87
|
+
|
|
88
|
+
// Invalidate document detail cache
|
|
89
|
+
queryClient.invalidateQueries({
|
|
90
|
+
queryKey: ["documents", variables.documentType, variables.documentId],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
options.onSuccess?.(data);
|
|
94
|
+
},
|
|
95
|
+
onError: (error: Error) => {
|
|
96
|
+
options.onError?.(error);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Delete Draft Document Hook
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
type DeleteDraftDocumentOptions = {
|
|
106
|
+
entityId: string;
|
|
107
|
+
onSuccess?: () => void;
|
|
108
|
+
onError?: (error: Error) => void;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type DeleteDraftDocumentVariables = {
|
|
112
|
+
documentId: string;
|
|
113
|
+
documentType: DocumentType;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Hook to delete a draft document
|
|
118
|
+
* Only draft documents can be deleted
|
|
119
|
+
*/
|
|
120
|
+
export function useDeleteDraftDocument(options: DeleteDraftDocumentOptions) {
|
|
121
|
+
const queryClient = useQueryClient();
|
|
122
|
+
|
|
123
|
+
return useMutation({
|
|
124
|
+
mutationFn: async ({ documentId, documentType }: DeleteDraftDocumentVariables) => {
|
|
125
|
+
return apiRequest(`/documents/${documentId}?type=${documentType}`, {
|
|
126
|
+
method: "DELETE",
|
|
127
|
+
entityId: options.entityId,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
onSuccess: (_, variables) => {
|
|
131
|
+
// Invalidate list cache
|
|
132
|
+
const cacheKey = CACHE_KEYS[variables.documentType];
|
|
133
|
+
queryClient.invalidateQueries({ queryKey: [cacheKey] });
|
|
134
|
+
|
|
135
|
+
// Remove document from detail cache
|
|
136
|
+
queryClient.removeQueries({
|
|
137
|
+
queryKey: ["documents", variables.documentType, variables.documentId],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
options.onSuccess?.();
|
|
141
|
+
},
|
|
142
|
+
onError: (error: Error) => {
|
|
143
|
+
options.onError?.(error);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documents - Shared components for all document types (invoices, estimates, credit notes)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { default as DocumentAddItemForm } from "./create/document-add-item-form";
|
|
6
|
+
export { default as DocumentAddItemTaxRateField } from "./create/document-add-item-tax-rate-field";
|
|
7
|
+
// Create form components
|
|
8
|
+
export { DocumentDetailsSection } from "./create/document-details-section";
|
|
9
|
+
export { DocumentItemsSection } from "./create/document-items-section";
|
|
10
|
+
export { DocumentRecipientSection } from "./create/document-recipient-section";
|
|
11
|
+
export { LiveInvoicePreview } from "./create/live-preview";
|
|
12
|
+
export { MarkAsPaidSection } from "./create/mark-as-paid-section";
|
|
13
|
+
// Shared utilities
|
|
14
|
+
export { prepareDocumentSubmission } from "./create/prepare-document-submission";
|
|
15
|
+
export { useDocumentCustomerForm } from "./create/use-document-customer-form";
|
|
16
|
+
// Preview components
|
|
17
|
+
export { default as DocumentPreview } from "./document-preview";
|
|
18
|
+
export { ScaledDocumentPreview } from "./shared/scaled-document-preview";
|
|
19
|
+
export { useA4Scaling } from "./shared/use-a4-scaling";
|
|
20
|
+
// Types
|
|
21
|
+
export * from "./types";
|
|
22
|
+
// View components
|
|
23
|
+
export { DocumentActionsBar, DocumentDetailsCard, DocumentPaymentsList } from "./view";
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { AdvanceInvoice, CreditNote, Estimate, Invoice } from "@spaceinvoices/js-sdk";
|
|
4
|
+
import { AlertCircle, FileText, Loader2 } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { cn } from "@/ui/lib/utils";
|
|
7
|
+
import { useEntitiesOptional } from "@/ui/providers/entities-context";
|
|
8
|
+
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
9
|
+
import { ScaledDocumentPreview } from "./scaled-document-preview";
|
|
10
|
+
import { useA4Scaling } from "./use-a4-scaling";
|
|
11
|
+
|
|
12
|
+
type Document = Invoice | Estimate | CreditNote | AdvanceInvoice;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get API path segment from shareable ID prefix
|
|
16
|
+
* Shareable IDs are prefixed with document type: inv_share_, est_share_, cre_share_, adv_share_
|
|
17
|
+
*/
|
|
18
|
+
function getDocTypePathFromShareableId(shareableId: string): string {
|
|
19
|
+
if (shareableId.startsWith("inv_share_")) return "invoices";
|
|
20
|
+
if (shareableId.startsWith("est_share_")) return "estimates";
|
|
21
|
+
if (shareableId.startsWith("cre_share_")) return "credit-notes";
|
|
22
|
+
if (shareableId.startsWith("adv_share_")) return "advance-invoices";
|
|
23
|
+
// Fallback to invoices for backwards compatibility
|
|
24
|
+
return "invoices";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type DocumentPreviewDisplayProps = {
|
|
28
|
+
/** The document to display (invoice, estimate, credit note, or advance invoice) */
|
|
29
|
+
document: Document;
|
|
30
|
+
template?: "modern";
|
|
31
|
+
className?: string;
|
|
32
|
+
apiBaseUrl?: string;
|
|
33
|
+
/** Locale for document rendering (e.g., "en-US", "sl-SI"). Uses user's UI language. */
|
|
34
|
+
locale?: string;
|
|
35
|
+
/** Whether this is a public view (no auth required) */
|
|
36
|
+
isPublicView?: boolean;
|
|
37
|
+
/** Shareable ID for public view (required when isPublicView is true) */
|
|
38
|
+
shareableId?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Document Preview Display Component
|
|
43
|
+
*
|
|
44
|
+
* Fetches and displays the HTML preview of a saved document.
|
|
45
|
+
* Works with any document type (invoice, estimate, credit note, advance invoice).
|
|
46
|
+
* Document type is auto-detected from the ID prefix.
|
|
47
|
+
*/
|
|
48
|
+
export function DocumentPreviewDisplay({
|
|
49
|
+
document,
|
|
50
|
+
template,
|
|
51
|
+
className,
|
|
52
|
+
apiBaseUrl,
|
|
53
|
+
locale,
|
|
54
|
+
isPublicView = false,
|
|
55
|
+
shareableId,
|
|
56
|
+
}: DocumentPreviewDisplayProps) {
|
|
57
|
+
const [previewHtml, setPreviewHtml] = useState<string>("");
|
|
58
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
59
|
+
const [error, setError] = useState<string | null>(null);
|
|
60
|
+
const entitiesContext = useEntitiesOptional();
|
|
61
|
+
const activeEntity = entitiesContext?.activeEntity;
|
|
62
|
+
const { sdk } = useSDK();
|
|
63
|
+
|
|
64
|
+
const { containerRef, contentRef, scale, contentHeight, A4_WIDTH_PX } = useA4Scaling(previewHtml);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const fetchPreview = async () => {
|
|
68
|
+
// For public view, use per-type shareable HTML endpoint
|
|
69
|
+
if (isPublicView && shareableId && apiBaseUrl) {
|
|
70
|
+
setIsLoading(true);
|
|
71
|
+
setError(null);
|
|
72
|
+
try {
|
|
73
|
+
// Determine document type from shareable ID prefix
|
|
74
|
+
const docTypePath = getDocTypePathFromShareableId(shareableId);
|
|
75
|
+
const response = await fetch(
|
|
76
|
+
`${apiBaseUrl}/${docTypePath}/shareable/${shareableId}/html${locale ? `?locale=${locale}` : ""}`,
|
|
77
|
+
);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error("Failed to load preview");
|
|
80
|
+
}
|
|
81
|
+
const html = await response.text();
|
|
82
|
+
setPreviewHtml(html);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setError(err instanceof Error ? err.message : "Failed to load preview");
|
|
85
|
+
setPreviewHtml("");
|
|
86
|
+
} finally {
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Authenticated view - require entity context and SDK
|
|
93
|
+
if (!document?.id || !activeEntity?.id || !sdk) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setIsLoading(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Fetch the rendered HTML by document ID using SDK wrapper
|
|
102
|
+
// Document type is auto-detected from ID prefix (inv_, est_, cre_, adv_)
|
|
103
|
+
const html = await sdk.invoices.renderHtml(document.id, { template, locale }, { entity_id: activeEntity.id });
|
|
104
|
+
|
|
105
|
+
setPreviewHtml(html);
|
|
106
|
+
setError(null);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
setError(err instanceof Error ? err.message : "Failed to load preview");
|
|
109
|
+
setPreviewHtml("");
|
|
110
|
+
} finally {
|
|
111
|
+
setIsLoading(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
fetchPreview();
|
|
116
|
+
}, [document?.id, activeEntity?.id, template, apiBaseUrl, locale, isPublicView, shareableId, sdk]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div ref={containerRef} className={cn("relative h-full", className)}>
|
|
120
|
+
{/* Loading state */}
|
|
121
|
+
{isLoading && (
|
|
122
|
+
<div className="flex h-full items-center justify-center rounded-lg border bg-muted/50">
|
|
123
|
+
<div className="flex flex-col items-center gap-2">
|
|
124
|
+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
125
|
+
<p className="text-muted-foreground text-sm">Loading preview...</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Error state */}
|
|
131
|
+
{error && !isLoading && (
|
|
132
|
+
<div className="flex h-full items-center justify-center rounded-lg border border-destructive/50 bg-destructive/10 p-8">
|
|
133
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
134
|
+
<AlertCircle className="h-8 w-8 text-destructive" />
|
|
135
|
+
<p className="font-semibold text-destructive">Preview Error</p>
|
|
136
|
+
<p className="text-muted-foreground text-sm">{error}</p>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Empty state - no preview available */}
|
|
142
|
+
{!previewHtml && !error && !isLoading && (
|
|
143
|
+
<div className="flex h-full items-center justify-center rounded-lg border border-dashed bg-muted/30">
|
|
144
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
145
|
+
<div className="rounded-full bg-muted p-4">
|
|
146
|
+
<FileText className="h-8 w-8 text-muted-foreground" />
|
|
147
|
+
</div>
|
|
148
|
+
<div>
|
|
149
|
+
<p className="font-medium text-muted-foreground">Document Preview</p>
|
|
150
|
+
<p className="text-muted-foreground/70 text-sm">Preview will appear here</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Preview - Scoped HTML injection with A4 scaling */}
|
|
157
|
+
{previewHtml && !error && !isLoading && (
|
|
158
|
+
<ScaledDocumentPreview
|
|
159
|
+
htmlContent={previewHtml}
|
|
160
|
+
scale={scale}
|
|
161
|
+
contentHeight={contentHeight}
|
|
162
|
+
A4_WIDTH_PX={A4_WIDTH_PX}
|
|
163
|
+
contentRef={contentRef}
|
|
164
|
+
entityUpdatedAt={activeEntity?.updated_at ? new Date(activeEntity.updated_at) : null}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @deprecated Use DocumentPreviewDisplay instead */
|
|
172
|
+
export const InvoicePreviewDisplay = DocumentPreviewDisplay;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type FC, useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
interface ScaledDocumentPreviewProps {
|
|
6
|
+
htmlContent: string;
|
|
7
|
+
scale: number;
|
|
8
|
+
contentHeight: number | null;
|
|
9
|
+
A4_WIDTH_PX: number;
|
|
10
|
+
contentRef: React.RefObject<HTMLDivElement | null>;
|
|
11
|
+
entityUpdatedAt?: Date | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scaled Document Preview Component
|
|
16
|
+
*
|
|
17
|
+
* Renders HTML content in a Shadow DOM with A4 scaling applied using CSS transforms.
|
|
18
|
+
* Uses Shadow DOM to completely isolate template CSS from the parent page.
|
|
19
|
+
*/
|
|
20
|
+
export const ScaledDocumentPreview: FC<ScaledDocumentPreviewProps> = ({ htmlContent, scale, A4_WIDTH_PX }) => {
|
|
21
|
+
const shadowHostRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const shadowRootRef = useRef<ShadowRoot | null>(null);
|
|
23
|
+
const [contentHeight, setContentHeight] = useState<number>(1123); // A4 height default
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const host = shadowHostRef.current;
|
|
27
|
+
if (!host) return;
|
|
28
|
+
|
|
29
|
+
// Create shadow root only once
|
|
30
|
+
if (!shadowRootRef.current) {
|
|
31
|
+
shadowRootRef.current = host.attachShadow({ mode: "open" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const shadowRoot = shadowRootRef.current;
|
|
35
|
+
shadowRoot.innerHTML = htmlContent;
|
|
36
|
+
|
|
37
|
+
// Measure content height after render
|
|
38
|
+
const measureHeight = () => {
|
|
39
|
+
const firstChild = shadowRoot.firstElementChild as HTMLElement;
|
|
40
|
+
if (firstChild) {
|
|
41
|
+
setContentHeight(firstChild.scrollHeight || 1123);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
setTimeout(measureHeight, 50);
|
|
46
|
+
}, [htmlContent]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="rounded-lg border bg-neutral-100 p-4">
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
width: A4_WIDTH_PX * scale,
|
|
53
|
+
height: contentHeight * scale,
|
|
54
|
+
margin: "0 auto",
|
|
55
|
+
overflow: "hidden",
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
ref={shadowHostRef}
|
|
60
|
+
style={{
|
|
61
|
+
width: A4_WIDTH_PX,
|
|
62
|
+
transform: `scale(${scale})`,
|
|
63
|
+
transformOrigin: "top left",
|
|
64
|
+
background: "white",
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook for A4 document scaling
|
|
5
|
+
* Handles responsive scaling of A4-sized documents to fit container width
|
|
6
|
+
*
|
|
7
|
+
* @returns Object containing scale, contentHeight, and refs for container and content
|
|
8
|
+
*/
|
|
9
|
+
export function useA4Scaling(_htmlContent?: string) {
|
|
10
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
const [scale, setScale] = useState(1);
|
|
13
|
+
const [contentHeight, setContentHeight] = useState<number | null>(null);
|
|
14
|
+
|
|
15
|
+
// A4 width in pixels at 96 DPI (210mm)
|
|
16
|
+
const A4_WIDTH_PX = 794;
|
|
17
|
+
|
|
18
|
+
// Observe container width and calculate scale
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const container = containerRef.current;
|
|
21
|
+
if (!container) return;
|
|
22
|
+
|
|
23
|
+
const observer = new ResizeObserver((entries) => {
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const width = entry.contentRect.width;
|
|
26
|
+
// Subtract padding
|
|
27
|
+
const availableWidth = width - 32;
|
|
28
|
+
// Round to 2 decimal places and only update if significant change (>1%)
|
|
29
|
+
const newScale = Math.round((availableWidth / A4_WIDTH_PX) * 100) / 100;
|
|
30
|
+
setScale((prev) => (Math.abs(prev - newScale) > 0.01 ? newScale : prev));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
observer.observe(container);
|
|
35
|
+
return () => observer.disconnect();
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
// Observe content height for proper container sizing
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const content = contentRef.current;
|
|
41
|
+
if (!content) return;
|
|
42
|
+
|
|
43
|
+
const observer = new ResizeObserver((entries) => {
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const newHeight = Math.round(entry.contentRect.height);
|
|
46
|
+
// Only update if height changed by more than 5px to avoid jitter
|
|
47
|
+
setContentHeight((prev) => (prev === null || Math.abs(prev - newHeight) > 5 ? newHeight : prev));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
observer.observe(content);
|
|
52
|
+
return () => observer.disconnect();
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
containerRef,
|
|
57
|
+
contentRef,
|
|
58
|
+
scale,
|
|
59
|
+
contentHeight,
|
|
60
|
+
A4_WIDTH_PX,
|
|
61
|
+
};
|
|
62
|
+
}
|