@spaceinvoices/react-ui 0.4.8 → 0.4.11
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/README.md +24 -8
- package/cli/dist/index.js +89 -26
- package/package.json +4 -1
- package/spaceinvoices.schema.json +6 -1
- package/src/common/autocomplete.tsx +69 -6
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +124 -285
- package/src/components/advance-invoices/list/list-table.tsx +10 -3
- package/src/components/advance-invoices/list/locales/de.ts +2 -0
- package/src/components/advance-invoices/list/locales/en.ts +1 -0
- package/src/components/advance-invoices/list/locales/es.ts +1 -0
- package/src/components/advance-invoices/list/locales/fr.ts +1 -0
- package/src/components/advance-invoices/list/locales/hr.ts +1 -0
- package/src/components/advance-invoices/list/locales/it.ts +1 -0
- package/src/components/advance-invoices/list/locales/nl.ts +1 -0
- package/src/components/advance-invoices/list/locales/pl.ts +1 -0
- package/src/components/advance-invoices/list/locales/pt.ts +1 -0
- package/src/components/advance-invoices/list/locales/sl.ts +1 -0
- package/src/components/advance-invoices/list/use-advance-invoice-download.ts +1 -12
- package/src/components/credit-notes/create/create-credit-note-form.tsx +116 -238
- package/src/components/credit-notes/list/list-table.tsx +6 -3
- package/src/components/credit-notes/list/use-credit-note-download.ts +1 -12
- package/src/components/customers/customer-autocomplete.tsx +64 -11
- package/src/components/customers/customer-list-table/customer-list-table.tsx +3 -2
- package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +9 -1
- package/src/components/dashboard/collection-rate-card/locales/bg.ts +3 -0
- package/src/components/dashboard/collection-rate-card/locales/cs.ts +3 -0
- package/src/components/dashboard/collection-rate-card/locales/et.ts +3 -0
- package/src/components/dashboard/collection-rate-card/locales/fi.ts +3 -0
- package/src/components/dashboard/collection-rate-card/locales/is.ts +3 -0
- package/src/components/dashboard/collection-rate-card/locales/nb.ts +3 -0
- package/src/components/dashboard/collection-rate-card/locales/sk.ts +3 -0
- package/src/components/dashboard/collection-rate-card/locales/sv.ts +3 -0
- package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +10 -2
- package/src/components/dashboard/invoice-status-chart/locales/bg.ts +10 -0
- package/src/components/dashboard/invoice-status-chart/locales/cs.ts +10 -0
- package/src/components/dashboard/invoice-status-chart/locales/de.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/es.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/et.ts +10 -0
- package/src/components/dashboard/invoice-status-chart/locales/fi.ts +10 -0
- package/src/components/dashboard/invoice-status-chart/locales/fr.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/hr.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/is.ts +10 -0
- package/src/components/dashboard/invoice-status-chart/locales/it.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/nb.ts +10 -0
- package/src/components/dashboard/invoice-status-chart/locales/nl.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/pl.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/pt.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/sk.ts +10 -0
- package/src/components/dashboard/invoice-status-chart/locales/sl.ts +1 -0
- package/src/components/dashboard/invoice-status-chart/locales/sv.ts +10 -0
- package/src/components/dashboard/payment-methods-chart/locales/bg.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/locales/cs.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/locales/et.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/locales/fi.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/locales/is.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/locales/nb.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/locales/sk.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/locales/sv.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +9 -1
- package/src/components/dashboard/payment-trend-chart/locales/bg.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/locales/cs.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/locales/de.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/es.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/et.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/locales/fi.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/locales/fr.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/hr.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/is.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/locales/it.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/nb.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/locales/nl.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/pl.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/pt.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/sk.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/locales/sl.ts +1 -0
- package/src/components/dashboard/payment-trend-chart/locales/sv.ts +6 -0
- package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +15 -8
- package/src/components/dashboard/revenue-trend-chart/locales/bg.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/locales/cs.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/locales/de.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/es.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/et.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/locales/fi.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/locales/fr.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/hr.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/is.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/locales/it.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/nb.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/locales/nl.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/pl.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/pt.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/sk.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +1 -0
- package/src/components/dashboard/revenue-trend-chart/locales/sv.ts +6 -0
- package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +15 -8
- package/src/components/dashboard/tax-collected-card/locales.ts +110 -0
- package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +8 -2
- package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +4 -4
- package/src/components/dashboard/top-customers-chart/locales/bg.ts +7 -0
- package/src/components/dashboard/top-customers-chart/locales/cs.ts +7 -0
- package/src/components/dashboard/top-customers-chart/locales/de.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/es.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/et.ts +7 -0
- package/src/components/dashboard/top-customers-chart/locales/fi.ts +7 -0
- package/src/components/dashboard/top-customers-chart/locales/fr.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/hr.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/is.ts +7 -0
- package/src/components/dashboard/top-customers-chart/locales/it.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/nb.ts +7 -0
- package/src/components/dashboard/top-customers-chart/locales/nl.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/pl.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/pt.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/sk.ts +7 -0
- package/src/components/dashboard/top-customers-chart/locales/sl.ts +2 -0
- package/src/components/dashboard/top-customers-chart/locales/sv.ts +7 -0
- package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +23 -12
- package/src/components/delivery-notes/create/create-delivery-note-form.tsx +33 -20
- package/src/components/delivery-notes/list/list-table.tsx +22 -13
- package/src/components/delivery-notes/list/locales/de.ts +2 -0
- package/src/components/delivery-notes/list/locales/en.ts +1 -0
- package/src/components/delivery-notes/list/locales/es.ts +1 -0
- package/src/components/delivery-notes/list/locales/fr.ts +1 -0
- package/src/components/delivery-notes/list/locales/hr.ts +1 -0
- package/src/components/delivery-notes/list/locales/it.ts +1 -0
- package/src/components/delivery-notes/list/locales/nl.ts +1 -0
- package/src/components/delivery-notes/list/locales/pl.ts +1 -0
- package/src/components/delivery-notes/list/locales/pt.ts +1 -0
- package/src/components/delivery-notes/list/locales/sl.ts +1 -0
- package/src/components/delivery-notes/list/use-delivery-note-download.ts +1 -12
- package/src/components/documents/create/document-add-item-form.tsx +28 -16
- package/src/components/documents/create/document-add-item-tax-rate-field.tsx +12 -2
- package/src/components/documents/create/document-items-section.tsx +70 -39
- package/src/components/documents/create/document-recipient-section.tsx +10 -1
- package/src/components/documents/create/live-preview.tsx +113 -15
- package/src/components/documents/create/prepare-document-submission.ts +35 -16
- package/src/components/documents/create/use-document-customer-form.ts +14 -3
- package/src/components/documents/documents.hooks.ts +7 -2
- package/src/components/documents/shared/document-preview-display.tsx +136 -67
- package/src/components/documents/shared/scaled-document-preview.tsx +45 -5
- package/src/components/documents/view/document-actions-bar.tsx +284 -182
- package/src/components/documents/view/document-activities-list.tsx +3 -0
- package/src/components/documents/view/document-payments-list.tsx +3 -0
- package/src/components/documents/view/locales/de.ts +8 -0
- package/src/components/documents/view/locales/es.ts +8 -0
- package/src/components/documents/view/locales/fr.ts +8 -0
- package/src/components/documents/view/locales/hr.ts +8 -0
- package/src/components/documents/view/locales/it.ts +8 -0
- package/src/components/documents/view/locales/nl.ts +8 -0
- package/src/components/documents/view/locales/pl.ts +8 -0
- package/src/components/documents/view/locales/pt.ts +8 -0
- package/src/components/documents/view/locales/sl.ts +8 -0
- package/src/components/documents/view/use-document-download.ts +14 -25
- package/src/components/entities/create-entity-form.tsx +101 -16
- package/src/components/entities/entity-settings-form/entity-settings-form.tsx +10 -10
- package/src/components/entities/entity-settings-form/locales/de.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/es.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/fr.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/hr.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/it.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/nl.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/pl.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/pt.ts +10 -0
- package/src/components/entities/entity-settings-form/locales/sl.ts +10 -0
- package/src/components/entities/fina-settings-form/fina-operator-required-dialog.tsx +3 -3
- package/src/components/entities/fina-settings-form/fina-settings-form.tsx +78 -124
- package/src/components/entities/fina-settings-form/sections/certificate-settings-section.tsx +8 -1
- package/src/components/entities/fina-settings-form/sections/premises-management-section.tsx +14 -2
- package/src/components/entities/fina-settings-form/sections/register-premise-dialog.tsx +7 -2
- package/src/components/entities/furs-settings-form/furs-settings-form.tsx +56 -130
- package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +8 -1
- package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +1 -0
- package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +15 -2
- package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +20 -3
- package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +38 -12
- package/src/components/entities/settings/defaults-settings-form.tsx +6 -6
- package/src/components/entities/settings/eslog-settings-form.tsx +13 -1
- package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +3 -22
- package/src/components/entities/shared/fiscalization-step-flow.ts +77 -0
- package/src/components/entities/shared/fiscalization-step-tabs.tsx +71 -0
- package/src/components/estimates/create/create-estimate-form.tsx +34 -21
- package/src/components/estimates/list/list-table.tsx +23 -14
- package/src/components/estimates/list/locales/de.ts +2 -0
- package/src/components/estimates/list/locales/en.ts +1 -0
- package/src/components/estimates/list/locales/es.ts +1 -0
- package/src/components/estimates/list/locales/fr.ts +1 -0
- package/src/components/estimates/list/locales/hr.ts +1 -0
- package/src/components/estimates/list/locales/it.ts +1 -0
- package/src/components/estimates/list/locales/nl.ts +1 -0
- package/src/components/estimates/list/locales/pl.ts +1 -0
- package/src/components/estimates/list/locales/pt.ts +1 -0
- package/src/components/estimates/list/locales/sl.ts +1 -0
- package/src/components/estimates/list/use-estimate-download.ts +1 -12
- package/src/components/export/document-export-form.tsx +33 -7
- package/src/components/export/sales-per-item-export-form.tsx +23 -7
- package/src/components/invoices/create/create-invoice-form.tsx +295 -329
- package/src/components/invoices/create/prepare-invoice-submission.ts +0 -8
- package/src/components/invoices/list/list-table.tsx +7 -4
- package/src/components/invoices/list/use-invoice-download.ts +1 -11
- package/src/components/invoices/send-email-dialog/locales/de.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/es.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/fr.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/hr.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/it.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/nl.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/pl.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/pt.ts +20 -0
- package/src/components/invoices/send-email-dialog/locales/sl.ts +20 -0
- package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +77 -8
- package/src/components/invoices/view/eslog-info-display.tsx +17 -1
- package/src/components/invoices/view/fiscalization-status-card.tsx +7 -3
- package/src/components/items/item-combobox.tsx +26 -6
- package/src/components/items/item-list-table/item-list-table.tsx +5 -2
- package/src/components/payments/create-payment-form/index.ts +1 -0
- package/src/components/payments/list/list-table.tsx +14 -4
- package/src/components/recurring-invoices/list/list-table.tsx +7 -4
- package/src/components/request-logs/locales.ts +412 -0
- package/src/components/request-logs/request-log-detail.tsx +37 -21
- package/src/components/request-logs/request-log-list-table.tsx +57 -11
- package/src/components/table/data-table.tsx +5 -2
- package/src/components/table/date-cell.tsx +3 -1
- package/src/components/table/filter-bar.tsx +14 -2
- package/src/components/table/hooks/use-table-query.ts +1 -1
- package/src/components/table/locales.ts +1116 -0
- package/src/components/table/search-input.tsx +12 -3
- package/src/components/table/selection-toolbar.tsx +23 -6
- package/src/components/table/table-empty-state.tsx +43 -3
- package/src/components/table/table-no-results.tsx +3 -3
- package/src/components/table/table-pagination.tsx +4 -3
- package/src/components/table/types.ts +1 -0
- package/src/components/tax-reports/index.ts +1 -0
- package/src/components/tax-reports/kir-export-form.tsx +46 -8
- package/src/components/tax-reports/slovenia-tax-profile-step.tsx +191 -0
- package/src/components/tax-reports/slovenia-yearly-export-form.tsx +509 -0
- package/src/components/tax-reports/slovenia-yearly-review-step.tsx +253 -0
- package/src/components/tax-reports/slovenia-yearly-summary.tsx +19 -0
- package/src/components/taxes/tax-list-table/tax-list-table.tsx +3 -2
- package/src/components/ui/sidebar.tsx +3 -2
- package/src/components/ui/sticky-form-footer.tsx +7 -1
- package/src/components/webhook-logs/index.ts +6 -0
- package/src/components/webhook-logs/locales.ts +392 -0
- package/src/components/webhook-logs/webhook-delivery-detail.tsx +255 -0
- package/src/components/webhook-logs/webhook-delivery-list-table.tsx +278 -0
- package/src/components/wl-subscription/index.ts +1 -0
- package/src/components/wl-subscription/locked-feature.tsx +1 -0
- package/src/components/wl-subscription/paywall.tsx +193 -0
- package/src/components/wl-subscription/upgrade-modal.tsx +93 -29
- package/src/generate-schemas.ts +12 -7
- package/src/generated/schemas/customer.ts +2 -0
- package/src/generated/schemas/entity.ts +134 -0
- package/src/generated/schemas/exportsloveniayearlynormiranireport_body.ts +27 -0
- package/src/generated/schemas/index.ts +2 -0
- package/src/generated/schemas/me.ts +20 -1
- package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +40 -34
- package/src/generated/schemas/rendercreditnotepreview_body.ts +42 -36
- package/src/generated/schemas/renderdeliverynotepreview_body.ts +23 -13
- package/src/generated/schemas/renderestimatepreview_body.ts +23 -13
- package/src/generated/schemas/renderinvoicepreview_body.ts +40 -34
- package/src/generated/schemas/sendemail_body.ts +44 -0
- package/src/generated/schemas/sloveniataxprofile.ts +42 -0
- package/src/generated/schemas/startpdfexport_body.ts +91 -1
- package/src/generated/schemas/webhook.ts +10 -0
- package/src/hooks/use-duplicate-document.ts +51 -13
- package/src/hooks/use-eslog-validation.ts +59 -0
- package/src/hooks/use-premise-selection.ts +186 -0
- package/src/lib/browser-cookies.ts +4 -4
- package/src/lib/date-fns-locale.ts +48 -0
- package/src/lib/fiscalization-options.ts +81 -0
- package/src/lib/locale.ts +38 -0
- package/src/lib/template-variables.tsx +1 -1
- package/src/lib/translation.ts +14 -3
- package/src/providers/entities-context.tsx +1 -0
- package/src/providers/entities-provider.tsx +102 -3
- package/src/providers/form-footer-context.tsx +37 -4
- package/src/providers/sdk-provider.tsx +7 -2
- package/src/providers/white-label-provider.tsx +4 -1
- package/src/providers/wl-subscription-provider.tsx +90 -3
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { PlusIcon, SeparatorHorizontal } from "lucide-react";
|
|
6
6
|
import type { MutableRefObject } from "react";
|
|
7
7
|
import type { UseFormGetValues, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
|
8
|
+
import { useFieldArray } from "react-hook-form";
|
|
8
9
|
import { Button } from "@/ui/components/ui/button";
|
|
9
10
|
import DocumentAddItemForm from "./document-add-item-form";
|
|
10
11
|
import type { AnyControl } from "./form-types";
|
|
@@ -12,6 +13,14 @@ import type { AnyControl } from "./form-types";
|
|
|
12
13
|
/** Map of item index to gross price mode */
|
|
13
14
|
export type PriceModesMap = Record<number, boolean>;
|
|
14
15
|
|
|
16
|
+
function reindexPriceModes(priceModes: PriceModesMap, nextLength: number): PriceModesMap {
|
|
17
|
+
const reindexed: PriceModesMap = {};
|
|
18
|
+
for (let index = 0; index < nextLength; index += 1) {
|
|
19
|
+
reindexed[index] = priceModes[index] ?? false;
|
|
20
|
+
}
|
|
21
|
+
return reindexed;
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
type DocumentItemsSectionProps = {
|
|
16
25
|
control: AnyControl;
|
|
17
26
|
|
|
@@ -34,6 +43,8 @@ type DocumentItemsSectionProps = {
|
|
|
34
43
|
priceModesRef?: MutableRefObject<PriceModesMap>;
|
|
35
44
|
/** Initial price modes (from duplicated document) */
|
|
36
45
|
initialPriceModes?: PriceModesMap;
|
|
46
|
+
/** Called when item ordering or price mode changes outside normal field edits. */
|
|
47
|
+
onItemsStateChange?: () => void;
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
export function DocumentItemsSection({
|
|
@@ -50,66 +61,85 @@ export function DocumentItemsSection({
|
|
|
50
61
|
maxTaxesPerItem,
|
|
51
62
|
priceModesRef,
|
|
52
63
|
initialPriceModes = {},
|
|
64
|
+
onItemsStateChange,
|
|
53
65
|
}: DocumentItemsSectionProps) {
|
|
66
|
+
const { fields, append, remove, move } = useFieldArray({
|
|
67
|
+
control: control as any,
|
|
68
|
+
name: "items",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const syncPriceModes = (updater: (current: PriceModesMap) => PriceModesMap) => {
|
|
72
|
+
if (!priceModesRef) return;
|
|
73
|
+
priceModesRef.current = updater(priceModesRef.current ?? {});
|
|
74
|
+
};
|
|
75
|
+
|
|
54
76
|
const addItem = () => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
]);
|
|
77
|
+
append({
|
|
78
|
+
name: "",
|
|
79
|
+
description: "",
|
|
80
|
+
quantity: 1,
|
|
81
|
+
price: undefined,
|
|
82
|
+
taxes: [],
|
|
83
|
+
});
|
|
84
|
+
syncPriceModes((current) => ({ ...current, [fields.length]: false }));
|
|
85
|
+
onItemsStateChange?.();
|
|
66
86
|
};
|
|
67
87
|
|
|
68
88
|
const addSeparator = () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
]);
|
|
89
|
+
append({
|
|
90
|
+
type: "separator",
|
|
91
|
+
name: "",
|
|
92
|
+
description: "",
|
|
93
|
+
});
|
|
94
|
+
syncPriceModes((current) => ({ ...current, [fields.length]: false }));
|
|
95
|
+
onItemsStateChange?.();
|
|
78
96
|
};
|
|
79
97
|
|
|
80
98
|
const removeItem = (index: number) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
remove(index);
|
|
100
|
+
syncPriceModes((current) => {
|
|
101
|
+
const next: PriceModesMap = {};
|
|
102
|
+
let nextIndex = 0;
|
|
103
|
+
for (let currentIndex = 0; currentIndex < fields.length; currentIndex += 1) {
|
|
104
|
+
if (currentIndex === index) continue;
|
|
105
|
+
next[nextIndex] = current[currentIndex] ?? false;
|
|
106
|
+
nextIndex += 1;
|
|
107
|
+
}
|
|
108
|
+
return next;
|
|
109
|
+
});
|
|
110
|
+
onItemsStateChange?.();
|
|
86
111
|
};
|
|
87
112
|
|
|
88
113
|
const moveItemUp = (index: number) => {
|
|
89
114
|
if (index === 0) return;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
move(index, index - 1);
|
|
116
|
+
syncPriceModes((current) => {
|
|
117
|
+
const next = reindexPriceModes(current, fields.length);
|
|
118
|
+
[next[index], next[index - 1]] = [next[index - 1], next[index]];
|
|
119
|
+
return next;
|
|
120
|
+
});
|
|
121
|
+
onItemsStateChange?.();
|
|
94
122
|
};
|
|
95
123
|
|
|
96
124
|
const moveItemDown = (index: number) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
if (index === fields.length - 1) return;
|
|
126
|
+
move(index, index + 1);
|
|
127
|
+
syncPriceModes((current) => {
|
|
128
|
+
const next = reindexPriceModes(current, fields.length);
|
|
129
|
+
[next[index], next[index + 1]] = [next[index + 1], next[index]];
|
|
130
|
+
return next;
|
|
131
|
+
});
|
|
132
|
+
onItemsStateChange?.();
|
|
102
133
|
};
|
|
103
134
|
|
|
104
|
-
const items = watch("items") ||
|
|
135
|
+
const items = watch("items") || fields;
|
|
105
136
|
|
|
106
137
|
return (
|
|
107
138
|
<div className="flex flex-col gap-4">
|
|
108
139
|
<h2 className="font-bold text-xl">{t("Items")}</h2>
|
|
109
140
|
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
<div key={index}>
|
|
141
|
+
{fields.map((field, index: number) => (
|
|
142
|
+
<div key={field.id}>
|
|
113
143
|
<DocumentAddItemForm
|
|
114
144
|
form={{ control, watch, setValue, getValues } as any}
|
|
115
145
|
index={index}
|
|
@@ -127,11 +157,12 @@ export function DocumentItemsSection({
|
|
|
127
157
|
taxesDisabled={taxesDisabled}
|
|
128
158
|
taxesDisabledMessage={taxesDisabledMessage}
|
|
129
159
|
maxTaxesPerItem={maxTaxesPerItem}
|
|
130
|
-
initialIsGrossPrice={initialPriceModes[index] ?? false}
|
|
160
|
+
initialIsGrossPrice={priceModesRef?.current[index] ?? initialPriceModes[index] ?? false}
|
|
131
161
|
onPriceModeChange={(isGross) => {
|
|
132
162
|
if (priceModesRef) {
|
|
133
163
|
priceModesRef.current[index] = isGross;
|
|
134
164
|
}
|
|
165
|
+
onItemsStateChange?.();
|
|
135
166
|
}}
|
|
136
167
|
/>
|
|
137
168
|
</div>
|
|
@@ -47,6 +47,10 @@ export function DocumentRecipientSection({
|
|
|
47
47
|
control: control as any,
|
|
48
48
|
name: "customer.is_end_consumer" as any,
|
|
49
49
|
});
|
|
50
|
+
const customerNameController = useController({
|
|
51
|
+
control: control as any,
|
|
52
|
+
name: "customer.name" as any,
|
|
53
|
+
});
|
|
50
54
|
|
|
51
55
|
useEffect(() => {
|
|
52
56
|
if (showCustomerForm && shouldFocusName) {
|
|
@@ -80,16 +84,21 @@ export function DocumentRecipientSection({
|
|
|
80
84
|
<CustomerAutocomplete
|
|
81
85
|
entityId={entityId}
|
|
82
86
|
value={selectedCustomerId}
|
|
87
|
+
committedDisplayName={customerNameController.field.value ?? initialCustomerName}
|
|
83
88
|
onValueChange={onCustomerSelect}
|
|
89
|
+
onCommitInlineName={(nextName) => customerNameController.field.onChange(nextName)}
|
|
84
90
|
onClear={onCustomerClear}
|
|
85
91
|
placeholder={t("Search or create customer...")}
|
|
86
92
|
initialDisplayName={initialCustomerName}
|
|
93
|
+
inputTestId="document-customer-input"
|
|
94
|
+
inputRef={nameInputRef}
|
|
95
|
+
commitOnBlurMode={showCustomerForm ? "update-inline" : "create"}
|
|
87
96
|
/>
|
|
88
97
|
</div>
|
|
89
98
|
|
|
90
99
|
{showCustomerForm && (
|
|
91
100
|
<>
|
|
92
|
-
<FormInput control={control} name="customer.address" placeholder={t("Address")} label=""
|
|
101
|
+
<FormInput control={control} name="customer.address" placeholder={t("Address")} label="" />
|
|
93
102
|
|
|
94
103
|
<FormInput control={control} name="customer.address_2" placeholder={t("Address 2")} label="" />
|
|
95
104
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { CreateInvoiceRequest } from "@spaceinvoices/js-sdk";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
4
5
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
6
|
import { cn } from "@/ui/lib/utils";
|
|
6
7
|
import { useEntities } from "@/ui/providers/entities-context";
|
|
@@ -11,6 +12,14 @@ import { useA4Scaling } from "../shared/use-a4-scaling";
|
|
|
11
12
|
import type { DocumentTypes } from "../types";
|
|
12
13
|
import { filterUnresolvedTaxes } from "./prepare-preview-data";
|
|
13
14
|
|
|
15
|
+
const LIVE_PREVIEW_TIMING_EVENT = "si:live-preview-timing";
|
|
16
|
+
const LIVE_PREVIEW_DEBOUNCE_MS = 300;
|
|
17
|
+
|
|
18
|
+
function emitLivePreviewDebug(detail: Record<string, unknown>) {
|
|
19
|
+
if (!import.meta.env.DEV || typeof window === "undefined") return;
|
|
20
|
+
window.dispatchEvent(new CustomEvent(LIVE_PREVIEW_TIMING_EVENT, { detail }));
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
export type PdfTemplateId = "modern" | "classic" | "condensed" | "minimal" | "fashion";
|
|
15
24
|
|
|
16
25
|
type LiveInvoicePreviewProps = {
|
|
@@ -30,6 +39,8 @@ type LiveInvoicePreviewProps = {
|
|
|
30
39
|
documentTypeLabel?: string;
|
|
31
40
|
/** Document type to determine which render endpoint to use */
|
|
32
41
|
documentType?: DocumentTypes;
|
|
42
|
+
/** Skip debounce for the first non-empty preview request. */
|
|
43
|
+
eagerFirstPreview?: boolean;
|
|
33
44
|
/** QR settings overrides for preview (before saving) */
|
|
34
45
|
qrOverrides?: {
|
|
35
46
|
upn_qr_enabled?: boolean;
|
|
@@ -45,7 +56,7 @@ type LiveInvoicePreviewProps = {
|
|
|
45
56
|
* Uses debouncing to avoid excessive API calls and shows loading states.
|
|
46
57
|
*
|
|
47
58
|
* Features:
|
|
48
|
-
* - Debounced API requests (
|
|
59
|
+
* - Debounced API requests (300ms delay after user stops typing)
|
|
49
60
|
* - Loading skeleton while fetching preview
|
|
50
61
|
* - Error handling with fallback display
|
|
51
62
|
* - Fully styled HTML with scoped CSS (prevents style leakage)
|
|
@@ -60,16 +71,20 @@ export function LiveInvoicePreview({
|
|
|
60
71
|
t: tProp,
|
|
61
72
|
documentTypeLabel: _documentTypeLabel,
|
|
62
73
|
documentType = "invoice",
|
|
74
|
+
eagerFirstPreview = false,
|
|
63
75
|
qrOverrides,
|
|
64
76
|
}: LiveInvoicePreviewProps) {
|
|
65
77
|
const t = tProp ?? ((key: string) => key);
|
|
66
78
|
const [previewHtml, setPreviewHtml] = useState<string>("");
|
|
67
79
|
const [isLoading, setIsLoading] = useState(false);
|
|
80
|
+
const [isRefreshPending, setIsRefreshPending] = useState(false);
|
|
68
81
|
const [error, setError] = useState<string | null>(null);
|
|
69
82
|
const { activeEntity } = useEntities();
|
|
70
83
|
const { sdk } = useSDK();
|
|
71
84
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
72
85
|
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
86
|
+
const lastRequestKeyRef = useRef<string | null>(null);
|
|
87
|
+
const hasDispatchedFirstPreviewRef = useRef(false);
|
|
73
88
|
|
|
74
89
|
const { containerRef, contentRef, scale: dynamicScale, contentHeight, A4_WIDTH_PX } = useA4Scaling(previewHtml);
|
|
75
90
|
const scale = fixedScale ?? dynamicScale;
|
|
@@ -82,6 +97,14 @@ export function LiveInvoicePreview({
|
|
|
82
97
|
// Don't fetch if no items exist (name can be empty for preview)
|
|
83
98
|
if (!invoiceData.items || invoiceData.items.length === 0) {
|
|
84
99
|
setPreviewHtml("");
|
|
100
|
+
lastRequestKeyRef.current = null;
|
|
101
|
+
setIsRefreshPending(false);
|
|
102
|
+
emitLivePreviewDebug({
|
|
103
|
+
stage: "skipped",
|
|
104
|
+
reason: "no_items",
|
|
105
|
+
documentType,
|
|
106
|
+
itemCount: 0,
|
|
107
|
+
});
|
|
85
108
|
return;
|
|
86
109
|
}
|
|
87
110
|
|
|
@@ -95,12 +118,14 @@ export function LiveInvoicePreview({
|
|
|
95
118
|
abortControllerRef.current = abortController;
|
|
96
119
|
|
|
97
120
|
setIsLoading(true);
|
|
121
|
+
setIsRefreshPending(false);
|
|
98
122
|
setError(null);
|
|
99
123
|
|
|
100
124
|
try {
|
|
101
125
|
if (!sdk || !activeEntity?.id) {
|
|
102
126
|
throw new Error("Authentication required");
|
|
103
127
|
}
|
|
128
|
+
const startedAt = performance.now();
|
|
104
129
|
|
|
105
130
|
// Prepare preview data with active entity as issuer (if not already set)
|
|
106
131
|
// Exclude 'number' as it's auto-generated by the render endpoint
|
|
@@ -122,6 +147,30 @@ export function LiveInvoicePreview({
|
|
|
122
147
|
...invoiceData.issuer,
|
|
123
148
|
},
|
|
124
149
|
};
|
|
150
|
+
const requestKey = JSON.stringify({
|
|
151
|
+
documentType,
|
|
152
|
+
template,
|
|
153
|
+
qrOverrides,
|
|
154
|
+
entityId: activeEntity.id,
|
|
155
|
+
previewData,
|
|
156
|
+
});
|
|
157
|
+
if (lastRequestKeyRef.current === requestKey) {
|
|
158
|
+
setIsLoading(false);
|
|
159
|
+
setIsRefreshPending(false);
|
|
160
|
+
emitLivePreviewDebug({
|
|
161
|
+
stage: "deduped",
|
|
162
|
+
documentType,
|
|
163
|
+
itemCount: previewData.items?.length ?? 0,
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
lastRequestKeyRef.current = requestKey;
|
|
168
|
+
emitLivePreviewDebug({
|
|
169
|
+
stage: "request_started",
|
|
170
|
+
documentType,
|
|
171
|
+
itemCount: previewData.items?.length ?? 0,
|
|
172
|
+
template,
|
|
173
|
+
});
|
|
125
174
|
|
|
126
175
|
// Call the render API using the appropriate SDK method for the document type
|
|
127
176
|
// Don't send locale — let entity locale drive formatting (decimal separators, date format)
|
|
@@ -135,7 +184,7 @@ export function LiveInvoicePreview({
|
|
|
135
184
|
if (qrOverrides?.epc_qr_enabled !== undefined) {
|
|
136
185
|
renderParams.epc_qr_enabled = qrOverrides.epc_qr_enabled ? "true" : "false";
|
|
137
186
|
}
|
|
138
|
-
const requestOpts = { entity_id: activeEntity.id };
|
|
187
|
+
const requestOpts = { entity_id: activeEntity.id, signal: abortController.signal };
|
|
139
188
|
let html: string;
|
|
140
189
|
switch (documentType) {
|
|
141
190
|
case "estimate":
|
|
@@ -157,19 +206,40 @@ export function LiveInvoicePreview({
|
|
|
157
206
|
|
|
158
207
|
setPreviewHtml(html);
|
|
159
208
|
setError(null);
|
|
209
|
+
emitLivePreviewDebug({
|
|
210
|
+
stage: "request_succeeded",
|
|
211
|
+
documentType,
|
|
212
|
+
itemCount: previewData.items?.length ?? 0,
|
|
213
|
+
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
214
|
+
});
|
|
160
215
|
} catch (err) {
|
|
161
216
|
// Ignore abort errors (they're expected when user keeps typing)
|
|
162
217
|
if (err instanceof Error && err.name === "AbortError") {
|
|
218
|
+
emitLivePreviewDebug({
|
|
219
|
+
stage: "request_aborted",
|
|
220
|
+
documentType,
|
|
221
|
+
});
|
|
163
222
|
return;
|
|
164
223
|
}
|
|
165
224
|
|
|
166
225
|
// Ignore 422 validation errors - expected while user is still filling the form
|
|
167
226
|
if (err instanceof Error && "status" in err && (err as any).status === 422) {
|
|
227
|
+
emitLivePreviewDebug({
|
|
228
|
+
stage: "request_validation_error",
|
|
229
|
+
documentType,
|
|
230
|
+
status: 422,
|
|
231
|
+
});
|
|
168
232
|
return;
|
|
169
233
|
}
|
|
170
234
|
|
|
171
235
|
setError(err instanceof Error ? err.message : "Failed to generate preview");
|
|
172
236
|
setPreviewHtml("");
|
|
237
|
+
lastRequestKeyRef.current = null;
|
|
238
|
+
emitLivePreviewDebug({
|
|
239
|
+
stage: "request_failed",
|
|
240
|
+
documentType,
|
|
241
|
+
message: err instanceof Error ? err.message : "Failed to generate preview",
|
|
242
|
+
});
|
|
173
243
|
} finally {
|
|
174
244
|
// Only set loading to false if this request wasn't aborted
|
|
175
245
|
if (!abortController.signal.aborted) {
|
|
@@ -190,6 +260,7 @@ export function LiveInvoicePreview({
|
|
|
190
260
|
template,
|
|
191
261
|
sdk,
|
|
192
262
|
documentType,
|
|
263
|
+
qrOverrides,
|
|
193
264
|
qrOverrides?.upn_qr_enabled,
|
|
194
265
|
qrOverrides?.upn_qr_display_mode,
|
|
195
266
|
qrOverrides?.epc_qr_enabled,
|
|
@@ -198,7 +269,7 @@ export function LiveInvoicePreview({
|
|
|
198
269
|
|
|
199
270
|
/**
|
|
200
271
|
* Debounced preview fetch
|
|
201
|
-
* Waits
|
|
272
|
+
* Waits briefly after user stops typing before fetching
|
|
202
273
|
*/
|
|
203
274
|
useEffect(() => {
|
|
204
275
|
// Clear previous timeout
|
|
@@ -206,18 +277,31 @@ export function LiveInvoicePreview({
|
|
|
206
277
|
clearTimeout(debounceTimeoutRef.current);
|
|
207
278
|
}
|
|
208
279
|
|
|
209
|
-
|
|
210
|
-
|
|
280
|
+
const hasItems = !!data.items && data.items.length > 0;
|
|
281
|
+
const shouldFetchImmediately = eagerFirstPreview && hasItems && !hasDispatchedFirstPreviewRef.current;
|
|
282
|
+
|
|
283
|
+
if (shouldFetchImmediately) {
|
|
284
|
+
setIsRefreshPending(false);
|
|
285
|
+
hasDispatchedFirstPreviewRef.current = true;
|
|
211
286
|
fetchPreview(data);
|
|
212
|
-
}
|
|
287
|
+
} else {
|
|
288
|
+
setIsRefreshPending(hasItems);
|
|
289
|
+
// Set new timeout
|
|
290
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
291
|
+
setIsRefreshPending(false);
|
|
292
|
+
hasDispatchedFirstPreviewRef.current = true;
|
|
293
|
+
fetchPreview(data);
|
|
294
|
+
}, LIVE_PREVIEW_DEBOUNCE_MS);
|
|
295
|
+
}
|
|
213
296
|
|
|
214
297
|
// Cleanup
|
|
215
298
|
return () => {
|
|
216
299
|
if (debounceTimeoutRef.current) {
|
|
217
300
|
clearTimeout(debounceTimeoutRef.current);
|
|
218
301
|
}
|
|
302
|
+
setIsRefreshPending(false);
|
|
219
303
|
};
|
|
220
|
-
}, [data, fetchPreview]);
|
|
304
|
+
}, [data, eagerFirstPreview, fetchPreview]);
|
|
221
305
|
|
|
222
306
|
/**
|
|
223
307
|
* Cleanup on unmount
|
|
@@ -232,10 +316,13 @@ export function LiveInvoicePreview({
|
|
|
232
316
|
if (debounceTimeoutRef.current) {
|
|
233
317
|
clearTimeout(debounceTimeoutRef.current);
|
|
234
318
|
}
|
|
319
|
+
setIsRefreshPending(false);
|
|
320
|
+
lastRequestKeyRef.current = null;
|
|
235
321
|
};
|
|
236
322
|
}, []);
|
|
237
323
|
|
|
238
324
|
const showSkeleton = (!previewHtml && !error) || (isLoading && !previewHtml);
|
|
325
|
+
const showRefreshBadge = !!previewHtml && (isRefreshPending || isLoading);
|
|
239
326
|
|
|
240
327
|
return (
|
|
241
328
|
<div ref={containerRef} className={cn("relative", className)}>
|
|
@@ -254,14 +341,25 @@ export function LiveInvoicePreview({
|
|
|
254
341
|
|
|
255
342
|
{/* Preview - Scoped HTML injection with A4 scaling */}
|
|
256
343
|
{previewHtml && !error && (
|
|
257
|
-
<div className=
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
344
|
+
<div className="relative">
|
|
345
|
+
{showRefreshBadge && (
|
|
346
|
+
<div className="absolute top-4 right-4 z-20">
|
|
347
|
+
<div className="inline-flex items-center gap-2 rounded-full border bg-background/95 px-3 py-1.5 text-muted-foreground text-xs shadow-sm">
|
|
348
|
+
<Loader2 className={cn("size-3.5", (isRefreshPending || isLoading) && "animate-spin")} />
|
|
349
|
+
<span>{isLoading ? t("Updating preview...") : t("Refreshing preview...")}</span>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
{isLoading && <div className="absolute inset-0 z-10 rounded-lg bg-background/35 backdrop-blur-[1.5px]" />}
|
|
354
|
+
<div className={cn(isLoading && "opacity-75 transition-opacity duration-200")}>
|
|
355
|
+
<ScaledDocumentPreview
|
|
356
|
+
htmlContent={previewHtml}
|
|
357
|
+
scale={scale}
|
|
358
|
+
contentHeight={contentHeight}
|
|
359
|
+
A4_WIDTH_PX={A4_WIDTH_PX}
|
|
360
|
+
contentRef={contentRef}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
265
363
|
</div>
|
|
266
364
|
)}
|
|
267
365
|
</div>
|
|
@@ -73,39 +73,47 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
|
|
|
73
73
|
values: T,
|
|
74
74
|
options: PrepareDocumentOptions,
|
|
75
75
|
): any {
|
|
76
|
+
const nextValues: any = {
|
|
77
|
+
...values,
|
|
78
|
+
customer: values.customer ? { ...values.customer } : values.customer,
|
|
79
|
+
items: values.items
|
|
80
|
+
? values.items.map((item: any) => ({ ...item, taxes: item?.taxes ? [...item.taxes] : item?.taxes }))
|
|
81
|
+
: values.items,
|
|
82
|
+
};
|
|
83
|
+
|
|
76
84
|
// Document numbers are always auto-generated by the server
|
|
77
85
|
// Remove number from payload (even if form provides a preview value)
|
|
78
86
|
|
|
79
87
|
// Handle customer logic
|
|
80
|
-
if (
|
|
88
|
+
if (nextValues.customer_id && nextValues.customer) {
|
|
81
89
|
// If customer form was not shown, remove customer data (keep only customer_id)
|
|
82
90
|
if (options.wasCustomerFormShown === false) {
|
|
83
|
-
delete
|
|
91
|
+
delete nextValues.customer;
|
|
84
92
|
} else {
|
|
85
93
|
// Existing customer loaded - check if data was actually modified
|
|
86
94
|
const customerChanged =
|
|
87
|
-
options.originalCustomer && JSON.stringify(
|
|
95
|
+
options.originalCustomer && JSON.stringify(nextValues.customer) !== JSON.stringify(options.originalCustomer);
|
|
88
96
|
|
|
89
97
|
if (!customerChanged) {
|
|
90
98
|
// No changes - send only customer_id
|
|
91
|
-
delete
|
|
99
|
+
delete nextValues.customer;
|
|
92
100
|
} else {
|
|
93
101
|
// Changes detected - clean null/empty values and send with save_customer flag
|
|
94
102
|
const cleanedCustomer: any = { save_customer: true };
|
|
95
|
-
for (const [key, value] of Object.entries(
|
|
103
|
+
for (const [key, value] of Object.entries(nextValues.customer)) {
|
|
96
104
|
if (key !== "save_customer" && value !== "" && value !== null && value !== undefined) {
|
|
97
105
|
cleanedCustomer[key] = value;
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
|
-
|
|
108
|
+
nextValues.customer = cleanedCustomer;
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
|
-
} else if (
|
|
111
|
+
} else if (nextValues.customer) {
|
|
104
112
|
// New inline customer - clean null/empty values and add save flag
|
|
105
113
|
const cleanedCustomer: any = { save_customer: true };
|
|
106
114
|
let hasAnyValue = false;
|
|
107
115
|
|
|
108
|
-
for (const [key, value] of Object.entries(
|
|
116
|
+
for (const [key, value] of Object.entries(nextValues.customer)) {
|
|
109
117
|
if (key !== "save_customer" && value !== "" && value !== null && value !== undefined) {
|
|
110
118
|
cleanedCustomer[key] = value;
|
|
111
119
|
hasAnyValue = true;
|
|
@@ -113,21 +121,21 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
|
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
if (!hasAnyValue) {
|
|
116
|
-
delete
|
|
124
|
+
delete nextValues.customer;
|
|
117
125
|
} else {
|
|
118
|
-
|
|
126
|
+
nextValues.customer = cleanedCustomer;
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
// Clean up customer_id if empty
|
|
123
|
-
if (!
|
|
124
|
-
delete
|
|
131
|
+
if (!nextValues.customer_id) {
|
|
132
|
+
delete nextValues.customer_id;
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
// Clean up taxes and handle gross price transformation
|
|
128
|
-
if (
|
|
136
|
+
if (nextValues.items) {
|
|
129
137
|
const priceModes = options.priceModes ?? {};
|
|
130
|
-
|
|
138
|
+
nextValues.items = nextValues.items.map((item: any, index: number) => {
|
|
131
139
|
// Separator items — pass through with only type, name, description
|
|
132
140
|
if (item.type === "separator") {
|
|
133
141
|
return {
|
|
@@ -162,7 +170,7 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
|
|
|
162
170
|
|
|
163
171
|
// Build payload with date conversions
|
|
164
172
|
// Destructure to exclude fields we handle explicitly (number is always server-generated)
|
|
165
|
-
const { number: _number, note, payment_terms, reference, signature, ...restValues } =
|
|
173
|
+
const { number: _number, note, payment_terms, reference, signature, ...restValues } = nextValues as any;
|
|
166
174
|
const payload: any = {
|
|
167
175
|
...restValues,
|
|
168
176
|
...(note?.trim() && { note: note.trim() }),
|
|
@@ -170,7 +178,7 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
|
|
|
170
178
|
// Advance invoices don't have payment terms - they are documents requesting payment
|
|
171
179
|
...(options.documentType !== "advance_invoice" && payment_terms?.trim() && { payment_terms: payment_terms.trim() }),
|
|
172
180
|
...(signature?.trim() && { signature: signature.trim() }),
|
|
173
|
-
date:
|
|
181
|
+
date: nextValues.date ? new Date(nextValues.date) : undefined,
|
|
174
182
|
};
|
|
175
183
|
|
|
176
184
|
// Add secondary date field based on document type
|
|
@@ -181,6 +189,17 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
|
|
|
181
189
|
}
|
|
182
190
|
// Credit notes don't have a secondary date field
|
|
183
191
|
|
|
192
|
+
// Add service date fields for invoices and credit notes
|
|
193
|
+
if (options.documentType === "invoice" || options.documentType === "credit_note") {
|
|
194
|
+
const v = nextValues as any;
|
|
195
|
+
if (v.date_service) {
|
|
196
|
+
payload.date_service = new Date(v.date_service);
|
|
197
|
+
}
|
|
198
|
+
if (v.date_service_to) {
|
|
199
|
+
payload.date_service_to = new Date(v.date_service_to);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
184
203
|
// Handle markAsPaid for invoices and credit notes
|
|
185
204
|
if (
|
|
186
205
|
options.documentType !== "estimate" &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
import type { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -67,9 +67,20 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
|
|
|
67
67
|
const [shouldFocusName, setShouldFocusName] = useState(false);
|
|
68
68
|
const [selectedCustomerId, setSelectedCustomerId] = useState<string | undefined>(initialCustomerId);
|
|
69
69
|
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const defaultCustomerId = form.formState.defaultValues?.customer_id as string | undefined;
|
|
72
|
+
const defaultCustomer = form.formState.defaultValues?.customer as CustomerData | undefined;
|
|
73
|
+
const hasDefaultCustomer = !!(defaultCustomerId || defaultCustomer?.name);
|
|
74
|
+
|
|
75
|
+
setOriginalCustomer(hasDefaultCustomer && defaultCustomer ? defaultCustomer : null);
|
|
76
|
+
setSelectedCustomerId(defaultCustomerId);
|
|
77
|
+
setShowCustomerForm(hasDefaultCustomer);
|
|
78
|
+
setShouldFocusName(false);
|
|
79
|
+
}, [form.formState.defaultValues]);
|
|
80
|
+
|
|
70
81
|
// Type-safe setValue that works with the generic form type
|
|
71
82
|
const setValue = <K extends Path<TForm>>(name: K, value: PathValue<TForm, K>) => {
|
|
72
|
-
form.setValue(name, value);
|
|
83
|
+
form.setValue(name, value, { shouldDirty: true, shouldTouch: true });
|
|
73
84
|
};
|
|
74
85
|
|
|
75
86
|
const handleCustomerSelect = (customerId: string, customer: CustomerData) => {
|
|
@@ -158,7 +169,7 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
|
|
|
158
169
|
shouldFocusName,
|
|
159
170
|
selectedCustomerId,
|
|
160
171
|
/** Initial customer name from form defaults (for duplication display) */
|
|
161
|
-
initialCustomerName:
|
|
172
|
+
initialCustomerName: (form.formState.defaultValues?.customer as CustomerData | undefined)?.name ?? undefined,
|
|
162
173
|
handleCustomerSelect,
|
|
163
174
|
handleCustomerClear,
|
|
164
175
|
};
|
|
@@ -26,6 +26,8 @@ type FinalizeDocumentOptions = {
|
|
|
26
26
|
type FinalizeDocumentVariables = {
|
|
27
27
|
documentId: string;
|
|
28
28
|
documentType: DocumentType;
|
|
29
|
+
furs?: { business_premise_name: string; electronic_device_name: string } | { skip: true };
|
|
30
|
+
fina?: { business_premise_name: string; electronic_device_name: string; payment_type?: string };
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -37,8 +39,11 @@ export function useFinalizeDocument(options: FinalizeDocumentOptions) {
|
|
|
37
39
|
const queryClient = useQueryClient();
|
|
38
40
|
|
|
39
41
|
return useMutation({
|
|
40
|
-
mutationFn: async ({ documentId, documentType }: FinalizeDocumentVariables) => {
|
|
41
|
-
|
|
42
|
+
mutationFn: async ({ documentId, documentType, furs, fina }: FinalizeDocumentVariables) => {
|
|
43
|
+
const body: Record<string, unknown> = {};
|
|
44
|
+
if (furs) body.furs = furs;
|
|
45
|
+
if (fina) body.fina = fina;
|
|
46
|
+
return sdk.documents.finalizeDocument(documentId, body, { type: documentType });
|
|
42
47
|
},
|
|
43
48
|
onSuccess: (data, variables) => {
|
|
44
49
|
// Invalidate list cache
|