@spaceinvoices/react-ui 0.4.6 → 0.4.8
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/advance-invoices/create/locales/de.ts +2 -1
- package/src/components/advance-invoices/create/locales/es.ts +2 -1
- package/src/components/advance-invoices/create/locales/fr.ts +2 -1
- package/src/components/advance-invoices/create/locales/hr.ts +2 -1
- package/src/components/advance-invoices/create/locales/it.ts +2 -1
- package/src/components/advance-invoices/create/locales/nl.ts +2 -1
- package/src/components/advance-invoices/create/locales/pl.ts +2 -1
- package/src/components/advance-invoices/create/locales/pt.ts +2 -1
- package/src/components/advance-invoices/create/locales/sl.ts +2 -1
- package/src/components/credit-notes/create/create-credit-note-form.tsx +52 -42
- package/src/components/credit-notes/create/locales/de.ts +2 -1
- package/src/components/credit-notes/create/locales/es.ts +2 -1
- package/src/components/credit-notes/create/locales/fr.ts +2 -1
- package/src/components/credit-notes/create/locales/hr.ts +2 -1
- package/src/components/credit-notes/create/locales/it.ts +2 -1
- package/src/components/credit-notes/create/locales/nl.ts +2 -1
- package/src/components/credit-notes/create/locales/pl.ts +2 -1
- package/src/components/credit-notes/create/locales/pt.ts +2 -1
- package/src/components/credit-notes/create/locales/sl.ts +2 -1
- 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/delivery-notes/create/locales/de.ts +2 -1
- package/src/components/delivery-notes/create/locales/es.ts +2 -1
- package/src/components/delivery-notes/create/locales/fr.ts +2 -1
- package/src/components/delivery-notes/create/locales/hr.ts +2 -1
- package/src/components/delivery-notes/create/locales/it.ts +2 -1
- package/src/components/delivery-notes/create/locales/nl.ts +2 -1
- package/src/components/delivery-notes/create/locales/pl.ts +2 -1
- package/src/components/delivery-notes/create/locales/pt.ts +2 -1
- package/src/components/delivery-notes/create/locales/sl.ts +2 -1
- 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/entity-settings-form/locales/de.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/es.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/fr.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/hr.ts +4 -2
- package/src/components/entities/entity-settings-form/locales/it.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/nl.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/pl.ts +6 -2
- package/src/components/entities/entity-settings-form/locales/pt.ts +6 -3
- package/src/components/entities/entity-settings-form/locales/sl.ts +4 -2
- 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/estimates/create/locales/de.ts +2 -1
- package/src/components/estimates/create/locales/es.ts +2 -1
- package/src/components/estimates/create/locales/fr.ts +2 -1
- package/src/components/estimates/create/locales/hr.ts +2 -1
- package/src/components/estimates/create/locales/it.ts +2 -1
- package/src/components/estimates/create/locales/nl.ts +2 -1
- package/src/components/estimates/create/locales/pl.ts +2 -1
- package/src/components/estimates/create/locales/pt.ts +2 -1
- package/src/components/estimates/create/locales/sl.ts +2 -1
- package/src/components/invoices/create/create-invoice-form.tsx +134 -62
- package/src/components/invoices/create/locales/de.ts +8 -1
- package/src/components/invoices/create/locales/es.ts +8 -1
- package/src/components/invoices/create/locales/fr.ts +8 -1
- package/src/components/invoices/create/locales/hr.ts +8 -1
- package/src/components/invoices/create/locales/it.ts +8 -1
- package/src/components/invoices/create/locales/nl.ts +8 -1
- package/src/components/invoices/create/locales/pl.ts +8 -1
- package/src/components/invoices/create/locales/pt.ts +8 -1
- package/src/components/invoices/create/locales/sl.ts +8 -1
- 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
|
|
|
@@ -59,7 +59,8 @@ export default {
|
|
|
59
59
|
Domestic: "Inland",
|
|
60
60
|
"EU B2B": "EU B2B",
|
|
61
61
|
"EU B2C": "EU B2C",
|
|
62
|
-
|
|
62
|
+
"3W B2B": "3W B2B",
|
|
63
|
+
"3W B2C": "3W B2C",
|
|
63
64
|
"Determining transaction type...": "Transaktionstyp wird ermittelt...",
|
|
64
65
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
65
66
|
"Diese Rechnung wird nicht fiskalisiert (nicht-inländische Transaktion)",
|
|
@@ -58,7 +58,8 @@ export default {
|
|
|
58
58
|
Domestic: "Nacional",
|
|
59
59
|
"EU B2B": "EU B2B",
|
|
60
60
|
"EU B2C": "EU B2C",
|
|
61
|
-
|
|
61
|
+
"3W B2B": "3W B2B",
|
|
62
|
+
"3W B2C": "3W B2C",
|
|
62
63
|
"Determining transaction type...": "Determinando tipo de transacción...",
|
|
63
64
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
64
65
|
"Esta factura no será fiscalizada (transacción no nacional)",
|
|
@@ -59,7 +59,8 @@ export default {
|
|
|
59
59
|
Domestic: "Nationale",
|
|
60
60
|
"EU B2B": "EU B2B",
|
|
61
61
|
"EU B2C": "EU B2C",
|
|
62
|
-
|
|
62
|
+
"3W B2B": "3W B2B",
|
|
63
|
+
"3W B2C": "3W B2C",
|
|
63
64
|
"Determining transaction type...": "Détermination du type de transaction...",
|
|
64
65
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
65
66
|
"Cette facture ne sera pas fiscalisée (transaction non nationale)",
|
|
@@ -58,7 +58,8 @@ export default {
|
|
|
58
58
|
Domestic: "Domaća",
|
|
59
59
|
"EU B2B": "EU B2B",
|
|
60
60
|
"EU B2C": "EU B2C",
|
|
61
|
-
|
|
61
|
+
"3W B2B": "3W B2B",
|
|
62
|
+
"3W B2C": "3W B2C",
|
|
62
63
|
"Determining transaction type...": "Određivanje vrste transakcije...",
|
|
63
64
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
64
65
|
"Ovaj račun neće biti fiskaliziran (nedomaća transakcija)",
|
|
@@ -58,7 +58,8 @@ export default {
|
|
|
58
58
|
Domestic: "Nazionale",
|
|
59
59
|
"EU B2B": "EU B2B",
|
|
60
60
|
"EU B2C": "EU B2C",
|
|
61
|
-
|
|
61
|
+
"3W B2B": "3W B2B",
|
|
62
|
+
"3W B2C": "3W B2C",
|
|
62
63
|
"Determining transaction type...": "Determinazione tipo di transazione...",
|
|
63
64
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
64
65
|
"Questa fattura non sarà fiscalizzata (transazione non nazionale)",
|
|
@@ -59,7 +59,8 @@ export default {
|
|
|
59
59
|
Domestic: "Binnenland",
|
|
60
60
|
"EU B2B": "EU B2B",
|
|
61
61
|
"EU B2C": "EU B2C",
|
|
62
|
-
|
|
62
|
+
"3W B2B": "3W B2B",
|
|
63
|
+
"3W B2C": "3W B2C",
|
|
63
64
|
"Determining transaction type...": "Transactietype bepalen...",
|
|
64
65
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
65
66
|
"Deze factuur wordt niet gefiscaliseerd (niet-binnenlandse transactie)",
|
|
@@ -58,7 +58,8 @@ export default {
|
|
|
58
58
|
Domestic: "Krajowa",
|
|
59
59
|
"EU B2B": "EU B2B",
|
|
60
60
|
"EU B2C": "EU B2C",
|
|
61
|
-
|
|
61
|
+
"3W B2B": "3W B2B",
|
|
62
|
+
"3W B2C": "3W B2C",
|
|
62
63
|
"Determining transaction type...": "Określanie typu transakcji...",
|
|
63
64
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
64
65
|
"Ta faktura nie będzie fiskalizowana (transakcja niekrajowa)",
|
|
@@ -59,7 +59,8 @@ export default {
|
|
|
59
59
|
Domestic: "Nacional",
|
|
60
60
|
"EU B2B": "EU B2B",
|
|
61
61
|
"EU B2C": "EU B2C",
|
|
62
|
-
|
|
62
|
+
"3W B2B": "3W B2B",
|
|
63
|
+
"3W B2C": "3W B2C",
|
|
63
64
|
"Determining transaction type...": "Determinando tipo de transação...",
|
|
64
65
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
65
66
|
"Esta fatura não será fiscalizada (transação não nacional)",
|
|
@@ -58,7 +58,8 @@ export default {
|
|
|
58
58
|
Domestic: "Domača",
|
|
59
59
|
"EU B2B": "EU B2B",
|
|
60
60
|
"EU B2C": "EU B2C",
|
|
61
|
-
|
|
61
|
+
"3W B2B": "3W B2B",
|
|
62
|
+
"3W B2C": "3W B2C",
|
|
62
63
|
"Determining transaction type...": "Določanje vrste transakcije...",
|
|
63
64
|
"This invoice will not be fiscalized (non-domestic transaction)":
|
|
64
65
|
"Ta račun ne bo fiskaliziran (nedomača transakcija)",
|
|
@@ -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 ? (
|
|
@@ -184,9 +184,12 @@ export default {
|
|
|
184
184
|
"tax-clauses.intra_eu_b2b.description":
|
|
185
185
|
"Für Verkäufe an EU-Unternehmen mit gültiger USt-IdNr. Enthält typischerweise eine Reverse-Charge-Erklärung.",
|
|
186
186
|
"Enter reverse charge clause...": "Reverse-Charge-Klausel eingeben...",
|
|
187
|
-
"tax-clauses.
|
|
188
|
-
"tax-clauses.
|
|
189
|
-
"Für Verkäufe an
|
|
187
|
+
"tax-clauses.3w_b2b.label": "Nicht-EU B2B (Export)",
|
|
188
|
+
"tax-clauses.3w_b2b.description":
|
|
189
|
+
"Für Verkäufe an Unternehmen außerhalb der EU. Enthält typischerweise die Steuerbefreiung für Exporte.",
|
|
190
|
+
"tax-clauses.3w_b2c.label": "Nicht-EU B2C (Export)",
|
|
191
|
+
"tax-clauses.3w_b2c.description":
|
|
192
|
+
"Für Verkäufe an Verbraucher außerhalb der EU. Enthält typischerweise die Steuerbefreiung für Exporte.",
|
|
190
193
|
"Enter export exemption clause...": "Exportbefreiungsklausel eingeben...",
|
|
191
194
|
"tax-clauses.domestic.label": "Standard / Inländisch",
|
|
192
195
|
"tax-clauses.domestic.description":
|
|
@@ -189,9 +189,12 @@ export default {
|
|
|
189
189
|
"tax-clauses.intra_eu_b2b.description":
|
|
190
190
|
"Para ventas a empresas de la UE con números de IVA válidos. Normalmente incluye una declaración de inversión del sujeto pasivo.",
|
|
191
191
|
"Enter reverse charge clause...": "Introduzca la cláusula de inversión del sujeto pasivo...",
|
|
192
|
-
"tax-clauses.
|
|
193
|
-
"tax-clauses.
|
|
194
|
-
"Para ventas a
|
|
192
|
+
"tax-clauses.3w_b2b.label": "Fuera de UE B2B (exportación)",
|
|
193
|
+
"tax-clauses.3w_b2b.description":
|
|
194
|
+
"Para ventas a empresas fuera de la UE. Normalmente indica la exención de IVA por exportación.",
|
|
195
|
+
"tax-clauses.3w_b2c.label": "Fuera de UE B2C (exportación)",
|
|
196
|
+
"tax-clauses.3w_b2c.description":
|
|
197
|
+
"Para ventas a consumidores fuera de la UE. Normalmente indica la exención de IVA por exportación.",
|
|
195
198
|
"Enter export exemption clause...": "Introduzca la cláusula de exención de exportación...",
|
|
196
199
|
"tax-clauses.domestic.label": "Predeterminado / Nacional",
|
|
197
200
|
"tax-clauses.domestic.description":
|
|
@@ -188,9 +188,12 @@ export default {
|
|
|
188
188
|
"tax-clauses.intra_eu_b2b.description":
|
|
189
189
|
"Pour les ventes à des entreprises UE avec des numéros de TVA valides. Inclut généralement une déclaration d'autoliquidation.",
|
|
190
190
|
"Enter reverse charge clause...": "Entrez la clause d'autoliquidation...",
|
|
191
|
-
"tax-clauses.
|
|
192
|
-
"tax-clauses.
|
|
193
|
-
"Pour les ventes à des
|
|
191
|
+
"tax-clauses.3w_b2b.label": "Hors UE B2B (exportation)",
|
|
192
|
+
"tax-clauses.3w_b2b.description":
|
|
193
|
+
"Pour les ventes à des entreprises hors UE. Indique généralement l'exonération de TVA à l'exportation.",
|
|
194
|
+
"tax-clauses.3w_b2c.label": "Hors UE B2C (exportation)",
|
|
195
|
+
"tax-clauses.3w_b2c.description":
|
|
196
|
+
"Pour les ventes à des consommateurs hors UE. Indique généralement l'exonération de TVA à l'exportation.",
|
|
194
197
|
"Enter export exemption clause...": "Entrez la clause d'exonération à l'exportation...",
|
|
195
198
|
"tax-clauses.domestic.label": "Par défaut / National",
|
|
196
199
|
"tax-clauses.domestic.description":
|