@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,1326 @@
|
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
2
|
+
import type { Entity } from "@spaceinvoices/js-sdk";
|
|
3
|
+
import { CreditCard, FileText, Globe, Mail, Palette, Sparkles } from "lucide-react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { SmartCodeInsertButton } from "@/ui/components/documents/create/smart-code-insert-button";
|
|
8
|
+
import { Button } from "@/ui/components/ui/button";
|
|
9
|
+
import {
|
|
10
|
+
Form,
|
|
11
|
+
FormControl,
|
|
12
|
+
FormDescription,
|
|
13
|
+
FormField,
|
|
14
|
+
FormItem,
|
|
15
|
+
FormLabel,
|
|
16
|
+
FormMessage,
|
|
17
|
+
} from "@/ui/components/ui/form";
|
|
18
|
+
import { Input } from "@/ui/components/ui/input";
|
|
19
|
+
import { Label } from "@/ui/components/ui/label";
|
|
20
|
+
import { RadioGroup, RadioGroupItem } from "@/ui/components/ui/radio-group";
|
|
21
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/components/ui/select";
|
|
22
|
+
import { Switch } from "@/ui/components/ui/switch";
|
|
23
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui/components/ui/tabs";
|
|
24
|
+
import { patchEntitySchema } from "@/ui/generated/schemas/entity";
|
|
25
|
+
import { CURRENCY_CODES } from "@/ui/lib/constants";
|
|
26
|
+
import type { ComponentTranslationProps } from "@/ui/lib/translation";
|
|
27
|
+
import { createTranslation } from "@/ui/lib/translation";
|
|
28
|
+
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
29
|
+
import ButtonLoader from "../../button-loader";
|
|
30
|
+
import { useUpdateEntity } from "../entities.hooks";
|
|
31
|
+
import { EmailTemplateVariablesInfo } from "./email-template-variables-info";
|
|
32
|
+
import { ImageUploadWithCrop } from "./image-upload-with-crop";
|
|
33
|
+
import { InputWithPreview } from "./input-with-preview";
|
|
34
|
+
import de from "./locales/de";
|
|
35
|
+
import sl from "./locales/sl";
|
|
36
|
+
|
|
37
|
+
const translations = {
|
|
38
|
+
sl,
|
|
39
|
+
de,
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
// Supported locales (matching backend)
|
|
43
|
+
const SUPPORTED_LOCALES = [
|
|
44
|
+
{ value: "en-US", label: "English (US)" },
|
|
45
|
+
{ value: "de-DE", label: "Deutsch (DE)" },
|
|
46
|
+
{ value: "it-IT", label: "Italiano (IT)" },
|
|
47
|
+
{ value: "fr-FR", label: "Français (FR)" },
|
|
48
|
+
{ value: "es-ES", label: "Español (ES)" },
|
|
49
|
+
{ value: "sl-SI", label: "Slovenščina (SI)" },
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
// Form schema extends the generated patchEntitySchema but flattens nested settings for better UX
|
|
53
|
+
// Uses .omit() to remove nested fields, then .extend() to add flattened versions
|
|
54
|
+
// This approach keeps the base validation from the API schema while allowing a better form structure
|
|
55
|
+
const entitySettingsFormSchema = patchEntitySchema
|
|
56
|
+
.omit({
|
|
57
|
+
settings: true, // Remove nested settings - we'll flatten them
|
|
58
|
+
metadata: true, // Not used in this form
|
|
59
|
+
environment: true, // Not editable here
|
|
60
|
+
})
|
|
61
|
+
.extend({
|
|
62
|
+
// Flattened settings fields for easier form handling
|
|
63
|
+
// These will be transformed back to nested structure on submission
|
|
64
|
+
primary_color: z
|
|
65
|
+
.union([z.string(), z.null()])
|
|
66
|
+
.refine((val) => !val || /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(val), {
|
|
67
|
+
message: "Must be a valid hex color (e.g., #5c6ac4)",
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
70
|
+
has_logo: z.union([z.boolean(), z.null()]).optional(),
|
|
71
|
+
has_signature: z.union([z.boolean(), z.null()]).optional(),
|
|
72
|
+
email: z
|
|
73
|
+
.union([z.string(), z.null()])
|
|
74
|
+
.refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), {
|
|
75
|
+
message: "Must be a valid email address",
|
|
76
|
+
})
|
|
77
|
+
.optional(),
|
|
78
|
+
invoice_email_subject: z.union([z.string(), z.null()]).optional(),
|
|
79
|
+
invoice_email_body: z.union([z.string(), z.null()]).optional(),
|
|
80
|
+
estimate_email_subject: z.union([z.string(), z.null()]).optional(),
|
|
81
|
+
estimate_email_body: z.union([z.string(), z.null()]).optional(),
|
|
82
|
+
default_invoice_note: z.union([z.string(), z.null()]).optional(),
|
|
83
|
+
// Bank account fields (stored in settings.bank_accounts array)
|
|
84
|
+
bank_account_iban: z
|
|
85
|
+
.union([z.string(), z.null()])
|
|
86
|
+
.refine((val) => !val || /^[A-Z]{2}[0-9A-Z]{2,32}$/.test(val.replace(/\s/g, "")), {
|
|
87
|
+
message: "Must be a valid IBAN",
|
|
88
|
+
})
|
|
89
|
+
.optional(),
|
|
90
|
+
bank_account_name: z.union([z.string(), z.null()]).optional(),
|
|
91
|
+
bank_account_bank_name: z.union([z.string(), z.null()]).optional(),
|
|
92
|
+
bank_account_bic: z.union([z.string(), z.null()]).optional(),
|
|
93
|
+
// UPN QR settings (Slovenia only)
|
|
94
|
+
upn_qr_enabled: z.union([z.boolean(), z.null()]).optional(),
|
|
95
|
+
upn_qr_display_mode: z.enum(["qr_only", "full_slip"]).optional(),
|
|
96
|
+
upn_qr_purpose_code: z
|
|
97
|
+
.union([z.string(), z.null()])
|
|
98
|
+
.refine((val) => !val || /^[A-Z]{4}$/.test(val), {
|
|
99
|
+
message: "Must be a 4-letter uppercase code (e.g., OTHR)",
|
|
100
|
+
})
|
|
101
|
+
.optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export type EntitySettingsFormSchema = z.infer<typeof entitySettingsFormSchema>;
|
|
105
|
+
|
|
106
|
+
export type EntitySettingsFormProps = {
|
|
107
|
+
entity: Entity;
|
|
108
|
+
cloudinaryCloudName?: string;
|
|
109
|
+
onSuccess?: (data: Entity) => void;
|
|
110
|
+
onError?: (error: unknown) => void;
|
|
111
|
+
onUploadSuccess?: () => void;
|
|
112
|
+
} & ComponentTranslationProps;
|
|
113
|
+
|
|
114
|
+
export function EntitySettingsForm({
|
|
115
|
+
entity,
|
|
116
|
+
cloudinaryCloudName,
|
|
117
|
+
t: translateProp,
|
|
118
|
+
namespace,
|
|
119
|
+
locale,
|
|
120
|
+
onSuccess,
|
|
121
|
+
onError,
|
|
122
|
+
onUploadSuccess,
|
|
123
|
+
}: EntitySettingsFormProps) {
|
|
124
|
+
const translate = createTranslation({
|
|
125
|
+
t: translateProp,
|
|
126
|
+
namespace,
|
|
127
|
+
locale,
|
|
128
|
+
translations,
|
|
129
|
+
});
|
|
130
|
+
const { sdk } = useSDK();
|
|
131
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
132
|
+
const [isUploadingSignature, setIsUploadingSignature] = useState(false);
|
|
133
|
+
const [logoTimestamp, setLogoTimestamp] = useState(Date.now());
|
|
134
|
+
const [signatureTimestamp, setSignatureTimestamp] = useState(Date.now());
|
|
135
|
+
const [uploadedLogoUrl, setUploadedLogoUrl] = useState<string | null>(null);
|
|
136
|
+
const [uploadedSignatureUrl, setUploadedSignatureUrl] = useState<string | null>(null);
|
|
137
|
+
const [fetchedLogoUrl, setFetchedLogoUrl] = useState<string | null>(null);
|
|
138
|
+
const [fetchedSignatureUrl, setFetchedSignatureUrl] = useState<string | null>(null);
|
|
139
|
+
|
|
140
|
+
// Extract current settings from entity (needed early for useEffect)
|
|
141
|
+
const currentSettings = (entity.settings as any) || {};
|
|
142
|
+
|
|
143
|
+
const form = useForm<EntitySettingsFormSchema>({
|
|
144
|
+
resolver: zodResolver(entitySettingsFormSchema),
|
|
145
|
+
defaultValues: {
|
|
146
|
+
name: entity.name || "",
|
|
147
|
+
tax_number: (entity as any).tax_number || null,
|
|
148
|
+
address: (entity as any).address || null,
|
|
149
|
+
address_2: (entity as any).address_2 || null,
|
|
150
|
+
post_code: (entity as any).post_code || null,
|
|
151
|
+
city: (entity as any).city || null,
|
|
152
|
+
state: (entity as any).state || null,
|
|
153
|
+
currency_code: entity.currency_code || undefined,
|
|
154
|
+
locale: entity.locale || "en-US",
|
|
155
|
+
primary_color: currentSettings.primary_color || null,
|
|
156
|
+
has_logo: currentSettings.has_logo || null,
|
|
157
|
+
has_signature: currentSettings.has_signature || null,
|
|
158
|
+
email: currentSettings.email || null,
|
|
159
|
+
invoice_email_subject: currentSettings.email_defaults?.invoice_subject || null,
|
|
160
|
+
invoice_email_body: currentSettings.email_defaults?.invoice_body || null,
|
|
161
|
+
estimate_email_subject: currentSettings.email_defaults?.estimate_subject || null,
|
|
162
|
+
estimate_email_body: currentSettings.email_defaults?.estimate_body || null,
|
|
163
|
+
default_invoice_note: currentSettings.default_invoice_note || null,
|
|
164
|
+
// Bank account and UPN QR settings
|
|
165
|
+
bank_account_iban: currentSettings.bank_accounts?.[0]?.iban || null,
|
|
166
|
+
bank_account_name: currentSettings.bank_accounts?.[0]?.name || null,
|
|
167
|
+
bank_account_bank_name: currentSettings.bank_accounts?.[0]?.bank_name || null,
|
|
168
|
+
bank_account_bic: currentSettings.bank_accounts?.[0]?.bic || null,
|
|
169
|
+
upn_qr_enabled: currentSettings.upn_qr?.enabled || false,
|
|
170
|
+
upn_qr_display_mode: currentSettings.upn_qr?.display_mode || "qr_only",
|
|
171
|
+
upn_qr_purpose_code: currentSettings.upn_qr?.purpose_code || "OTHR",
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Refs for textarea cursor position tracking
|
|
176
|
+
const defaultInvoiceNoteRef = useRef<HTMLTextAreaElement>(null);
|
|
177
|
+
const invoiceEmailSubjectRef = useRef<HTMLInputElement>(null);
|
|
178
|
+
const invoiceEmailBodyRef = useRef<HTMLTextAreaElement>(null);
|
|
179
|
+
const estimateEmailSubjectRef = useRef<HTMLInputElement>(null);
|
|
180
|
+
const estimateEmailBodyRef = useRef<HTMLTextAreaElement>(null);
|
|
181
|
+
|
|
182
|
+
// Fetch logo and signature URLs from file metadata on mount
|
|
183
|
+
// Always fetch files to ensure we have the latest uploads, even if entity settings are stale
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
async function fetchFileUrls() {
|
|
186
|
+
try {
|
|
187
|
+
// SDK auto-unwraps response, returns data array directly
|
|
188
|
+
const files = await sdk.files.list({ entity_id: entity.id });
|
|
189
|
+
|
|
190
|
+
const logoFile = files.data.find((f: { category: string }) => f.category === "logo");
|
|
191
|
+
const signatureFile = files.data.find((f: { category: string }) => f.category === "signature");
|
|
192
|
+
|
|
193
|
+
if (logoFile) {
|
|
194
|
+
setFetchedLogoUrl(logoFile.secureUrl);
|
|
195
|
+
// Sync form state if logo exists but form doesn't know about it
|
|
196
|
+
if (!form.getValues("has_logo")) {
|
|
197
|
+
form.setValue("has_logo", true);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (signatureFile) {
|
|
201
|
+
setFetchedSignatureUrl(signatureFile.secureUrl);
|
|
202
|
+
// Sync form state if signature exists but form doesn't know about it
|
|
203
|
+
if (!form.getValues("has_signature")) {
|
|
204
|
+
form.setValue("has_signature", true);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("Failed to fetch file URLs:", error);
|
|
209
|
+
// Fall back to constructed URLs if fetch fails
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fetchFileUrls();
|
|
214
|
+
}, [entity.id, sdk.files, form]);
|
|
215
|
+
|
|
216
|
+
// Watch the has_logo and has_signature form fields for changes
|
|
217
|
+
const hasLogo = form.watch("has_logo");
|
|
218
|
+
const hasSignature = form.watch("has_signature");
|
|
219
|
+
|
|
220
|
+
// Logo URL priority: freshly uploaded > fetched from API > constructed fallback
|
|
221
|
+
const entityTimestamp = entity.updated_at ? new Date(entity.updated_at).getTime() : logoTimestamp;
|
|
222
|
+
const logoUrl =
|
|
223
|
+
hasLogo && cloudinaryCloudName
|
|
224
|
+
? uploadedLogoUrl ||
|
|
225
|
+
fetchedLogoUrl ||
|
|
226
|
+
`https://res.cloudinary.com/${cloudinaryCloudName}/image/upload/leka/entities/${entity.id}/logos/logo_${entity.id}.png?v=${entityTimestamp}`
|
|
227
|
+
: undefined;
|
|
228
|
+
|
|
229
|
+
// Signature URL priority: freshly uploaded > fetched from API > constructed fallback
|
|
230
|
+
const signatureEntityTimestamp = entity.updated_at ? new Date(entity.updated_at).getTime() : signatureTimestamp;
|
|
231
|
+
const signatureUrl =
|
|
232
|
+
hasSignature && cloudinaryCloudName
|
|
233
|
+
? uploadedSignatureUrl ||
|
|
234
|
+
fetchedSignatureUrl ||
|
|
235
|
+
`https://res.cloudinary.com/${cloudinaryCloudName}/image/upload/leka/entities/${entity.id}/signatures/signature_${entity.id}.png?v=${signatureEntityTimestamp}`
|
|
236
|
+
: undefined;
|
|
237
|
+
|
|
238
|
+
const { mutate: updateEntity, isPending } = useUpdateEntity({
|
|
239
|
+
entityId: entity.id,
|
|
240
|
+
onSuccess: (data) => {
|
|
241
|
+
onSuccess?.(data);
|
|
242
|
+
},
|
|
243
|
+
onError: (error) => {
|
|
244
|
+
onError?.(error);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const handleImageUpload = async (file: File): Promise<{ secureUrl: string }> => {
|
|
249
|
+
setIsUploading(true);
|
|
250
|
+
try {
|
|
251
|
+
// SDK expects { file: Blob } as first arg, SDKMethodOptions as last
|
|
252
|
+
const result = await sdk.upload.uploadImage({ file }, { entity_id: entity.id });
|
|
253
|
+
|
|
254
|
+
// Note: The upload endpoint automatically sets has_logo=true in entity settings
|
|
255
|
+
// We just need to update the form state
|
|
256
|
+
form.setValue("has_logo", true);
|
|
257
|
+
|
|
258
|
+
// Use the freshly uploaded URL with Cloudinary version (bypasses CDN cache)
|
|
259
|
+
// This ensures immediate preview update without waiting for CDN invalidation
|
|
260
|
+
setUploadedLogoUrl(result.secureUrl);
|
|
261
|
+
|
|
262
|
+
// Update timestamp to bust cache for future loads
|
|
263
|
+
setLogoTimestamp(Date.now());
|
|
264
|
+
|
|
265
|
+
// Trigger entity refetch to get the updated has_logo from database
|
|
266
|
+
onUploadSuccess?.();
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error("Upload failed:", error);
|
|
271
|
+
onError?.(error);
|
|
272
|
+
throw error;
|
|
273
|
+
} finally {
|
|
274
|
+
setIsUploading(false);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const handleSignatureUpload = async (file: File): Promise<{ secureUrl: string }> => {
|
|
279
|
+
setIsUploadingSignature(true);
|
|
280
|
+
try {
|
|
281
|
+
// SDK expects { file, category } as first arg, SDKMethodOptions as last
|
|
282
|
+
const result = await sdk.files.uploadFile({ file, category: "signature" }, { entity_id: entity.id });
|
|
283
|
+
|
|
284
|
+
// Update form state
|
|
285
|
+
form.setValue("has_signature", true);
|
|
286
|
+
|
|
287
|
+
// Use the freshly uploaded URL
|
|
288
|
+
setUploadedSignatureUrl(result.secureUrl);
|
|
289
|
+
|
|
290
|
+
// Update timestamp to bust cache
|
|
291
|
+
setSignatureTimestamp(Date.now());
|
|
292
|
+
|
|
293
|
+
// Trigger entity refetch
|
|
294
|
+
onUploadSuccess?.();
|
|
295
|
+
|
|
296
|
+
return { secureUrl: result.secureUrl };
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error("Signature upload failed:", error);
|
|
299
|
+
onError?.(error);
|
|
300
|
+
throw error;
|
|
301
|
+
} finally {
|
|
302
|
+
setIsUploadingSignature(false);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const onSubmit = async (values: EntitySettingsFormSchema) => {
|
|
307
|
+
try {
|
|
308
|
+
// Prepare the update payload
|
|
309
|
+
const updatePayload: any = {
|
|
310
|
+
settings: {
|
|
311
|
+
...currentSettings,
|
|
312
|
+
primary_color: values.primary_color || undefined,
|
|
313
|
+
has_logo: values.has_logo || undefined,
|
|
314
|
+
has_signature: values.has_signature || undefined,
|
|
315
|
+
email: values.email || undefined,
|
|
316
|
+
email_defaults: {
|
|
317
|
+
invoice_subject: values.invoice_email_subject || undefined,
|
|
318
|
+
invoice_body: values.invoice_email_body || undefined,
|
|
319
|
+
estimate_subject: values.estimate_email_subject || undefined,
|
|
320
|
+
estimate_body: values.estimate_email_body || undefined,
|
|
321
|
+
},
|
|
322
|
+
default_invoice_note: values.default_invoice_note || undefined,
|
|
323
|
+
// UPN QR settings - only include if enabled or was previously enabled
|
|
324
|
+
upn_qr:
|
|
325
|
+
values.upn_qr_enabled || currentSettings.upn_qr
|
|
326
|
+
? {
|
|
327
|
+
enabled: values.upn_qr_enabled || false,
|
|
328
|
+
display_mode: values.upn_qr_display_mode || "qr_only",
|
|
329
|
+
purpose_code: values.upn_qr_purpose_code || "OTHR",
|
|
330
|
+
}
|
|
331
|
+
: undefined,
|
|
332
|
+
// Bank accounts - store in array format (preserving other accounts if any)
|
|
333
|
+
bank_accounts: values.bank_account_iban
|
|
334
|
+
? [
|
|
335
|
+
{
|
|
336
|
+
type: "iban" as const,
|
|
337
|
+
iban: values.bank_account_iban,
|
|
338
|
+
name: values.bank_account_name || undefined,
|
|
339
|
+
bank_name: values.bank_account_bank_name || undefined,
|
|
340
|
+
bic: values.bank_account_bic || undefined,
|
|
341
|
+
is_default: true,
|
|
342
|
+
},
|
|
343
|
+
// Preserve any additional bank accounts (beyond the first one)
|
|
344
|
+
...(currentSettings.bank_accounts?.slice(1) || []),
|
|
345
|
+
]
|
|
346
|
+
: currentSettings.bank_accounts || undefined,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Add top-level fields if changed
|
|
351
|
+
if (values.name && values.name !== entity.name) {
|
|
352
|
+
updatePayload.name = values.name;
|
|
353
|
+
}
|
|
354
|
+
if (values.tax_number !== undefined && values.tax_number !== (entity as any).tax_number) {
|
|
355
|
+
updatePayload.tax_number = values.tax_number;
|
|
356
|
+
}
|
|
357
|
+
if (values.address !== undefined && values.address !== (entity as any).address) {
|
|
358
|
+
updatePayload.address = values.address;
|
|
359
|
+
}
|
|
360
|
+
if (values.address_2 !== undefined && values.address_2 !== (entity as any).address_2) {
|
|
361
|
+
updatePayload.address_2 = values.address_2;
|
|
362
|
+
}
|
|
363
|
+
if (values.post_code !== undefined && values.post_code !== (entity as any).post_code) {
|
|
364
|
+
updatePayload.post_code = values.post_code;
|
|
365
|
+
}
|
|
366
|
+
if (values.city !== undefined && values.city !== (entity as any).city) {
|
|
367
|
+
updatePayload.city = values.city;
|
|
368
|
+
}
|
|
369
|
+
if (values.state !== undefined && values.state !== (entity as any).state) {
|
|
370
|
+
updatePayload.state = values.state;
|
|
371
|
+
}
|
|
372
|
+
if (values.currency_code && values.currency_code !== entity.currency_code) {
|
|
373
|
+
updatePayload.currency_code = values.currency_code;
|
|
374
|
+
}
|
|
375
|
+
if (values.locale && values.locale !== entity.locale) {
|
|
376
|
+
updatePayload.locale = values.locale;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
updateEntity({ id: entity.id, data: updatePayload });
|
|
380
|
+
} catch (e) {
|
|
381
|
+
onError?.(e);
|
|
382
|
+
form.setError("root", {
|
|
383
|
+
type: "submit",
|
|
384
|
+
message: "Failed to update entity settings",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<Form {...form}>
|
|
391
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-12">
|
|
392
|
+
{/* Entity Details Section */}
|
|
393
|
+
<div className="grid gap-8 lg:grid-cols-2">
|
|
394
|
+
<div className="space-y-4">
|
|
395
|
+
<div className="flex items-center gap-3">
|
|
396
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-500/10">
|
|
397
|
+
<Globe className="h-5 w-5 text-green-600 dark:text-green-400" />
|
|
398
|
+
</div>
|
|
399
|
+
<div>
|
|
400
|
+
<h3 className="font-semibold text-lg">{translate("Entity Details")}</h3>
|
|
401
|
+
<p className="text-muted-foreground text-sm">{translate("Basic information about your entity")}</p>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div className="space-y-6 pl-[52px]">
|
|
406
|
+
<FormField
|
|
407
|
+
control={form.control}
|
|
408
|
+
name="name"
|
|
409
|
+
render={({ field }) => (
|
|
410
|
+
<FormItem>
|
|
411
|
+
<FormLabel className="font-medium text-base">{translate("Entity Name")}</FormLabel>
|
|
412
|
+
<FormControl>
|
|
413
|
+
<Input {...field} placeholder="My Company LLC" className="h-10" />
|
|
414
|
+
</FormControl>
|
|
415
|
+
<FormDescription className="text-xs">
|
|
416
|
+
{translate("Your company or organization name")}
|
|
417
|
+
</FormDescription>
|
|
418
|
+
<FormMessage />
|
|
419
|
+
</FormItem>
|
|
420
|
+
)}
|
|
421
|
+
/>
|
|
422
|
+
|
|
423
|
+
<FormField
|
|
424
|
+
control={form.control}
|
|
425
|
+
name="tax_number"
|
|
426
|
+
render={({ field }) => (
|
|
427
|
+
<FormItem className="max-w-xs">
|
|
428
|
+
<FormLabel className="font-medium text-base">{translate("Tax ID")}</FormLabel>
|
|
429
|
+
<FormControl>
|
|
430
|
+
<Input
|
|
431
|
+
{...field}
|
|
432
|
+
value={field.value || ""}
|
|
433
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
434
|
+
placeholder="12-3456789"
|
|
435
|
+
className="h-10"
|
|
436
|
+
/>
|
|
437
|
+
</FormControl>
|
|
438
|
+
<FormDescription className="text-xs">
|
|
439
|
+
{translate("Tax identification number (optional)")}
|
|
440
|
+
</FormDescription>
|
|
441
|
+
<FormMessage />
|
|
442
|
+
</FormItem>
|
|
443
|
+
)}
|
|
444
|
+
/>
|
|
445
|
+
|
|
446
|
+
<div className="border-t pt-6">
|
|
447
|
+
<FormField
|
|
448
|
+
control={form.control}
|
|
449
|
+
name="address"
|
|
450
|
+
render={({ field }) => (
|
|
451
|
+
<FormItem>
|
|
452
|
+
<FormLabel className="font-medium text-base">{translate("Address")}</FormLabel>
|
|
453
|
+
<FormControl>
|
|
454
|
+
<Input
|
|
455
|
+
{...field}
|
|
456
|
+
value={field.value || ""}
|
|
457
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
458
|
+
placeholder="123 Main Street"
|
|
459
|
+
className="h-10"
|
|
460
|
+
/>
|
|
461
|
+
</FormControl>
|
|
462
|
+
<FormDescription className="text-xs">{translate("Street address")}</FormDescription>
|
|
463
|
+
<FormMessage />
|
|
464
|
+
</FormItem>
|
|
465
|
+
)}
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<FormField
|
|
470
|
+
control={form.control}
|
|
471
|
+
name="address_2"
|
|
472
|
+
render={({ field }) => (
|
|
473
|
+
<FormItem>
|
|
474
|
+
<FormLabel className="font-medium text-base">{translate("Address Line 2")}</FormLabel>
|
|
475
|
+
<FormControl>
|
|
476
|
+
<Input
|
|
477
|
+
{...field}
|
|
478
|
+
value={field.value || ""}
|
|
479
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
480
|
+
placeholder="Suite 100"
|
|
481
|
+
className="h-10"
|
|
482
|
+
/>
|
|
483
|
+
</FormControl>
|
|
484
|
+
<FormDescription className="text-xs">
|
|
485
|
+
{translate("Apartment, suite, unit, etc. (optional)")}
|
|
486
|
+
</FormDescription>
|
|
487
|
+
<FormMessage />
|
|
488
|
+
</FormItem>
|
|
489
|
+
)}
|
|
490
|
+
/>
|
|
491
|
+
|
|
492
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
493
|
+
<FormField
|
|
494
|
+
control={form.control}
|
|
495
|
+
name="city"
|
|
496
|
+
render={({ field }) => (
|
|
497
|
+
<FormItem>
|
|
498
|
+
<FormLabel className="font-medium text-base">{translate("City")}</FormLabel>
|
|
499
|
+
<FormControl>
|
|
500
|
+
<Input
|
|
501
|
+
{...field}
|
|
502
|
+
value={field.value || ""}
|
|
503
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
504
|
+
placeholder="San Francisco"
|
|
505
|
+
className="h-10"
|
|
506
|
+
/>
|
|
507
|
+
</FormControl>
|
|
508
|
+
<FormMessage />
|
|
509
|
+
</FormItem>
|
|
510
|
+
)}
|
|
511
|
+
/>
|
|
512
|
+
|
|
513
|
+
<FormField
|
|
514
|
+
control={form.control}
|
|
515
|
+
name="state"
|
|
516
|
+
render={({ field }) => (
|
|
517
|
+
<FormItem>
|
|
518
|
+
<FormLabel className="font-medium text-base">{translate("State/Province")}</FormLabel>
|
|
519
|
+
<FormControl>
|
|
520
|
+
<Input
|
|
521
|
+
{...field}
|
|
522
|
+
value={field.value || ""}
|
|
523
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
524
|
+
placeholder="CA"
|
|
525
|
+
className="h-10"
|
|
526
|
+
/>
|
|
527
|
+
</FormControl>
|
|
528
|
+
<FormMessage />
|
|
529
|
+
</FormItem>
|
|
530
|
+
)}
|
|
531
|
+
/>
|
|
532
|
+
|
|
533
|
+
<FormField
|
|
534
|
+
control={form.control}
|
|
535
|
+
name="post_code"
|
|
536
|
+
render={({ field }) => (
|
|
537
|
+
<FormItem>
|
|
538
|
+
<FormLabel className="font-medium text-base">{translate("Postal Code")}</FormLabel>
|
|
539
|
+
<FormControl>
|
|
540
|
+
<Input
|
|
541
|
+
{...field}
|
|
542
|
+
value={field.value || ""}
|
|
543
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
544
|
+
placeholder="94102"
|
|
545
|
+
className="h-10"
|
|
546
|
+
/>
|
|
547
|
+
</FormControl>
|
|
548
|
+
<FormMessage />
|
|
549
|
+
</FormItem>
|
|
550
|
+
)}
|
|
551
|
+
/>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<FormItem>
|
|
555
|
+
<FormLabel className="font-medium text-base">{translate("Country")}</FormLabel>
|
|
556
|
+
<FormControl>
|
|
557
|
+
<Input value={(entity as any).country || ""} disabled className="h-10" />
|
|
558
|
+
</FormControl>
|
|
559
|
+
<FormDescription className="text-xs">{translate("Country cannot be changed")}</FormDescription>
|
|
560
|
+
</FormItem>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
{/* Help Content */}
|
|
565
|
+
<div className="hidden lg:block">
|
|
566
|
+
<div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
|
|
567
|
+
<div className="flex items-start gap-2">
|
|
568
|
+
<Globe className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
569
|
+
<div className="space-y-2">
|
|
570
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("Entity Information")}</p>
|
|
571
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
572
|
+
{translate(
|
|
573
|
+
"This information appears on your invoices and estimates. Your entity name and address will be displayed prominently on all documents.",
|
|
574
|
+
)}
|
|
575
|
+
</p>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
{/* Customization Section */}
|
|
583
|
+
<div className="grid gap-8 lg:grid-cols-2">
|
|
584
|
+
<div className="space-y-4">
|
|
585
|
+
<div className="flex items-center gap-3">
|
|
586
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
|
587
|
+
<Palette className="h-5 w-5 text-primary" />
|
|
588
|
+
</div>
|
|
589
|
+
<div>
|
|
590
|
+
<h3 className="font-semibold text-lg">{translate("Customization")}</h3>
|
|
591
|
+
<p className="text-muted-foreground text-sm">{translate("Customize your invoice appearance")}</p>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
<div className="space-y-6 pl-[52px]">
|
|
596
|
+
<FormField
|
|
597
|
+
control={form.control}
|
|
598
|
+
name="primary_color"
|
|
599
|
+
render={({ field }) => (
|
|
600
|
+
<FormItem className="max-w-xs">
|
|
601
|
+
<FormLabel className="font-medium text-base">{translate("Primary Color")}</FormLabel>
|
|
602
|
+
<FormControl>
|
|
603
|
+
<div className="flex items-center gap-3">
|
|
604
|
+
<div className="relative">
|
|
605
|
+
<Input
|
|
606
|
+
type="color"
|
|
607
|
+
{...field}
|
|
608
|
+
value={field.value || "#5c6ac4"}
|
|
609
|
+
className="h-12 w-16 cursor-pointer rounded-md border-2 p-1.5 shadow-sm transition-all hover:border-primary/50 hover:shadow-md"
|
|
610
|
+
/>
|
|
611
|
+
</div>
|
|
612
|
+
<Input
|
|
613
|
+
type="text"
|
|
614
|
+
value={field.value || ""}
|
|
615
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
616
|
+
placeholder="#5c6ac4"
|
|
617
|
+
className="w-32 font-mono text-sm"
|
|
618
|
+
/>
|
|
619
|
+
</div>
|
|
620
|
+
</FormControl>
|
|
621
|
+
<FormDescription className="text-xs">
|
|
622
|
+
{translate("Color used in invoices and documents")}
|
|
623
|
+
</FormDescription>
|
|
624
|
+
<FormMessage />
|
|
625
|
+
</FormItem>
|
|
626
|
+
)}
|
|
627
|
+
/>
|
|
628
|
+
|
|
629
|
+
<div className="grid gap-6 border-t pt-6 lg:grid-cols-2">
|
|
630
|
+
<FormField
|
|
631
|
+
control={form.control}
|
|
632
|
+
name="has_logo"
|
|
633
|
+
render={({ field }) => (
|
|
634
|
+
<FormItem>
|
|
635
|
+
<FormLabel className="font-medium text-base">{translate("Logo")}</FormLabel>
|
|
636
|
+
<FormControl>
|
|
637
|
+
<ImageUploadWithCrop
|
|
638
|
+
value={logoUrl || ""}
|
|
639
|
+
onChange={(url) => field.onChange(!!url)}
|
|
640
|
+
onUpload={handleImageUpload}
|
|
641
|
+
translate={translate}
|
|
642
|
+
isUploading={isUploading}
|
|
643
|
+
/>
|
|
644
|
+
</FormControl>
|
|
645
|
+
<FormDescription className="text-xs">
|
|
646
|
+
{translate("Upload your company logo for invoices")}
|
|
647
|
+
</FormDescription>
|
|
648
|
+
<FormMessage />
|
|
649
|
+
</FormItem>
|
|
650
|
+
)}
|
|
651
|
+
/>
|
|
652
|
+
|
|
653
|
+
<FormField
|
|
654
|
+
control={form.control}
|
|
655
|
+
name="has_signature"
|
|
656
|
+
render={({ field }) => (
|
|
657
|
+
<FormItem>
|
|
658
|
+
<FormLabel className="font-medium text-base">{translate("Signature")}</FormLabel>
|
|
659
|
+
<FormControl>
|
|
660
|
+
<ImageUploadWithCrop
|
|
661
|
+
value={signatureUrl || ""}
|
|
662
|
+
onChange={(url) => field.onChange(!!url)}
|
|
663
|
+
onUpload={handleSignatureUpload}
|
|
664
|
+
translate={translate}
|
|
665
|
+
isUploading={isUploadingSignature}
|
|
666
|
+
imageType="signature"
|
|
667
|
+
/>
|
|
668
|
+
</FormControl>
|
|
669
|
+
<FormDescription className="text-xs">
|
|
670
|
+
{translate("Upload a signature image for PDFs (optional)")}
|
|
671
|
+
</FormDescription>
|
|
672
|
+
<FormMessage />
|
|
673
|
+
</FormItem>
|
|
674
|
+
)}
|
|
675
|
+
/>
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
{/* Help Content */}
|
|
681
|
+
<div className="hidden lg:block">
|
|
682
|
+
<div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
|
|
683
|
+
<div className="flex items-start gap-2">
|
|
684
|
+
<Palette className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
685
|
+
<div className="space-y-2">
|
|
686
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("Brand Appearance")}</p>
|
|
687
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
688
|
+
{translate(
|
|
689
|
+
"Customize how your invoices and estimates look. Set your brand color, upload your company logo, and add a signature for a professional touch.",
|
|
690
|
+
)}
|
|
691
|
+
</p>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
{/* Entity Settings Section */}
|
|
699
|
+
<div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
|
|
700
|
+
<div className="space-y-4">
|
|
701
|
+
<div className="flex items-center gap-3">
|
|
702
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10">
|
|
703
|
+
<Globe className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
704
|
+
</div>
|
|
705
|
+
<div>
|
|
706
|
+
<h3 className="font-semibold text-lg">{translate("Entity Settings")}</h3>
|
|
707
|
+
<p className="text-muted-foreground text-sm">{translate("Configure entity localization")}</p>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<div className="space-y-6 pl-[52px]">
|
|
712
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
713
|
+
<FormField
|
|
714
|
+
control={form.control}
|
|
715
|
+
name="currency_code"
|
|
716
|
+
render={({ field }) => (
|
|
717
|
+
<FormItem>
|
|
718
|
+
<FormLabel className="font-medium text-sm">{translate("Currency")}</FormLabel>
|
|
719
|
+
<Select onValueChange={field.onChange} value={field.value || ""}>
|
|
720
|
+
<FormControl>
|
|
721
|
+
<SelectTrigger className="h-10">
|
|
722
|
+
<SelectValue placeholder={translate("Select currency")} />
|
|
723
|
+
</SelectTrigger>
|
|
724
|
+
</FormControl>
|
|
725
|
+
<SelectContent>
|
|
726
|
+
{CURRENCY_CODES.map((currency) => (
|
|
727
|
+
<SelectItem key={currency.value} value={currency.value}>
|
|
728
|
+
{currency.label}
|
|
729
|
+
</SelectItem>
|
|
730
|
+
))}
|
|
731
|
+
</SelectContent>
|
|
732
|
+
</Select>
|
|
733
|
+
<FormMessage />
|
|
734
|
+
</FormItem>
|
|
735
|
+
)}
|
|
736
|
+
/>
|
|
737
|
+
|
|
738
|
+
<FormField
|
|
739
|
+
control={form.control}
|
|
740
|
+
name="locale"
|
|
741
|
+
render={({ field }) => (
|
|
742
|
+
<FormItem>
|
|
743
|
+
<FormLabel className="font-medium text-sm">{translate("Locale")}</FormLabel>
|
|
744
|
+
<Select onValueChange={field.onChange} value={field.value || ""}>
|
|
745
|
+
<FormControl>
|
|
746
|
+
<SelectTrigger className="h-10">
|
|
747
|
+
<SelectValue placeholder={translate("Select locale")} />
|
|
748
|
+
</SelectTrigger>
|
|
749
|
+
</FormControl>
|
|
750
|
+
<SelectContent>
|
|
751
|
+
{SUPPORTED_LOCALES.map((locale) => (
|
|
752
|
+
<SelectItem key={locale.value} value={locale.value}>
|
|
753
|
+
{locale.label}
|
|
754
|
+
</SelectItem>
|
|
755
|
+
))}
|
|
756
|
+
</SelectContent>
|
|
757
|
+
</Select>
|
|
758
|
+
<FormMessage />
|
|
759
|
+
</FormItem>
|
|
760
|
+
)}
|
|
761
|
+
/>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
{/* Help Content */}
|
|
767
|
+
<div className="hidden lg:block">
|
|
768
|
+
<div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
|
|
769
|
+
<div className="flex items-start gap-2">
|
|
770
|
+
<Globe className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
771
|
+
<div className="space-y-2">
|
|
772
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("Localization")}</p>
|
|
773
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
774
|
+
{translate(
|
|
775
|
+
"Set your default currency and locale for invoices and estimates. These settings affect how numbers, dates, and currencies are displayed.",
|
|
776
|
+
)}
|
|
777
|
+
</p>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
{/* UPN QR Payment Section - Only for Slovenian entities */}
|
|
785
|
+
{(entity as any).country_code === "SI" && (
|
|
786
|
+
<div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
|
|
787
|
+
<div className="space-y-4">
|
|
788
|
+
<div className="flex items-center gap-3">
|
|
789
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/10">
|
|
790
|
+
<CreditCard className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
|
791
|
+
</div>
|
|
792
|
+
<div>
|
|
793
|
+
<h3 className="font-semibold text-lg">{translate("UPN QR Payment")}</h3>
|
|
794
|
+
<p className="text-muted-foreground text-sm">
|
|
795
|
+
{translate("Configure UPN QR payment slip for invoices")}
|
|
796
|
+
</p>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<div className="space-y-6 pl-[52px]">
|
|
801
|
+
<div className="space-y-4 rounded-lg border p-4">
|
|
802
|
+
<p className="font-medium text-sm">{translate("Bank Account")}</p>
|
|
803
|
+
|
|
804
|
+
<FormField
|
|
805
|
+
control={form.control}
|
|
806
|
+
name="bank_account_name"
|
|
807
|
+
render={({ field }) => (
|
|
808
|
+
<FormItem>
|
|
809
|
+
<FormLabel>{translate("Account Name")}</FormLabel>
|
|
810
|
+
<FormControl>
|
|
811
|
+
<Input
|
|
812
|
+
{...field}
|
|
813
|
+
value={field.value || ""}
|
|
814
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
815
|
+
placeholder={translate("Main Business Account")}
|
|
816
|
+
className="h-9"
|
|
817
|
+
/>
|
|
818
|
+
</FormControl>
|
|
819
|
+
<FormMessage />
|
|
820
|
+
</FormItem>
|
|
821
|
+
)}
|
|
822
|
+
/>
|
|
823
|
+
|
|
824
|
+
<FormField
|
|
825
|
+
control={form.control}
|
|
826
|
+
name="bank_account_iban"
|
|
827
|
+
render={({ field }) => (
|
|
828
|
+
<FormItem>
|
|
829
|
+
<FormLabel>{translate("IBAN")}</FormLabel>
|
|
830
|
+
<FormControl>
|
|
831
|
+
<Input
|
|
832
|
+
{...field}
|
|
833
|
+
value={field.value || ""}
|
|
834
|
+
onChange={(e) => field.onChange(e.target.value.toUpperCase().replace(/\s/g, "") || null)}
|
|
835
|
+
placeholder="SI56 0123 4567 8901 234"
|
|
836
|
+
className="h-9 font-mono"
|
|
837
|
+
/>
|
|
838
|
+
</FormControl>
|
|
839
|
+
<FormMessage />
|
|
840
|
+
</FormItem>
|
|
841
|
+
)}
|
|
842
|
+
/>
|
|
843
|
+
|
|
844
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
845
|
+
<FormField
|
|
846
|
+
control={form.control}
|
|
847
|
+
name="bank_account_bank_name"
|
|
848
|
+
render={({ field }) => (
|
|
849
|
+
<FormItem>
|
|
850
|
+
<FormLabel>{translate("Bank Name")}</FormLabel>
|
|
851
|
+
<FormControl>
|
|
852
|
+
<Input
|
|
853
|
+
{...field}
|
|
854
|
+
value={field.value || ""}
|
|
855
|
+
onChange={(e) => field.onChange(e.target.value || null)}
|
|
856
|
+
placeholder="NLB d.d."
|
|
857
|
+
className="h-9"
|
|
858
|
+
/>
|
|
859
|
+
</FormControl>
|
|
860
|
+
<FormMessage />
|
|
861
|
+
</FormItem>
|
|
862
|
+
)}
|
|
863
|
+
/>
|
|
864
|
+
|
|
865
|
+
<FormField
|
|
866
|
+
control={form.control}
|
|
867
|
+
name="bank_account_bic"
|
|
868
|
+
render={({ field }) => (
|
|
869
|
+
<FormItem>
|
|
870
|
+
<FormLabel>{translate("BIC/SWIFT")}</FormLabel>
|
|
871
|
+
<FormControl>
|
|
872
|
+
<Input
|
|
873
|
+
{...field}
|
|
874
|
+
value={field.value || ""}
|
|
875
|
+
onChange={(e) => field.onChange(e.target.value.toUpperCase() || null)}
|
|
876
|
+
placeholder="LJBASI2X"
|
|
877
|
+
className="h-9 font-mono"
|
|
878
|
+
/>
|
|
879
|
+
</FormControl>
|
|
880
|
+
<FormMessage />
|
|
881
|
+
</FormItem>
|
|
882
|
+
)}
|
|
883
|
+
/>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
|
|
887
|
+
<div className="border-t pt-6">
|
|
888
|
+
<FormField
|
|
889
|
+
control={form.control}
|
|
890
|
+
name="upn_qr_enabled"
|
|
891
|
+
render={({ field }) => (
|
|
892
|
+
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
893
|
+
<div className="space-y-0.5">
|
|
894
|
+
<FormLabel className="font-medium text-base">
|
|
895
|
+
{translate("Enable UPN QR on invoices")}
|
|
896
|
+
</FormLabel>
|
|
897
|
+
<FormDescription className="text-xs">
|
|
898
|
+
{translate("Show payment QR code on PDF invoices for easy mobile banking payments")}
|
|
899
|
+
</FormDescription>
|
|
900
|
+
</div>
|
|
901
|
+
<FormControl>
|
|
902
|
+
<Switch
|
|
903
|
+
checked={field.value || false}
|
|
904
|
+
onCheckedChange={field.onChange}
|
|
905
|
+
disabled={!form.watch("bank_account_iban")}
|
|
906
|
+
/>
|
|
907
|
+
</FormControl>
|
|
908
|
+
</FormItem>
|
|
909
|
+
)}
|
|
910
|
+
/>
|
|
911
|
+
|
|
912
|
+
{form.watch("upn_qr_enabled") && (
|
|
913
|
+
<div className="mt-4 space-y-4">
|
|
914
|
+
<FormField
|
|
915
|
+
control={form.control}
|
|
916
|
+
name="upn_qr_display_mode"
|
|
917
|
+
render={({ field }) => (
|
|
918
|
+
<FormItem className="space-y-3">
|
|
919
|
+
<FormLabel className="font-medium text-sm">{translate("Display Mode")}</FormLabel>
|
|
920
|
+
<FormControl>
|
|
921
|
+
<RadioGroup
|
|
922
|
+
onValueChange={field.onChange}
|
|
923
|
+
value={field.value}
|
|
924
|
+
className="flex flex-col space-y-2"
|
|
925
|
+
>
|
|
926
|
+
<div className="flex items-center space-x-3">
|
|
927
|
+
<RadioGroupItem value="qr_only" id="qr_only" />
|
|
928
|
+
<Label htmlFor="qr_only" className="cursor-pointer font-normal">
|
|
929
|
+
<span className="font-medium">{translate("QR code only")}</span>
|
|
930
|
+
<span className="block text-muted-foreground text-xs">
|
|
931
|
+
{translate("Shows compact QR code inline with invoice content")}
|
|
932
|
+
</span>
|
|
933
|
+
</Label>
|
|
934
|
+
</div>
|
|
935
|
+
<div className="flex items-center space-x-3">
|
|
936
|
+
<RadioGroupItem value="full_slip" id="full_slip" />
|
|
937
|
+
<Label htmlFor="full_slip" className="cursor-pointer font-normal">
|
|
938
|
+
<span className="font-medium">{translate("Full UPN payment slip")}</span>
|
|
939
|
+
<span className="block text-muted-foreground text-xs">
|
|
940
|
+
{translate("Shows complete payment slip at bottom of page")}
|
|
941
|
+
</span>
|
|
942
|
+
</Label>
|
|
943
|
+
</div>
|
|
944
|
+
</RadioGroup>
|
|
945
|
+
</FormControl>
|
|
946
|
+
<FormMessage />
|
|
947
|
+
</FormItem>
|
|
948
|
+
)}
|
|
949
|
+
/>
|
|
950
|
+
|
|
951
|
+
<FormField
|
|
952
|
+
control={form.control}
|
|
953
|
+
name="upn_qr_purpose_code"
|
|
954
|
+
render={({ field }) => (
|
|
955
|
+
<FormItem className="max-w-xs">
|
|
956
|
+
<FormLabel className="font-medium text-sm">{translate("Purpose Code")}</FormLabel>
|
|
957
|
+
<Select onValueChange={field.onChange} value={field.value || "OTHR"}>
|
|
958
|
+
<FormControl>
|
|
959
|
+
<SelectTrigger className="h-10">
|
|
960
|
+
<SelectValue />
|
|
961
|
+
</SelectTrigger>
|
|
962
|
+
</FormControl>
|
|
963
|
+
<SelectContent>
|
|
964
|
+
<SelectItem value="OTHR">{translate("OTHR - Other")}</SelectItem>
|
|
965
|
+
<SelectItem value="GDSV">{translate("GDSV - Goods and Services")}</SelectItem>
|
|
966
|
+
<SelectItem value="SUPP">{translate("SUPP - Supplier Payment")}</SelectItem>
|
|
967
|
+
</SelectContent>
|
|
968
|
+
</Select>
|
|
969
|
+
<FormDescription className="text-xs">
|
|
970
|
+
{translate("Payment purpose code (ISO 20022)")}
|
|
971
|
+
</FormDescription>
|
|
972
|
+
<FormMessage />
|
|
973
|
+
</FormItem>
|
|
974
|
+
)}
|
|
975
|
+
/>
|
|
976
|
+
</div>
|
|
977
|
+
)}
|
|
978
|
+
</div>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
|
|
982
|
+
{/* Help Content */}
|
|
983
|
+
<div className="hidden lg:block">
|
|
984
|
+
<div className="sticky top-6 space-y-3 border-muted border-l-2 pl-4">
|
|
985
|
+
<div className="flex items-start gap-2">
|
|
986
|
+
<CreditCard className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
987
|
+
<div className="space-y-2">
|
|
988
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("UPN QR Payments")}</p>
|
|
989
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
990
|
+
{translate(
|
|
991
|
+
"UPN QR is a Slovenian standard for payment slips. When enabled, your invoices will include a QR code that customers can scan with their mobile banking app to pay instantly.",
|
|
992
|
+
)}
|
|
993
|
+
</p>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
</div>
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
)}
|
|
1000
|
+
|
|
1001
|
+
{/* Document Defaults Section */}
|
|
1002
|
+
<div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
|
|
1003
|
+
<div className="space-y-4">
|
|
1004
|
+
<div className="flex items-center gap-3">
|
|
1005
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500/10">
|
|
1006
|
+
<FileText className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
|
1007
|
+
</div>
|
|
1008
|
+
<div>
|
|
1009
|
+
<h3 className="font-semibold text-lg">{translate("Document Defaults")}</h3>
|
|
1010
|
+
<p className="text-muted-foreground text-sm">{translate("Default values for new documents")}</p>
|
|
1011
|
+
</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<div className="space-y-6 pl-[52px]">
|
|
1015
|
+
<FormField
|
|
1016
|
+
control={form.control}
|
|
1017
|
+
name="default_invoice_note"
|
|
1018
|
+
render={({ field }) => (
|
|
1019
|
+
<FormItem>
|
|
1020
|
+
<div className="flex items-center justify-between">
|
|
1021
|
+
<FormLabel className="font-medium text-sm">{translate("Default Invoice Note")}</FormLabel>
|
|
1022
|
+
<SmartCodeInsertButton
|
|
1023
|
+
textareaRef={defaultInvoiceNoteRef}
|
|
1024
|
+
value={field.value || ""}
|
|
1025
|
+
onInsert={(newValue) => field.onChange(newValue)}
|
|
1026
|
+
t={translate}
|
|
1027
|
+
/>
|
|
1028
|
+
</div>
|
|
1029
|
+
<FormControl>
|
|
1030
|
+
<InputWithPreview
|
|
1031
|
+
ref={defaultInvoiceNoteRef}
|
|
1032
|
+
value={field.value || ""}
|
|
1033
|
+
onChange={field.onChange}
|
|
1034
|
+
placeholder={translate(
|
|
1035
|
+
"Payment due by {document_due_date}. Please reference invoice {document_number}.",
|
|
1036
|
+
)}
|
|
1037
|
+
entity={entity}
|
|
1038
|
+
multiline
|
|
1039
|
+
rows={3}
|
|
1040
|
+
className="resize-y"
|
|
1041
|
+
/>
|
|
1042
|
+
</FormControl>
|
|
1043
|
+
<FormDescription className="text-xs">
|
|
1044
|
+
{translate("This note will be pre-filled when creating new invoices")}
|
|
1045
|
+
</FormDescription>
|
|
1046
|
+
<FormMessage />
|
|
1047
|
+
</FormItem>
|
|
1048
|
+
)}
|
|
1049
|
+
/>
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
|
|
1053
|
+
{/* Help Content */}
|
|
1054
|
+
<div className="hidden lg:block">
|
|
1055
|
+
<div className="sticky top-6 space-y-6 border-muted border-l-2 pl-4">
|
|
1056
|
+
<div className="flex items-start gap-2">
|
|
1057
|
+
<FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
1058
|
+
<div className="space-y-2">
|
|
1059
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("Invoice Notes")}</p>
|
|
1060
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
1061
|
+
{translate(
|
|
1062
|
+
"Set a default note that will appear on all new invoices. Use template variables to personalize the note automatically.",
|
|
1063
|
+
)}
|
|
1064
|
+
</p>
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div className="space-y-3">
|
|
1068
|
+
<div className="flex items-start gap-2">
|
|
1069
|
+
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
1070
|
+
<div className="space-y-2">
|
|
1071
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("Smart Template Variables")}</p>
|
|
1072
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
1073
|
+
{translate("Use variables to personalize your notes automatically")}
|
|
1074
|
+
</p>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
<EmailTemplateVariablesInfo translate={translate} />
|
|
1078
|
+
</div>
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
|
|
1083
|
+
{/* Email Settings Section */}
|
|
1084
|
+
<div className="grid gap-8 border-t pt-8 lg:grid-cols-2">
|
|
1085
|
+
<div className="space-y-4">
|
|
1086
|
+
<div className="flex items-center gap-3">
|
|
1087
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10">
|
|
1088
|
+
<Mail className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
|
1089
|
+
</div>
|
|
1090
|
+
<div>
|
|
1091
|
+
<h3 className="font-semibold text-lg">{translate("Email Settings")}</h3>
|
|
1092
|
+
<p className="text-muted-foreground text-sm">{translate("Configure email settings for invoices")}</p>
|
|
1093
|
+
</div>
|
|
1094
|
+
</div>
|
|
1095
|
+
|
|
1096
|
+
<div className="space-y-6 pl-[52px]">
|
|
1097
|
+
<FormField
|
|
1098
|
+
control={form.control}
|
|
1099
|
+
name="email"
|
|
1100
|
+
render={({ field }) => (
|
|
1101
|
+
<FormItem>
|
|
1102
|
+
<FormLabel className="font-medium text-sm">{translate("Email Address")}</FormLabel>
|
|
1103
|
+
<FormControl>
|
|
1104
|
+
<div className="relative">
|
|
1105
|
+
<Mail className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
1106
|
+
<Input
|
|
1107
|
+
type="email"
|
|
1108
|
+
{...field}
|
|
1109
|
+
value={field.value || ""}
|
|
1110
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
1111
|
+
placeholder="invoices@example.com"
|
|
1112
|
+
className="h-10 pl-10"
|
|
1113
|
+
/>
|
|
1114
|
+
</div>
|
|
1115
|
+
</FormControl>
|
|
1116
|
+
<FormDescription className="text-xs">
|
|
1117
|
+
{translate("Email address to send invoices to")}
|
|
1118
|
+
</FormDescription>
|
|
1119
|
+
<FormMessage />
|
|
1120
|
+
</FormItem>
|
|
1121
|
+
)}
|
|
1122
|
+
/>
|
|
1123
|
+
|
|
1124
|
+
<div className="border-t pt-6">
|
|
1125
|
+
<div className="mb-4 flex items-center gap-2">
|
|
1126
|
+
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
|
1127
|
+
<p className="font-medium text-muted-foreground text-xs">Default Email Templates</p>
|
|
1128
|
+
</div>
|
|
1129
|
+
|
|
1130
|
+
<Tabs defaultValue="invoice" className="w-full">
|
|
1131
|
+
<TabsList className="w-full">
|
|
1132
|
+
<TabsTrigger value="invoice" className="cursor-pointer">
|
|
1133
|
+
{translate("Invoice")}
|
|
1134
|
+
</TabsTrigger>
|
|
1135
|
+
<TabsTrigger value="estimate" className="cursor-pointer">
|
|
1136
|
+
{translate("Estimate")}
|
|
1137
|
+
</TabsTrigger>
|
|
1138
|
+
</TabsList>
|
|
1139
|
+
|
|
1140
|
+
<TabsContent value="invoice" className="mt-4 space-y-4">
|
|
1141
|
+
<FormField
|
|
1142
|
+
control={form.control}
|
|
1143
|
+
name="invoice_email_subject"
|
|
1144
|
+
render={({ field }) => (
|
|
1145
|
+
<FormItem>
|
|
1146
|
+
<div className="flex items-center justify-between">
|
|
1147
|
+
<FormLabel className="font-medium text-sm">{translate("Email Subject")}</FormLabel>
|
|
1148
|
+
<SmartCodeInsertButton
|
|
1149
|
+
textareaRef={invoiceEmailSubjectRef as React.RefObject<HTMLTextAreaElement | null>}
|
|
1150
|
+
value={field.value || ""}
|
|
1151
|
+
onInsert={(newValue) => field.onChange(newValue)}
|
|
1152
|
+
t={translate}
|
|
1153
|
+
/>
|
|
1154
|
+
</div>
|
|
1155
|
+
<FormControl>
|
|
1156
|
+
<InputWithPreview
|
|
1157
|
+
ref={invoiceEmailSubjectRef}
|
|
1158
|
+
value={field.value || ""}
|
|
1159
|
+
onChange={field.onChange}
|
|
1160
|
+
placeholder="Invoice {document_number} from {entity_name}"
|
|
1161
|
+
entity={entity}
|
|
1162
|
+
className="h-10"
|
|
1163
|
+
/>
|
|
1164
|
+
</FormControl>
|
|
1165
|
+
<FormDescription className="text-xs">
|
|
1166
|
+
{translate("Subject line for invoice emails")}
|
|
1167
|
+
</FormDescription>
|
|
1168
|
+
<FormMessage />
|
|
1169
|
+
</FormItem>
|
|
1170
|
+
)}
|
|
1171
|
+
/>
|
|
1172
|
+
|
|
1173
|
+
<FormField
|
|
1174
|
+
control={form.control}
|
|
1175
|
+
name="invoice_email_body"
|
|
1176
|
+
render={({ field }) => (
|
|
1177
|
+
<FormItem>
|
|
1178
|
+
<div className="flex items-center justify-between">
|
|
1179
|
+
<FormLabel className="font-medium text-sm">{translate("Email Body")}</FormLabel>
|
|
1180
|
+
<SmartCodeInsertButton
|
|
1181
|
+
textareaRef={invoiceEmailBodyRef}
|
|
1182
|
+
value={field.value || ""}
|
|
1183
|
+
onInsert={(newValue) => field.onChange(newValue)}
|
|
1184
|
+
t={translate}
|
|
1185
|
+
/>
|
|
1186
|
+
</div>
|
|
1187
|
+
<FormControl>
|
|
1188
|
+
<InputWithPreview
|
|
1189
|
+
ref={invoiceEmailBodyRef}
|
|
1190
|
+
value={field.value || ""}
|
|
1191
|
+
onChange={field.onChange}
|
|
1192
|
+
placeholder="Please find your invoice attached."
|
|
1193
|
+
entity={entity}
|
|
1194
|
+
multiline
|
|
1195
|
+
className="min-h-[200px] resize-none"
|
|
1196
|
+
rows={8}
|
|
1197
|
+
/>
|
|
1198
|
+
</FormControl>
|
|
1199
|
+
<FormDescription className="text-xs">
|
|
1200
|
+
{translate("Body content for invoice emails")}
|
|
1201
|
+
</FormDescription>
|
|
1202
|
+
<FormMessage />
|
|
1203
|
+
</FormItem>
|
|
1204
|
+
)}
|
|
1205
|
+
/>
|
|
1206
|
+
</TabsContent>
|
|
1207
|
+
|
|
1208
|
+
<TabsContent value="estimate" className="mt-4 space-y-4">
|
|
1209
|
+
<FormField
|
|
1210
|
+
control={form.control}
|
|
1211
|
+
name="estimate_email_subject"
|
|
1212
|
+
render={({ field }) => (
|
|
1213
|
+
<FormItem>
|
|
1214
|
+
<div className="flex items-center justify-between">
|
|
1215
|
+
<FormLabel className="font-medium text-sm">{translate("Email Subject")}</FormLabel>
|
|
1216
|
+
<SmartCodeInsertButton
|
|
1217
|
+
textareaRef={estimateEmailSubjectRef as React.RefObject<HTMLTextAreaElement | null>}
|
|
1218
|
+
value={field.value || ""}
|
|
1219
|
+
onInsert={(newValue) => field.onChange(newValue)}
|
|
1220
|
+
t={translate}
|
|
1221
|
+
/>
|
|
1222
|
+
</div>
|
|
1223
|
+
<FormControl>
|
|
1224
|
+
<InputWithPreview
|
|
1225
|
+
ref={estimateEmailSubjectRef}
|
|
1226
|
+
value={field.value || ""}
|
|
1227
|
+
onChange={field.onChange}
|
|
1228
|
+
placeholder="Estimate {document_number} from {entity_name}"
|
|
1229
|
+
entity={entity}
|
|
1230
|
+
className="h-10"
|
|
1231
|
+
/>
|
|
1232
|
+
</FormControl>
|
|
1233
|
+
<FormDescription className="text-xs">
|
|
1234
|
+
{translate("Subject line for estimate emails")}
|
|
1235
|
+
</FormDescription>
|
|
1236
|
+
<FormMessage />
|
|
1237
|
+
</FormItem>
|
|
1238
|
+
)}
|
|
1239
|
+
/>
|
|
1240
|
+
|
|
1241
|
+
<FormField
|
|
1242
|
+
control={form.control}
|
|
1243
|
+
name="estimate_email_body"
|
|
1244
|
+
render={({ field }) => (
|
|
1245
|
+
<FormItem>
|
|
1246
|
+
<div className="flex items-center justify-between">
|
|
1247
|
+
<FormLabel className="font-medium text-sm">{translate("Email Body")}</FormLabel>
|
|
1248
|
+
<SmartCodeInsertButton
|
|
1249
|
+
textareaRef={estimateEmailBodyRef}
|
|
1250
|
+
value={field.value || ""}
|
|
1251
|
+
onInsert={(newValue) => field.onChange(newValue)}
|
|
1252
|
+
t={translate}
|
|
1253
|
+
/>
|
|
1254
|
+
</div>
|
|
1255
|
+
<FormControl>
|
|
1256
|
+
<InputWithPreview
|
|
1257
|
+
ref={estimateEmailBodyRef}
|
|
1258
|
+
value={field.value || ""}
|
|
1259
|
+
onChange={field.onChange}
|
|
1260
|
+
placeholder="Please find your estimate attached."
|
|
1261
|
+
entity={entity}
|
|
1262
|
+
multiline
|
|
1263
|
+
className="min-h-[200px] resize-none"
|
|
1264
|
+
rows={8}
|
|
1265
|
+
/>
|
|
1266
|
+
</FormControl>
|
|
1267
|
+
<FormDescription className="text-xs">
|
|
1268
|
+
{translate("Body content for estimate emails")}
|
|
1269
|
+
</FormDescription>
|
|
1270
|
+
<FormMessage />
|
|
1271
|
+
</FormItem>
|
|
1272
|
+
)}
|
|
1273
|
+
/>
|
|
1274
|
+
</TabsContent>
|
|
1275
|
+
</Tabs>
|
|
1276
|
+
</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
</div>
|
|
1279
|
+
|
|
1280
|
+
{/* Help Content */}
|
|
1281
|
+
<div className="hidden lg:block">
|
|
1282
|
+
<div className="sticky top-6 space-y-6 border-muted border-l-2 pl-4">
|
|
1283
|
+
<div className="space-y-3">
|
|
1284
|
+
<div className="flex items-start gap-2">
|
|
1285
|
+
<Mail className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
1286
|
+
<div className="space-y-2">
|
|
1287
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("Email Defaults")}</p>
|
|
1288
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
1289
|
+
{translate(
|
|
1290
|
+
"Configure default email templates for sending invoices and estimates to your customers. Customize the subject line and body for each document type.",
|
|
1291
|
+
)}
|
|
1292
|
+
</p>
|
|
1293
|
+
</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
</div>
|
|
1296
|
+
<div className="space-y-3">
|
|
1297
|
+
<div className="flex items-start gap-2">
|
|
1298
|
+
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
1299
|
+
<div className="space-y-2">
|
|
1300
|
+
<p className="font-medium text-muted-foreground text-sm">{translate("Smart Template Variables")}</p>
|
|
1301
|
+
<p className="text-muted-foreground/80 text-xs leading-relaxed">
|
|
1302
|
+
{translate("Use variables to personalize your emails automatically")}
|
|
1303
|
+
</p>
|
|
1304
|
+
</div>
|
|
1305
|
+
</div>
|
|
1306
|
+
<EmailTemplateVariablesInfo translate={translate} />
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
</div>
|
|
1310
|
+
</div>
|
|
1311
|
+
|
|
1312
|
+
<div className="flex items-center justify-end gap-3 pt-4">
|
|
1313
|
+
<Button type="submit" className="cursor-pointer px-8" disabled={isPending} aria-busy={isPending} size="lg">
|
|
1314
|
+
{isPending ? <ButtonLoader /> : translate("Save Settings")}
|
|
1315
|
+
</Button>
|
|
1316
|
+
</div>
|
|
1317
|
+
|
|
1318
|
+
{form.formState.errors.root && (
|
|
1319
|
+
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3">
|
|
1320
|
+
<p className="font-medium text-destructive text-sm">{form.formState.errors.root.message}</p>
|
|
1321
|
+
</div>
|
|
1322
|
+
)}
|
|
1323
|
+
</form>
|
|
1324
|
+
</Form>
|
|
1325
|
+
);
|
|
1326
|
+
}
|