@spaceinvoices/react-ui 0.4.6 → 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/create/create-advance-invoice-form.tsx +60 -44
- package/src/components/credit-notes/create/create-credit-note-form.tsx +52 -42
- 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 +3 -2
- package/src/components/documents/create/document-details-section.tsx +6 -4
- 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 +1 -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/entities/settings/tax-rules-settings-form.tsx +31 -13
- package/src/components/estimates/create/create-estimate-form.tsx +3 -2
- package/src/components/invoices/create/create-invoice-form.tsx +134 -62
- package/src/components/invoices/create/locales/de.ts +6 -0
- package/src/components/invoices/create/locales/es.ts +6 -0
- package/src/components/invoices/create/locales/fr.ts +6 -0
- package/src/components/invoices/create/locales/hr.ts +6 -0
- package/src/components/invoices/create/locales/it.ts +6 -0
- package/src/components/invoices/create/locales/nl.ts +6 -0
- package/src/components/invoices/create/locales/pl.ts +6 -0
- package/src/components/invoices/create/locales/pt.ts +6 -0
- package/src/components/invoices/create/locales/sl.ts +6 -0
- package/src/components/invoices/invoices.hooks.ts +1 -1
- package/src/components/ui/progress.tsx +27 -0
- package/src/generate-schemas.ts +15 -2
- package/src/generated/schemas/advanceinvoice.ts +2 -0
- package/src/generated/schemas/creditnote.ts +2 -0
- package/src/generated/schemas/customer.ts +2 -0
- package/src/generated/schemas/deliverynote.ts +2 -0
- package/src/generated/schemas/entity.ts +10 -0
- package/src/generated/schemas/estimate.ts +2 -0
- package/src/generated/schemas/finasettings.ts +4 -3
- package/src/generated/schemas/invoice.ts +2 -0
- package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +16 -10
- package/src/generated/schemas/rendercreditnotepreview_body.ts +16 -10
- package/src/generated/schemas/renderdeliverynotepreview_body.ts +14 -7
- package/src/generated/schemas/renderestimatepreview_body.ts +14 -7
- package/src/generated/schemas/renderinvoicepreview_body.ts +16 -10
- package/src/generated/schemas/startpdfexport_body.ts +12 -17
- package/src/hooks/use-transaction-type-check.ts +152 -0
- package/src/hooks/use-vies-check.ts +7 -131
|
@@ -1,68 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Top customers hook using the entity stats API.
|
|
3
3
|
* Server-side aggregation by customer for accurate rankings.
|
|
4
|
+
* Sends 1 query in a batch request.
|
|
4
5
|
*/
|
|
5
6
|
import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
|
|
6
|
-
import {
|
|
7
|
-
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
8
|
-
import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
|
|
7
|
+
import { useStatsQuery } from "../shared/use-stats-query";
|
|
9
8
|
|
|
10
9
|
export const TOP_CUSTOMERS_CACHE_KEY = "dashboard-top-customers";
|
|
11
10
|
|
|
12
11
|
export type TopCustomersData = { name: string; revenue: number }[];
|
|
13
12
|
|
|
14
13
|
export function useTopCustomersData(entityId: string | undefined, limit = 5) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
table: "invoices",
|
|
28
|
-
filters: { is_draft: false, voided_at: null },
|
|
29
|
-
group_by: ["customer_name", "quote_currency"], // Include currency for display
|
|
30
|
-
order_by: [{ field: "revenue", direction: "desc" }],
|
|
31
|
-
limit: limit * 2, // Get more results to aggregate across currencies
|
|
32
|
-
},
|
|
33
|
-
{ entity_id: entityId },
|
|
34
|
-
);
|
|
14
|
+
const query = useStatsQuery(
|
|
15
|
+
entityId,
|
|
16
|
+
{
|
|
17
|
+
metrics: [
|
|
18
|
+
{ type: "sum", field: "total_with_tax_converted", alias: "revenue" },
|
|
19
|
+
{ type: "count", alias: "invoice_count" },
|
|
20
|
+
],
|
|
21
|
+
table: "invoices",
|
|
22
|
+
filters: { is_draft: false, voided_at: null },
|
|
23
|
+
group_by: ["customer_name", "quote_currency"],
|
|
24
|
+
order_by: [{ field: "revenue", direction: "desc" }],
|
|
25
|
+
limit: limit * 2,
|
|
35
26
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
currency = String(row.quote_currency);
|
|
27
|
+
{
|
|
28
|
+
select: (response) => {
|
|
29
|
+
const data = response.data || [];
|
|
30
|
+
|
|
31
|
+
// Aggregate by customer name (in case of multiple quote_currency rows)
|
|
32
|
+
const customerMap: Record<string, number> = {};
|
|
33
|
+
let currency = "EUR";
|
|
34
|
+
|
|
35
|
+
for (const row of data as StatsQueryDataItem[]) {
|
|
36
|
+
const name = String(row.customer_name || "Unknown");
|
|
37
|
+
customerMap[name] = (customerMap[name] || 0) + (Number(row.revenue) || 0);
|
|
38
|
+
if (row.quote_currency && currency === "EUR") {
|
|
39
|
+
currency = String(row.quote_currency);
|
|
40
|
+
}
|
|
51
41
|
}
|
|
52
|
-
}
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.slice(0, limit);
|
|
43
|
+
const customers = Object.entries(customerMap)
|
|
44
|
+
.map(([name, revenue]) => ({ name, revenue }))
|
|
45
|
+
.sort((a, b) => b.revenue - a.revenue)
|
|
46
|
+
.slice(0, limit);
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
48
|
+
return {
|
|
49
|
+
data: customers,
|
|
50
|
+
currency,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
64
53
|
},
|
|
65
|
-
|
|
54
|
+
);
|
|
66
55
|
|
|
67
56
|
return {
|
|
68
57
|
data: query.data?.data || [],
|
|
@@ -10,7 +10,7 @@ import { Form } from "@/ui/components/ui/form";
|
|
|
10
10
|
import { Label } from "@/ui/components/ui/label";
|
|
11
11
|
import { createDeliveryNoteSchema } from "@/ui/generated/schemas";
|
|
12
12
|
import { useNextDocumentNumber } from "@/ui/hooks/use-next-document-number";
|
|
13
|
-
import {
|
|
13
|
+
import { useTransactionTypeCheck } from "@/ui/hooks/use-transaction-type-check";
|
|
14
14
|
import type { ComponentTranslationProps } from "@/ui/lib/translation";
|
|
15
15
|
import { createTranslation } from "@/ui/lib/translation";
|
|
16
16
|
import { useEntities } from "@/ui/providers/entities-context";
|
|
@@ -202,12 +202,13 @@ export default function CreateDeliveryNoteForm({
|
|
|
202
202
|
transactionType,
|
|
203
203
|
isFetching: isViesFetching,
|
|
204
204
|
warning: viesWarning,
|
|
205
|
-
} =
|
|
205
|
+
} = useTransactionTypeCheck({
|
|
206
206
|
issuerCountryCode: activeEntity?.country_code,
|
|
207
207
|
isTaxSubject: activeEntity?.is_tax_subject ?? true,
|
|
208
208
|
customerCountry: formValues.customer?.country,
|
|
209
209
|
customerCountryCode: formValues.customer?.country_code,
|
|
210
210
|
customerTaxNumber: formValues.customer?.tax_number,
|
|
211
|
+
customerIsEndConsumer: (formValues.customer as any)?.is_end_consumer,
|
|
211
212
|
enabled: !!activeEntity,
|
|
212
213
|
});
|
|
213
214
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared document details section for invoices and estimates
|
|
3
3
|
* Handles: number, date, and document-type-specific date field (date_due or date_valid_till)
|
|
4
4
|
*/
|
|
5
|
-
import type { Entity, Estimate, Invoice,
|
|
5
|
+
import type { Entity, Estimate, Invoice, TransactionTypeCheckResponse } from "@spaceinvoices/js-sdk";
|
|
6
6
|
import { CalendarIcon, ChevronDown, Globe, Info, Loader2 } from "lucide-react";
|
|
7
7
|
import { useRef, useState } from "react";
|
|
8
8
|
import { Badge } from "@/ui/components/ui/badge";
|
|
@@ -596,20 +596,22 @@ export function DocumentNoteField({
|
|
|
596
596
|
* Tax clause field component with smart code insertion button
|
|
597
597
|
* Similar to DocumentNoteField, auto-populated from entity settings based on transaction type
|
|
598
598
|
*/
|
|
599
|
-
type TransactionType =
|
|
599
|
+
type TransactionType = TransactionTypeCheckResponse["transaction_type"];
|
|
600
600
|
|
|
601
601
|
const TRANSACTION_TYPE_LABELS: Record<NonNullable<TransactionType>, string> = {
|
|
602
602
|
domestic: "Domestic",
|
|
603
603
|
intra_eu_b2b: "EU B2B",
|
|
604
604
|
intra_eu_b2c: "EU B2C",
|
|
605
|
-
|
|
605
|
+
"3w_b2b": "3W B2B",
|
|
606
|
+
"3w_b2c": "3W B2C",
|
|
606
607
|
};
|
|
607
608
|
|
|
608
609
|
const TRANSACTION_TYPE_VARIANTS: Record<NonNullable<TransactionType>, "secondary" | "default" | "outline"> = {
|
|
609
610
|
domestic: "secondary",
|
|
610
611
|
intra_eu_b2b: "default",
|
|
611
612
|
intra_eu_b2c: "outline",
|
|
612
|
-
|
|
613
|
+
"3w_b2b": "outline",
|
|
614
|
+
"3w_b2c": "outline",
|
|
613
615
|
};
|
|
614
616
|
|
|
615
617
|
export function DocumentTaxClauseField({
|
|
@@ -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
|
);
|
|
@@ -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 ? (
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
2
|
-
import type {
|
|
3
|
-
Entity,
|
|
4
|
-
EntitySettings,
|
|
5
|
-
EntitySettingsTaxClauseDefaults,
|
|
6
|
-
TaxRules,
|
|
7
|
-
} from "@spaceinvoices/js-sdk";
|
|
2
|
+
import type { Entity, EntitySettings, EntitySettingsTaxClauseDefaults, TaxRules } from "@spaceinvoices/js-sdk";
|
|
8
3
|
import { ChevronDown, Globe, MessageSquareText } from "lucide-react";
|
|
9
4
|
import type { ReactNode } from "react";
|
|
10
5
|
import { useState } from "react";
|
|
@@ -39,7 +34,8 @@ const taxRulesSettingsSchema = z.object({
|
|
|
39
34
|
require_gross_prices: z.boolean(),
|
|
40
35
|
// Tax clause defaults per transaction type
|
|
41
36
|
tax_clause_intra_eu_b2b: z.string().optional(),
|
|
42
|
-
|
|
37
|
+
tax_clause_3w_b2b: z.string().optional(),
|
|
38
|
+
tax_clause_3w_b2c: z.string().optional(),
|
|
43
39
|
tax_clause_domestic: z.string().optional(),
|
|
44
40
|
tax_clause_intra_eu_b2c: z.string().optional(),
|
|
45
41
|
});
|
|
@@ -92,7 +88,8 @@ export function TaxRulesSettingsForm({
|
|
|
92
88
|
auto_remove_tax_export: currentTaxRules.auto_remove_tax_export ?? false,
|
|
93
89
|
require_gross_prices: currentTaxRules.require_gross_prices ?? false,
|
|
94
90
|
tax_clause_intra_eu_b2b: currentTaxClauseDefaults.intra_eu_b2b ?? "",
|
|
95
|
-
|
|
91
|
+
tax_clause_3w_b2b: (currentTaxClauseDefaults as any)["3w_b2b"] ?? currentTaxClauseDefaults.export ?? "",
|
|
92
|
+
tax_clause_3w_b2c: (currentTaxClauseDefaults as any)["3w_b2c"] ?? currentTaxClauseDefaults.export ?? "",
|
|
96
93
|
tax_clause_domestic: currentTaxClauseDefaults.domestic ?? "",
|
|
97
94
|
tax_clause_intra_eu_b2c: currentTaxClauseDefaults.intra_eu_b2c ?? "",
|
|
98
95
|
},
|
|
@@ -127,7 +124,8 @@ export function TaxRulesSettingsForm({
|
|
|
127
124
|
},
|
|
128
125
|
tax_clause_defaults: {
|
|
129
126
|
intra_eu_b2b: values.tax_clause_intra_eu_b2b || null,
|
|
130
|
-
|
|
127
|
+
"3w_b2b": values.tax_clause_3w_b2b || null,
|
|
128
|
+
"3w_b2c": values.tax_clause_3w_b2c || null,
|
|
131
129
|
domestic: values.tax_clause_domestic || null,
|
|
132
130
|
intra_eu_b2c: values.tax_clause_intra_eu_b2c || null,
|
|
133
131
|
},
|
|
@@ -288,14 +286,34 @@ export function TaxRulesSettingsForm({
|
|
|
288
286
|
)}
|
|
289
287
|
/>
|
|
290
288
|
|
|
291
|
-
{/*
|
|
289
|
+
{/* 3W B2B (non-EU business) */}
|
|
292
290
|
<FormField
|
|
293
291
|
control={form.control}
|
|
294
|
-
name="
|
|
292
|
+
name="tax_clause_3w_b2b"
|
|
295
293
|
render={({ field }) => (
|
|
296
294
|
<FormItem className="rounded-lg border p-4">
|
|
297
|
-
<FormLabel>{t("tax-clauses.
|
|
298
|
-
<FormDescription>{t("tax-clauses.
|
|
295
|
+
<FormLabel>{t("tax-clauses.3w_b2b.label")}</FormLabel>
|
|
296
|
+
<FormDescription>{t("tax-clauses.3w_b2b.description")}</FormDescription>
|
|
297
|
+
<FormControl>
|
|
298
|
+
<Textarea
|
|
299
|
+
placeholder={t("Enter export exemption clause...")}
|
|
300
|
+
className="min-h-[80px] resize-y"
|
|
301
|
+
{...field}
|
|
302
|
+
/>
|
|
303
|
+
</FormControl>
|
|
304
|
+
<FormMessage />
|
|
305
|
+
</FormItem>
|
|
306
|
+
)}
|
|
307
|
+
/>
|
|
308
|
+
|
|
309
|
+
{/* 3W B2C (non-EU consumer) */}
|
|
310
|
+
<FormField
|
|
311
|
+
control={form.control}
|
|
312
|
+
name="tax_clause_3w_b2c"
|
|
313
|
+
render={({ field }) => (
|
|
314
|
+
<FormItem className="rounded-lg border p-4">
|
|
315
|
+
<FormLabel>{t("tax-clauses.3w_b2c.label")}</FormLabel>
|
|
316
|
+
<FormDescription>{t("tax-clauses.3w_b2c.description")}</FormDescription>
|
|
299
317
|
<FormControl>
|
|
300
318
|
<Textarea
|
|
301
319
|
placeholder={t("Enter export exemption clause...")}
|
|
@@ -11,7 +11,7 @@ import { Form } from "@/ui/components/ui/form";
|
|
|
11
11
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/ui/components/ui/tooltip";
|
|
12
12
|
import { createEstimateSchema } from "@/ui/generated/schemas";
|
|
13
13
|
import { useNextDocumentNumber } from "@/ui/hooks/use-next-document-number";
|
|
14
|
-
import {
|
|
14
|
+
import { useTransactionTypeCheck } from "@/ui/hooks/use-transaction-type-check";
|
|
15
15
|
import type { ComponentTranslationProps } from "@/ui/lib/translation";
|
|
16
16
|
import { createTranslation } from "@/ui/lib/translation";
|
|
17
17
|
import { useEntities } from "@/ui/providers/entities-context";
|
|
@@ -259,12 +259,13 @@ export default function CreateEstimateForm({
|
|
|
259
259
|
transactionType,
|
|
260
260
|
isFetching: isViesFetching,
|
|
261
261
|
warning: viesWarning,
|
|
262
|
-
} =
|
|
262
|
+
} = useTransactionTypeCheck({
|
|
263
263
|
issuerCountryCode: activeEntity?.country_code,
|
|
264
264
|
isTaxSubject: activeEntity?.is_tax_subject ?? true,
|
|
265
265
|
customerCountry: formValues.customer?.country,
|
|
266
266
|
customerCountryCode: formValues.customer?.country_code,
|
|
267
267
|
customerTaxNumber: formValues.customer?.tax_number,
|
|
268
|
+
customerIsEndConsumer: (formValues.customer as any)?.is_end_consumer,
|
|
268
269
|
enabled: !!activeEntity,
|
|
269
270
|
});
|
|
270
271
|
|