@spaceinvoices/react-ui 0.4.5 → 0.4.7
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/cli/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/advance-invoices/advance-invoices.hooks.ts +2 -2
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +146 -74
- package/src/components/advance-invoices/create/locales/de.ts +5 -0
- package/src/components/advance-invoices/create/locales/es.ts +5 -0
- package/src/components/advance-invoices/create/locales/fr.ts +5 -0
- package/src/components/advance-invoices/create/locales/hr.ts +5 -0
- package/src/components/advance-invoices/create/locales/it.ts +5 -0
- package/src/components/advance-invoices/create/locales/nl.ts +5 -0
- package/src/components/advance-invoices/create/locales/pl.ts +5 -0
- package/src/components/advance-invoices/create/locales/pt.ts +5 -0
- package/src/components/advance-invoices/create/locales/sl.ts +5 -0
- package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +5 -5
- package/src/components/credit-notes/create/create-credit-note-form.tsx +138 -72
- package/src/components/credit-notes/create/locales/de.ts +5 -0
- package/src/components/credit-notes/create/locales/es.ts +5 -0
- package/src/components/credit-notes/create/locales/fr.ts +5 -0
- package/src/components/credit-notes/create/locales/hr.ts +5 -0
- package/src/components/credit-notes/create/locales/it.ts +5 -0
- package/src/components/credit-notes/create/locales/nl.ts +5 -0
- package/src/components/credit-notes/create/locales/pl.ts +5 -0
- package/src/components/credit-notes/create/locales/pt.ts +5 -0
- package/src/components/credit-notes/create/locales/sl.ts +5 -0
- package/src/components/credit-notes/credit-notes.hooks.ts +2 -2
- package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +48 -92
- package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +48 -82
- package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +22 -31
- package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +33 -48
- package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +56 -76
- package/src/components/dashboard/shared/index.ts +1 -1
- package/src/components/dashboard/shared/use-revenue-data.ts +106 -182
- package/src/components/dashboard/shared/use-stats-counts.ts +18 -68
- package/src/components/dashboard/shared/use-stats-query.ts +35 -5
- package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +57 -75
- package/src/components/dashboard/top-customers-chart/use-top-customers.ts +38 -49
- package/src/components/delivery-notes/create/create-delivery-note-form.tsx +50 -2
- package/src/components/delivery-notes/create/locales/de.ts +5 -0
- package/src/components/delivery-notes/create/locales/es.ts +5 -0
- package/src/components/delivery-notes/create/locales/fr.ts +5 -0
- package/src/components/delivery-notes/create/locales/hr.ts +5 -0
- package/src/components/delivery-notes/create/locales/it.ts +5 -0
- package/src/components/delivery-notes/create/locales/nl.ts +5 -0
- package/src/components/delivery-notes/create/locales/pl.ts +5 -0
- package/src/components/delivery-notes/create/locales/pt.ts +5 -0
- package/src/components/delivery-notes/create/locales/sl.ts +5 -0
- package/src/components/documents/create/document-details-section.tsx +478 -350
- package/src/components/documents/create/document-recipient-section.tsx +30 -1
- package/src/components/documents/create/live-preview.tsx +15 -28
- package/src/components/documents/create/prepare-document-submission.ts +4 -1
- package/src/components/documents/create/smart-code-insert-button.tsx +6 -0
- package/src/components/documents/create/use-document-customer-form.ts +4 -0
- package/src/components/documents/shared/document-preview-skeleton.tsx +63 -0
- package/src/components/documents/shared/index.ts +1 -0
- package/src/components/documents/view/document-actions-bar.tsx +29 -7
- package/src/components/documents/view/document-details-card.tsx +6 -0
- package/src/components/documents/view/locales/de.ts +1 -0
- package/src/components/documents/view/locales/es.ts +1 -0
- package/src/components/documents/view/locales/fr.ts +1 -0
- package/src/components/documents/view/locales/hr.ts +1 -0
- package/src/components/documents/view/locales/it.ts +1 -0
- package/src/components/documents/view/locales/nl.ts +1 -0
- package/src/components/documents/view/locales/pl.ts +1 -0
- package/src/components/documents/view/locales/pt.ts +1 -0
- package/src/components/documents/view/locales/sl.ts +1 -0
- package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +6 -0
- package/src/components/entities/entity-settings-form/input-with-preview.tsx +2 -145
- package/src/components/entities/entity-settings-form/locales/de.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/es.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/fr.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/hr.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/it.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/nl.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/pl.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/pt.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/sl.ts +4 -0
- package/src/components/entities/fina-settings-form/fina-settings-form.tsx +15 -0
- package/src/components/entities/fina-settings-form/fina-settings.hooks.ts +5 -1
- package/src/components/entities/fina-settings-form/locales/de.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/en.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/es.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/fr.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/hr.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/it.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/nl.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/pl.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/pt.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/sl.ts +3 -0
- package/src/components/entities/fina-settings-form/sections/premises-management-section.tsx +4 -4
- package/src/components/entities/fina-settings-form/sections/register-premise-dialog.tsx +3 -3
- package/src/components/entities/settings/defaults-settings-form.tsx +38 -1
- package/src/components/entities/settings/tax-rules-settings-form.tsx +32 -15
- package/src/components/estimates/create/create-estimate-form.tsx +46 -4
- package/src/components/estimates/create/locales/de.ts +5 -0
- package/src/components/estimates/create/locales/es.ts +5 -0
- package/src/components/estimates/create/locales/fr.ts +5 -0
- package/src/components/estimates/create/locales/hr.ts +5 -0
- package/src/components/estimates/create/locales/it.ts +5 -0
- package/src/components/estimates/create/locales/nl.ts +5 -0
- package/src/components/estimates/create/locales/pl.ts +5 -0
- package/src/components/estimates/create/locales/pt.ts +5 -0
- package/src/components/estimates/create/locales/sl.ts +5 -0
- package/src/components/invoices/create/create-invoice-form.tsx +258 -96
- package/src/components/invoices/create/locales/de.ts +19 -0
- package/src/components/invoices/create/locales/es.ts +19 -0
- package/src/components/invoices/create/locales/fr.ts +19 -0
- package/src/components/invoices/create/locales/hr.ts +19 -0
- package/src/components/invoices/create/locales/it.ts +19 -0
- package/src/components/invoices/create/locales/nl.ts +19 -0
- package/src/components/invoices/create/locales/pl.ts +19 -0
- package/src/components/invoices/create/locales/pt.ts +19 -0
- package/src/components/invoices/create/locales/sl.ts +19 -0
- package/src/components/invoices/create/prepare-invoice-submission.ts +5 -5
- package/src/components/invoices/invoices.hooks.ts +3 -3
- package/src/components/table/table-pagination.tsx +1 -1
- package/src/components/ui/progress.tsx +27 -0
- package/src/generate-schemas.ts +15 -2
- package/src/generated/schemas/advanceinvoice.ts +4 -0
- package/src/generated/schemas/creditnote.ts +3 -0
- package/src/generated/schemas/customer.ts +2 -0
- package/src/generated/schemas/deliverynote.ts +3 -0
- package/src/generated/schemas/entity.ts +14 -4
- package/src/generated/schemas/entityapikey.ts +19 -0
- package/src/generated/schemas/estimate.ts +4 -0
- package/src/generated/schemas/finasettings.ts +4 -3
- package/src/generated/schemas/index.ts +1 -0
- package/src/generated/schemas/invoice.ts +4 -0
- package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +17 -11
- package/src/generated/schemas/rendercreditnotepreview_body.ts +17 -11
- package/src/generated/schemas/renderdeliverynotepreview_body.ts +15 -8
- package/src/generated/schemas/renderestimatepreview_body.ts +15 -8
- package/src/generated/schemas/renderinvoicepreview_body.ts +17 -11
- package/src/generated/schemas/startpdfexport_body.ts +12 -5
- package/src/generated/schemas/webhook.ts +4 -0
- package/src/hooks/use-transaction-type-check.ts +152 -0
- package/src/hooks/use-vies-check.ts +7 -131
- package/src/lib/template-variables.tsx +167 -0
- package/src/providers/entities-context.tsx +2 -2
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
7
|
+
import { useController } from "react-hook-form";
|
|
7
8
|
import { FormInput } from "@/ui/components/form";
|
|
8
9
|
import { Button } from "@/ui/components/ui/button";
|
|
10
|
+
import { Checkbox } from "@/ui/components/ui/checkbox";
|
|
9
11
|
import { Label } from "@/ui/components/ui/label";
|
|
10
12
|
import { cn } from "@/ui/lib/utils";
|
|
11
13
|
import { CustomerAutocomplete } from "../../customers/customer-autocomplete";
|
|
@@ -22,6 +24,8 @@ type DocumentRecipientSectionProps = {
|
|
|
22
24
|
selectedCustomerId?: string;
|
|
23
25
|
/** Initial customer name for display (used when duplicating documents) */
|
|
24
26
|
initialCustomerName?: string;
|
|
27
|
+
/** Show end consumer (B2C) toggle next to tax number (Croatian entity + domestic transaction) */
|
|
28
|
+
showEndConsumerToggle?: boolean;
|
|
25
29
|
t: (key: string) => string;
|
|
26
30
|
};
|
|
27
31
|
|
|
@@ -34,10 +38,16 @@ export function DocumentRecipientSection({
|
|
|
34
38
|
shouldFocusName,
|
|
35
39
|
selectedCustomerId,
|
|
36
40
|
initialCustomerName,
|
|
41
|
+
showEndConsumerToggle,
|
|
37
42
|
t,
|
|
38
43
|
}: DocumentRecipientSectionProps) {
|
|
39
44
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
40
45
|
|
|
46
|
+
const endConsumerController = useController({
|
|
47
|
+
control: control as any,
|
|
48
|
+
name: "customer.is_end_consumer" as any,
|
|
49
|
+
});
|
|
50
|
+
|
|
41
51
|
useEffect(() => {
|
|
42
52
|
if (showCustomerForm && shouldFocusName) {
|
|
43
53
|
// Small delay to ensure the input is rendered
|
|
@@ -93,7 +103,26 @@ export function DocumentRecipientSection({
|
|
|
93
103
|
<FormInput control={control} name="customer.country" placeholder={t("Country")} label="" />
|
|
94
104
|
</div>
|
|
95
105
|
|
|
96
|
-
<
|
|
106
|
+
<div className="flex items-center gap-3">
|
|
107
|
+
<div className="flex-1">
|
|
108
|
+
<FormInput control={control} name="customer.tax_number" placeholder={t("Tax Number")} label="" />
|
|
109
|
+
</div>
|
|
110
|
+
{showEndConsumerToggle && (
|
|
111
|
+
<div className="flex items-center gap-1.5 pt-0.5">
|
|
112
|
+
<Checkbox
|
|
113
|
+
id="is_end_consumer"
|
|
114
|
+
checked={endConsumerController.field.value === true}
|
|
115
|
+
onCheckedChange={(checked) => endConsumerController.field.onChange(checked === true)}
|
|
116
|
+
/>
|
|
117
|
+
<Label
|
|
118
|
+
htmlFor="is_end_consumer"
|
|
119
|
+
className="cursor-pointer whitespace-nowrap font-normal text-muted-foreground text-sm"
|
|
120
|
+
>
|
|
121
|
+
{t("End consumer")}
|
|
122
|
+
</Label>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
97
126
|
</>
|
|
98
127
|
)}
|
|
99
128
|
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { CreateInvoiceRequest } from "@spaceinvoices/js-sdk";
|
|
4
|
-
import { Loader2 } from "lucide-react";
|
|
5
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
5
|
import { cn } from "@/ui/lib/utils";
|
|
7
6
|
import { useEntities } from "@/ui/providers/entities-context";
|
|
8
7
|
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
8
|
+
import { DocumentPreviewSkeleton } from "../shared/document-preview-skeleton";
|
|
9
9
|
import { ScaledDocumentPreview } from "../shared/scaled-document-preview";
|
|
10
10
|
import { useA4Scaling } from "../shared/use-a4-scaling";
|
|
11
11
|
import type { DocumentTypes } from "../types";
|
|
@@ -58,7 +58,7 @@ export function LiveInvoicePreview({
|
|
|
58
58
|
locale: _locale,
|
|
59
59
|
fixedScale,
|
|
60
60
|
t: tProp,
|
|
61
|
-
documentTypeLabel,
|
|
61
|
+
documentTypeLabel: _documentTypeLabel,
|
|
62
62
|
documentType = "invoice",
|
|
63
63
|
qrOverrides,
|
|
64
64
|
}: LiveInvoicePreviewProps) {
|
|
@@ -235,18 +235,10 @@ export function LiveInvoicePreview({
|
|
|
235
235
|
};
|
|
236
236
|
}, []);
|
|
237
237
|
|
|
238
|
+
const showSkeleton = (!previewHtml && !error) || (isLoading && !previewHtml);
|
|
239
|
+
|
|
238
240
|
return (
|
|
239
241
|
<div ref={containerRef} className={cn("relative", className)}>
|
|
240
|
-
{/* Loading overlay */}
|
|
241
|
-
{isLoading && (
|
|
242
|
-
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
|
243
|
-
<div className="flex flex-col items-center gap-2">
|
|
244
|
-
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
245
|
-
<p className="text-muted-foreground text-sm">{t("Generating preview...")}</p>
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
)}
|
|
249
|
-
|
|
250
242
|
{/* Error state */}
|
|
251
243
|
{error && !isLoading && (
|
|
252
244
|
<div className="flex min-h-[200px] items-center justify-center rounded-lg border border-destructive/50 bg-destructive/10 p-8">
|
|
@@ -257,25 +249,20 @@ export function LiveInvoicePreview({
|
|
|
257
249
|
</div>
|
|
258
250
|
)}
|
|
259
251
|
|
|
260
|
-
{/*
|
|
261
|
-
{
|
|
262
|
-
<div className="flex min-h-[200px] items-center justify-center rounded-lg border border-dashed p-8">
|
|
263
|
-
<div className="text-center">
|
|
264
|
-
<p className="font-semibold text-muted-foreground">{documentTypeLabel || t("Document Preview")}</p>
|
|
265
|
-
<p className="text-muted-foreground text-sm">{t("Start filling the form to see a live preview")}</p>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
)}
|
|
252
|
+
{/* Skeleton: shown on initial load and when loading without existing preview */}
|
|
253
|
+
{showSkeleton && <DocumentPreviewSkeleton />}
|
|
269
254
|
|
|
270
255
|
{/* Preview - Scoped HTML injection with A4 scaling */}
|
|
271
256
|
{previewHtml && !error && (
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
257
|
+
<div className={cn(isLoading && "opacity-50 transition-opacity duration-200")}>
|
|
258
|
+
<ScaledDocumentPreview
|
|
259
|
+
htmlContent={previewHtml}
|
|
260
|
+
scale={scale}
|
|
261
|
+
contentHeight={contentHeight}
|
|
262
|
+
A4_WIDTH_PX={A4_WIDTH_PX}
|
|
263
|
+
contentRef={contentRef}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
279
266
|
)}
|
|
280
267
|
</div>
|
|
281
268
|
);
|
|
@@ -7,6 +7,7 @@ type CustomerData = {
|
|
|
7
7
|
state?: string | null;
|
|
8
8
|
country?: string | null;
|
|
9
9
|
tax_number?: string | null;
|
|
10
|
+
is_end_consumer?: boolean | null;
|
|
10
11
|
save_customer?: boolean;
|
|
11
12
|
};
|
|
12
13
|
|
|
@@ -161,12 +162,14 @@ export function prepareDocumentSubmission<T extends BaseDocumentValues>(
|
|
|
161
162
|
|
|
162
163
|
// Build payload with date conversions
|
|
163
164
|
// Destructure to exclude fields we handle explicitly (number is always server-generated)
|
|
164
|
-
const { number: _number, note, payment_terms, ...restValues } = values as any;
|
|
165
|
+
const { number: _number, note, payment_terms, reference, signature, ...restValues } = values as any;
|
|
165
166
|
const payload: any = {
|
|
166
167
|
...restValues,
|
|
167
168
|
...(note?.trim() && { note: note.trim() }),
|
|
169
|
+
...(reference?.trim() && { reference: reference.trim() }),
|
|
168
170
|
// Advance invoices don't have payment terms - they are documents requesting payment
|
|
169
171
|
...(options.documentType !== "advance_invoice" && payment_terms?.trim() && { payment_terms: payment_terms.trim() }),
|
|
172
|
+
...(signature?.trim() && { signature: signature.trim() }),
|
|
170
173
|
date: values.date ? new Date(values.date) : undefined,
|
|
171
174
|
};
|
|
172
175
|
|
|
@@ -22,6 +22,12 @@ const TEMPLATE_VARIABLES = [
|
|
|
22
22
|
variables: [
|
|
23
23
|
{ code: "{entity_name}", label: "Company name" },
|
|
24
24
|
{ code: "{entity_email}", label: "Email address" },
|
|
25
|
+
{ code: "{entity_address}", label: "Address" },
|
|
26
|
+
{ code: "{entity_post_code}", label: "Post code" },
|
|
27
|
+
{ code: "{entity_city}", label: "City" },
|
|
28
|
+
{ code: "{entity_country}", label: "Country" },
|
|
29
|
+
{ code: "{entity_tax_number}", label: "Tax number" },
|
|
30
|
+
{ code: "{entity_company_number}", label: "Company number" },
|
|
25
31
|
],
|
|
26
32
|
},
|
|
27
33
|
{
|
|
@@ -14,6 +14,7 @@ export type CustomerData = {
|
|
|
14
14
|
state?: string | null;
|
|
15
15
|
country?: string | null;
|
|
16
16
|
tax_number?: string | null;
|
|
17
|
+
is_end_consumer?: boolean | null;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -97,6 +98,7 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
|
|
|
97
98
|
state: toFormValue(customer.state),
|
|
98
99
|
country: toFormValue(customer.country),
|
|
99
100
|
tax_number: toFormValue(customer.tax_number),
|
|
101
|
+
is_end_consumer: customer.is_end_consumer ?? undefined,
|
|
100
102
|
} as PathValue<TForm, Path<TForm>>,
|
|
101
103
|
);
|
|
102
104
|
setOriginalCustomer(null);
|
|
@@ -115,6 +117,7 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
|
|
|
115
117
|
state: toFormValue(customer.state),
|
|
116
118
|
country: toFormValue(customer.country),
|
|
117
119
|
tax_number: toFormValue(customer.tax_number),
|
|
120
|
+
is_end_consumer: customer.is_end_consumer ?? undefined,
|
|
118
121
|
};
|
|
119
122
|
|
|
120
123
|
setValue("customer" as Path<TForm>, customerData as PathValue<TForm, Path<TForm>>);
|
|
@@ -140,6 +143,7 @@ export function useDocumentCustomerForm<TForm extends DocumentFormWithCustomer>(
|
|
|
140
143
|
state: undefined,
|
|
141
144
|
country: undefined,
|
|
142
145
|
tax_number: undefined,
|
|
146
|
+
is_end_consumer: true,
|
|
143
147
|
} as PathValue<TForm, Path<TForm>>,
|
|
144
148
|
);
|
|
145
149
|
setOriginalCustomer(null);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Skeleton } from "@/ui/components/ui/skeleton";
|
|
2
|
+
|
|
3
|
+
/** A4-shaped skeleton that mimics a document layout */
|
|
4
|
+
export function DocumentPreviewSkeleton() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="rounded-lg border bg-neutral-100 p-4">
|
|
7
|
+
<div className="mx-auto bg-white p-8" style={{ maxWidth: 794, aspectRatio: "210 / 297" }}>
|
|
8
|
+
{/* Header: logo + company info */}
|
|
9
|
+
<div className="flex justify-between">
|
|
10
|
+
<Skeleton className="h-10 w-32 bg-neutral-200" />
|
|
11
|
+
<div className="flex flex-col items-end gap-1.5">
|
|
12
|
+
<Skeleton className="h-3 w-40 bg-neutral-200" />
|
|
13
|
+
<Skeleton className="h-3 w-32 bg-neutral-200" />
|
|
14
|
+
<Skeleton className="h-3 w-28 bg-neutral-200" />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{/* Title */}
|
|
19
|
+
<Skeleton className="mt-8 h-5 w-28 bg-neutral-200" />
|
|
20
|
+
|
|
21
|
+
{/* Recipient + details row */}
|
|
22
|
+
<div className="mt-6 flex justify-between">
|
|
23
|
+
<div className="flex flex-col gap-1.5">
|
|
24
|
+
<Skeleton className="h-3 w-36 bg-neutral-200" />
|
|
25
|
+
<Skeleton className="h-3 w-28 bg-neutral-200" />
|
|
26
|
+
<Skeleton className="h-3 w-24 bg-neutral-200" />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="flex flex-col items-end gap-1.5">
|
|
29
|
+
<Skeleton className="h-3 w-24 bg-neutral-200" />
|
|
30
|
+
<Skeleton className="h-3 w-20 bg-neutral-200" />
|
|
31
|
+
<Skeleton className="h-3 w-24 bg-neutral-200" />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* Table header */}
|
|
36
|
+
<div className="mt-8 flex gap-4 border-b pb-2">
|
|
37
|
+
<Skeleton className="h-3 w-8 bg-neutral-200" />
|
|
38
|
+
<Skeleton className="h-3 flex-1 bg-neutral-200" />
|
|
39
|
+
<Skeleton className="h-3 w-12 bg-neutral-200" />
|
|
40
|
+
<Skeleton className="h-3 w-16 bg-neutral-200" />
|
|
41
|
+
<Skeleton className="h-3 w-16 bg-neutral-200" />
|
|
42
|
+
</div>
|
|
43
|
+
{/* Table rows */}
|
|
44
|
+
{[1, 2, 3].map((i) => (
|
|
45
|
+
<div key={i} className="flex gap-4 border-b py-2.5">
|
|
46
|
+
<Skeleton className="h-3 w-8 bg-neutral-200" />
|
|
47
|
+
<Skeleton className="h-3 flex-1 bg-neutral-200" />
|
|
48
|
+
<Skeleton className="h-3 w-12 bg-neutral-200" />
|
|
49
|
+
<Skeleton className="h-3 w-16 bg-neutral-200" />
|
|
50
|
+
<Skeleton className="h-3 w-16 bg-neutral-200" />
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
|
|
54
|
+
{/* Totals */}
|
|
55
|
+
<div className="mt-4 flex flex-col items-end gap-1.5">
|
|
56
|
+
<Skeleton className="h-3 w-32 bg-neutral-200" />
|
|
57
|
+
<Skeleton className="h-3 w-28 bg-neutral-200" />
|
|
58
|
+
<Skeleton className="h-4 w-36 bg-neutral-200" />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { DocumentPreviewDisplay, InvoicePreviewDisplay } from "./document-preview-display";
|
|
2
|
+
export { DocumentPreviewSkeleton } from "./document-preview-skeleton";
|
|
2
3
|
export { ScaledDocumentPreview } from "./scaled-document-preview";
|
|
3
4
|
export { useA4Scaling } from "./use-a4-scaling";
|
|
@@ -81,8 +81,10 @@ interface DocumentActionsBarProps extends ComponentTranslationProps {
|
|
|
81
81
|
onDuplicate?: (targetType: DocumentType) => void;
|
|
82
82
|
/** Called when user wants to edit the document */
|
|
83
83
|
onEdit?: () => void;
|
|
84
|
-
/** Whether the document is editable (not voided, not
|
|
84
|
+
/** Whether the document is editable (not voided, not fiscalized) */
|
|
85
85
|
isEditable?: boolean;
|
|
86
|
+
/** Reason why editing is disabled (shown as tooltip) */
|
|
87
|
+
editDisabledReason?: string;
|
|
86
88
|
/** Called when user wants to finalize a draft document */
|
|
87
89
|
onFinalize?: () => void;
|
|
88
90
|
/** Whether finalization is in progress */
|
|
@@ -119,6 +121,7 @@ export function DocumentActionsBar({
|
|
|
119
121
|
onDuplicate,
|
|
120
122
|
onEdit,
|
|
121
123
|
isEditable,
|
|
124
|
+
editDisabledReason,
|
|
122
125
|
onFinalize,
|
|
123
126
|
isFinalizing,
|
|
124
127
|
onDeleteDraft,
|
|
@@ -233,12 +236,31 @@ export function DocumentActionsBar({
|
|
|
233
236
|
)}
|
|
234
237
|
|
|
235
238
|
{/* Edit */}
|
|
236
|
-
{onEdit &&
|
|
237
|
-
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
{onEdit &&
|
|
240
|
+
(isEditable ? (
|
|
241
|
+
<Button variant="outline" size="sm" onClick={onEdit} className="cursor-pointer">
|
|
242
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
243
|
+
{t("Edit")}
|
|
244
|
+
</Button>
|
|
245
|
+
) : (
|
|
246
|
+
<Tooltip>
|
|
247
|
+
<TooltipTrigger asChild>
|
|
248
|
+
<Button
|
|
249
|
+
variant="outline"
|
|
250
|
+
size="sm"
|
|
251
|
+
aria-disabled="true"
|
|
252
|
+
className="pointer-events-auto opacity-50"
|
|
253
|
+
onClick={(e) => e.preventDefault()}
|
|
254
|
+
>
|
|
255
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
256
|
+
{t("Edit")}
|
|
257
|
+
</Button>
|
|
258
|
+
</TooltipTrigger>
|
|
259
|
+
<TooltipContent>
|
|
260
|
+
<p>{editDisabledReason}</p>
|
|
261
|
+
</TooltipContent>
|
|
262
|
+
</Tooltip>
|
|
263
|
+
))}
|
|
242
264
|
|
|
243
265
|
{/* Share Link */}
|
|
244
266
|
{shareUrl ? (
|
|
@@ -148,6 +148,12 @@ export function DocumentDetailsCard({
|
|
|
148
148
|
|
|
149
149
|
{/* Totals */}
|
|
150
150
|
<div className="space-y-2 text-sm">
|
|
151
|
+
{document.total_discount != null && document.total_discount !== 0 && (
|
|
152
|
+
<div className="flex justify-between">
|
|
153
|
+
<span className="text-muted-foreground">{t("Discount")}</span>
|
|
154
|
+
<span>{fmt(document.total_discount * sign)}</span>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
151
157
|
<div className="flex justify-between">
|
|
152
158
|
<span className="text-muted-foreground">{t("Subtotal")}</span>
|
|
153
159
|
<span>{fmt(document.total * sign)}</span>
|
|
@@ -12,6 +12,12 @@ const TEMPLATE_VARIABLES = [
|
|
|
12
12
|
variables: [
|
|
13
13
|
{ name: "{entity_name}", description: "Your company or entity name" },
|
|
14
14
|
{ name: "{entity_email}", description: "Entity email address" },
|
|
15
|
+
{ name: "{entity_address}", description: "Entity address" },
|
|
16
|
+
{ name: "{entity_post_code}", description: "Entity post code" },
|
|
17
|
+
{ name: "{entity_city}", description: "Entity city" },
|
|
18
|
+
{ name: "{entity_country}", description: "Entity country" },
|
|
19
|
+
{ name: "{entity_tax_number}", description: "Entity tax number" },
|
|
20
|
+
{ name: "{entity_company_number}", description: "Entity company number" },
|
|
15
21
|
],
|
|
16
22
|
},
|
|
17
23
|
{
|
|
@@ -2,6 +2,7 @@ import type { Entity, Estimate, Invoice } from "@spaceinvoices/js-sdk";
|
|
|
2
2
|
import { forwardRef, useState } from "react";
|
|
3
3
|
import { Input } from "@/ui/components/ui/input";
|
|
4
4
|
import { Textarea } from "@/ui/components/ui/textarea";
|
|
5
|
+
import { replaceTemplateVariablesForPreview } from "@/ui/lib/template-variables";
|
|
5
6
|
import { cn } from "@/ui/lib/utils";
|
|
6
7
|
|
|
7
8
|
interface InputWithPreviewProps {
|
|
@@ -16,150 +17,6 @@ interface InputWithPreviewProps {
|
|
|
16
17
|
disabled?: boolean;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function formatVariableName(varName: string): string {
|
|
20
|
-
// Convert snake_case (with optional dot notation) to Title Case with spaces
|
|
21
|
-
// e.g., "document_number" -> "Document Number", "bank_account.iban" -> "Bank Account Iban"
|
|
22
|
-
return varName
|
|
23
|
-
.replace(/\./g, "_")
|
|
24
|
-
.split("_")
|
|
25
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
26
|
-
.join(" ");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getVariableValue(varName: string, entity: Entity, document?: Invoice | Estimate | null): string | null {
|
|
30
|
-
// Entity-related variables
|
|
31
|
-
if (varName === "entity_name") return entity.name || null;
|
|
32
|
-
if (varName === "entity_email") return (entity.settings as any)?.email || null;
|
|
33
|
-
|
|
34
|
-
// Date variables
|
|
35
|
-
if (varName === "current_date") {
|
|
36
|
-
return new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
|
|
37
|
-
}
|
|
38
|
-
if (varName === "current_year") return new Date().getFullYear().toString();
|
|
39
|
-
|
|
40
|
-
// Document-specific variables (only available when document is provided)
|
|
41
|
-
if (document) {
|
|
42
|
-
if (varName === "document_number") return document.number || null;
|
|
43
|
-
if (varName === "document_date") {
|
|
44
|
-
return document.date
|
|
45
|
-
? new Date(document.date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })
|
|
46
|
-
: null;
|
|
47
|
-
}
|
|
48
|
-
if (varName === "document_total") {
|
|
49
|
-
return document.total_with_tax
|
|
50
|
-
? new Intl.NumberFormat("en-US", { style: "currency", currency: document.currency_code || "USD" }).format(
|
|
51
|
-
Number(document.total_with_tax),
|
|
52
|
-
)
|
|
53
|
-
: null;
|
|
54
|
-
}
|
|
55
|
-
if (varName === "document_currency") return document.currency_code || null;
|
|
56
|
-
|
|
57
|
-
// Invoice-specific
|
|
58
|
-
if ("date_due" in document && varName === "document_due_date") {
|
|
59
|
-
return document.date_due
|
|
60
|
-
? new Date(document.date_due).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })
|
|
61
|
-
: null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Estimate-specific
|
|
65
|
-
if ("date_valid_till" in document && varName === "document_valid_until") {
|
|
66
|
-
return document.date_valid_till
|
|
67
|
-
? new Date(document.date_valid_till).toLocaleDateString("en-US", {
|
|
68
|
-
month: "long",
|
|
69
|
-
day: "numeric",
|
|
70
|
-
year: "numeric",
|
|
71
|
-
})
|
|
72
|
-
: null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Customer variables
|
|
76
|
-
if (document.customer) {
|
|
77
|
-
if (varName === "customer_name") return document.customer.name || null;
|
|
78
|
-
if (varName === "customer_email") return (document.customer as any).email || null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Bank account variables (from entity settings)
|
|
83
|
-
const bankAccounts = (entity.settings as any)?.bank_accounts as
|
|
84
|
-
| Array<{
|
|
85
|
-
iban?: string;
|
|
86
|
-
bank_name?: string;
|
|
87
|
-
bic?: string;
|
|
88
|
-
account_number?: string;
|
|
89
|
-
routing_number?: string;
|
|
90
|
-
sort_code?: string;
|
|
91
|
-
is_default?: boolean;
|
|
92
|
-
}>
|
|
93
|
-
| undefined;
|
|
94
|
-
const bankAccount = bankAccounts?.find((acc) => acc.is_default) ?? bankAccounts?.[0];
|
|
95
|
-
|
|
96
|
-
if (varName === "bank_account" && bankAccount) {
|
|
97
|
-
const lines: string[] = [];
|
|
98
|
-
if (bankAccount.bank_name) lines.push(bankAccount.bank_name);
|
|
99
|
-
if (bankAccount.iban) lines.push(`IBAN: ${bankAccount.iban}`);
|
|
100
|
-
else if (bankAccount.account_number) lines.push(`Account: ${bankAccount.account_number}`);
|
|
101
|
-
if (bankAccount.bic) lines.push(`BIC: ${bankAccount.bic}`);
|
|
102
|
-
return lines.join(", ") || null;
|
|
103
|
-
}
|
|
104
|
-
if (varName === "bank_account.iban") return bankAccount?.iban || null;
|
|
105
|
-
if (varName === "bank_account.bank_name") return bankAccount?.bank_name || null;
|
|
106
|
-
if (varName === "bank_account.bic") return bankAccount?.bic || null;
|
|
107
|
-
if (varName === "bank_account.account_number") return bankAccount?.account_number || null;
|
|
108
|
-
|
|
109
|
-
// Return null for unavailable variables - they will show as placeholders
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function replaceTemplateVariables(
|
|
114
|
-
template: string,
|
|
115
|
-
entity: Entity,
|
|
116
|
-
document?: Invoice | Estimate | null,
|
|
117
|
-
): React.ReactNode[] {
|
|
118
|
-
if (!template) return [];
|
|
119
|
-
|
|
120
|
-
// Split by variable pattern and process
|
|
121
|
-
const parts: React.ReactNode[] = [];
|
|
122
|
-
const regex = /\{([^}]+)\}/g;
|
|
123
|
-
let lastIndex = 0;
|
|
124
|
-
let match: RegExpExecArray | null = null;
|
|
125
|
-
|
|
126
|
-
match = regex.exec(template);
|
|
127
|
-
while (match !== null) {
|
|
128
|
-
// Add text before the match
|
|
129
|
-
if (match.index > lastIndex) {
|
|
130
|
-
parts.push(template.slice(lastIndex, match.index));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Add the replaced variable with styling
|
|
134
|
-
// Green for resolved values, primary color for placeholders
|
|
135
|
-
const varName = match[1];
|
|
136
|
-
const actualValue = getVariableValue(varName, entity, document);
|
|
137
|
-
const displayValue = actualValue || formatVariableName(varName);
|
|
138
|
-
|
|
139
|
-
parts.push(
|
|
140
|
-
<span
|
|
141
|
-
key={match.index}
|
|
142
|
-
className={cn(
|
|
143
|
-
"rounded px-1.5 py-0.5 font-medium text-xs",
|
|
144
|
-
actualValue ? "bg-secondary text-secondary-foreground" : "bg-primary/10 text-primary",
|
|
145
|
-
)}
|
|
146
|
-
>
|
|
147
|
-
{displayValue}
|
|
148
|
-
</span>,
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
lastIndex = regex.lastIndex;
|
|
152
|
-
match = regex.exec(template);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Add remaining text
|
|
156
|
-
if (lastIndex < template.length) {
|
|
157
|
-
parts.push(template.slice(lastIndex));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return parts;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
20
|
export const InputWithPreview = forwardRef<HTMLInputElement | HTMLTextAreaElement, InputWithPreviewProps>(
|
|
164
21
|
function InputWithPreview(
|
|
165
22
|
{ value, onChange, placeholder, entity, document, multiline = false, className, rows, disabled = false },
|
|
@@ -167,7 +24,7 @@ export const InputWithPreview = forwardRef<HTMLInputElement | HTMLTextAreaElemen
|
|
|
167
24
|
) {
|
|
168
25
|
const [isFocused, setIsFocused] = useState(false);
|
|
169
26
|
|
|
170
|
-
const preview =
|
|
27
|
+
const preview = replaceTemplateVariablesForPreview(value, entity, document);
|
|
171
28
|
const hasValue = Boolean(value);
|
|
172
29
|
|
|
173
30
|
const showPreview = !isFocused && hasValue && !disabled;
|
|
@@ -106,6 +106,10 @@ export default {
|
|
|
106
106
|
"Footer text displayed at the bottom of all PDF documents": "Fußzeilentext am unteren Rand aller PDF-Dokumente",
|
|
107
107
|
"{entity_name} | Due Date: {document_due_date} | Invoice #{document_number}":
|
|
108
108
|
"{entity_name} | Fällig am: {document_due_date} | Rechnung #{document_number}",
|
|
109
|
+
// Document signature
|
|
110
|
+
"Document Signature": "Dokumentunterschrift",
|
|
111
|
+
"Signature text displayed on all PDF documents": "Unterschriftstext auf allen PDF-Dokumenten",
|
|
112
|
+
"Add signature text...": "Unterschriftstext hinzufügen...",
|
|
109
113
|
// Overdue Notifications section
|
|
110
114
|
"Overdue Notifications": "Überfälligkeits-Benachrichtigungen",
|
|
111
115
|
"Automatically remind customers about overdue invoices": "Erinnern Sie Kunden automatisch an überfällige Rechnungen",
|
|
@@ -106,6 +106,10 @@ export default {
|
|
|
106
106
|
"Texto del pie de página mostrado en la parte inferior de todos los documentos PDF",
|
|
107
107
|
"{entity_name} | Due Date: {document_due_date} | Invoice #{document_number}":
|
|
108
108
|
"{entity_name} | Vencimiento: {document_due_date} | Factura #{document_number}",
|
|
109
|
+
// Document signature
|
|
110
|
+
"Document Signature": "Firma del documento",
|
|
111
|
+
"Signature text displayed on all PDF documents": "Texto de firma mostrado en todos los documentos PDF",
|
|
112
|
+
"Add signature text...": "Añadir texto de firma...",
|
|
109
113
|
// Overdue Notifications section
|
|
110
114
|
"Overdue Notifications": "Notificaciones de vencimiento",
|
|
111
115
|
"Automatically remind customers about overdue invoices":
|
|
@@ -106,6 +106,10 @@ export default {
|
|
|
106
106
|
"Texte de pied de page affiché en bas de tous les documents PDF",
|
|
107
107
|
"{entity_name} | Due Date: {document_due_date} | Invoice #{document_number}":
|
|
108
108
|
"{entity_name} | Échéance : {document_due_date} | Facture #{document_number}",
|
|
109
|
+
// Document signature
|
|
110
|
+
"Document Signature": "Signature du document",
|
|
111
|
+
"Signature text displayed on all PDF documents": "Texte de signature affiché sur tous les documents PDF",
|
|
112
|
+
"Add signature text...": "Ajouter un texte de signature...",
|
|
109
113
|
// Overdue Notifications section
|
|
110
114
|
"Overdue Notifications": "Notifications de retard",
|
|
111
115
|
"Automatically remind customers about overdue invoices":
|