@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
package/cli/dist/index.js
CHANGED
|
@@ -870,7 +870,7 @@ async function list(options = {}) {
|
|
|
870
870
|
|
|
871
871
|
// cli/src/index.ts
|
|
872
872
|
var program = new Command();
|
|
873
|
-
program.name("spaceinvoices-ui").description("CLI for adding Space Invoices React UI components to your project").version("0.4.
|
|
873
|
+
program.name("spaceinvoices-ui").description("CLI for adding Space Invoices React UI components to your project").version("0.4.7");
|
|
874
874
|
program.option("--local <path>", "Use local registry from specified path (for development)");
|
|
875
875
|
program.command("init").description("Initialize Space Invoices UI in your project").option("-y, --yes", "Skip prompts and use defaults").option("-f, --force", "Overwrite existing configuration").option("--cwd <path>", "Working directory (defaults to current directory)").action(async (options) => {
|
|
876
876
|
const globalOpts = program.opts();
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/ui/c
|
|
|
13
13
|
import type { CreateAdvanceInvoiceSchema } from "@/ui/generated/schemas";
|
|
14
14
|
import { createAdvanceInvoiceSchema } from "@/ui/generated/schemas";
|
|
15
15
|
import { useNextDocumentNumber } from "@/ui/hooks/use-next-document-number";
|
|
16
|
-
import {
|
|
16
|
+
import { useTransactionTypeCheck } from "@/ui/hooks/use-transaction-type-check";
|
|
17
17
|
import type { ComponentTranslationProps } from "@/ui/lib/translation";
|
|
18
18
|
import { createTranslation } from "@/ui/lib/translation";
|
|
19
19
|
import { cn } from "@/ui/lib/utils";
|
|
@@ -350,19 +350,6 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
350
350
|
const isFinaActive =
|
|
351
351
|
isFinaEnabled && hasFinaPremises && selectedFinaBusinessPremiseName && selectedFinaElectronicDeviceName;
|
|
352
352
|
|
|
353
|
-
// ============================================================================
|
|
354
|
-
// Next Advance Invoice Number Preview
|
|
355
|
-
// ============================================================================
|
|
356
|
-
const { data: nextNumberData, isLoading: isNextNumberLoading } = useNextDocumentNumber(entityId, "advance_invoice", {
|
|
357
|
-
businessPremiseName: isFursActive ? selectedPremiseName : undefined,
|
|
358
|
-
electronicDeviceName: isFursActive ? selectedDeviceName : undefined,
|
|
359
|
-
enabled: !!entityId && !isFursLoading && isFursSelectionReady && !isFinaLoading && isFinaSelectionReady,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// Overall loading state
|
|
363
|
-
const isFormDataLoading =
|
|
364
|
-
isFursLoading || !isFursSelectionReady || isFinaLoading || !isFinaSelectionReady || isNextNumberLoading;
|
|
365
|
-
|
|
366
353
|
// Update header action with FURS and e-SLOG toggle buttons
|
|
367
354
|
useEffect(() => {
|
|
368
355
|
if (!onHeaderActionChange) return;
|
|
@@ -472,13 +459,6 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
472
459
|
t,
|
|
473
460
|
]);
|
|
474
461
|
|
|
475
|
-
// Pre-fill advance invoice number from preview
|
|
476
|
-
useEffect(() => {
|
|
477
|
-
if (nextNumberData?.number) {
|
|
478
|
-
form.setValue("number", nextNumberData.number);
|
|
479
|
-
}
|
|
480
|
-
}, [nextNumberData?.number, form]);
|
|
481
|
-
|
|
482
462
|
const formValues = useWatch({
|
|
483
463
|
control: form.control,
|
|
484
464
|
});
|
|
@@ -489,20 +469,55 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
489
469
|
const {
|
|
490
470
|
reverseChargeApplies,
|
|
491
471
|
transactionType,
|
|
492
|
-
customerCountryCode: viesCustomerCountryCode,
|
|
493
472
|
isFetching: isViesFetching,
|
|
494
473
|
warning: viesWarning,
|
|
495
|
-
} =
|
|
474
|
+
} = useTransactionTypeCheck({
|
|
496
475
|
issuerCountryCode: activeEntity?.country_code,
|
|
497
476
|
isTaxSubject: activeEntity?.is_tax_subject ?? true,
|
|
498
477
|
customerCountry: formValues.customer?.country,
|
|
499
478
|
customerCountryCode: formValues.customer?.country_code,
|
|
500
479
|
customerTaxNumber: formValues.customer?.tax_number,
|
|
480
|
+
customerIsEndConsumer: (formValues.customer as any)?.is_end_consumer,
|
|
501
481
|
enabled: !!activeEntity,
|
|
502
482
|
});
|
|
503
483
|
|
|
504
|
-
// FINA
|
|
505
|
-
const
|
|
484
|
+
// FINA numbering guard: use FINA numbering for domestic transactions (or all if unified numbering is on)
|
|
485
|
+
const finaUnifiedNumbering = finaSettings?.unified_numbering !== false;
|
|
486
|
+
const useFinaNumbering =
|
|
487
|
+
!!isFinaActive && (finaUnifiedNumbering || transactionType == null || transactionType === "domestic");
|
|
488
|
+
const isFinaNonDomestic = !!isFinaActive && !useFinaNumbering;
|
|
489
|
+
|
|
490
|
+
// ============================================================================
|
|
491
|
+
// Next Advance Invoice Number Preview
|
|
492
|
+
// ============================================================================
|
|
493
|
+
// Use same premise/device params for both FURS and FINA (entity is either one, never both)
|
|
494
|
+
const activePremiseNameForNumber = isFursActive
|
|
495
|
+
? selectedPremiseName
|
|
496
|
+
: useFinaNumbering
|
|
497
|
+
? selectedFinaBusinessPremiseName
|
|
498
|
+
: undefined;
|
|
499
|
+
const activeDeviceNameForNumber = isFursActive
|
|
500
|
+
? selectedDeviceName
|
|
501
|
+
: useFinaNumbering
|
|
502
|
+
? selectedFinaElectronicDeviceName
|
|
503
|
+
: undefined;
|
|
504
|
+
|
|
505
|
+
const { data: nextNumberData, isLoading: isNextNumberLoading } = useNextDocumentNumber(entityId, "advance_invoice", {
|
|
506
|
+
businessPremiseName: activePremiseNameForNumber,
|
|
507
|
+
electronicDeviceName: activeDeviceNameForNumber,
|
|
508
|
+
enabled: !!entityId && !isFursLoading && isFursSelectionReady && !isFinaLoading && isFinaSelectionReady,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Overall loading state
|
|
512
|
+
const isFormDataLoading =
|
|
513
|
+
isFursLoading || !isFursSelectionReady || isFinaLoading || !isFinaSelectionReady || isNextNumberLoading;
|
|
514
|
+
|
|
515
|
+
// Pre-fill advance invoice number from preview
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
if (nextNumberData?.number) {
|
|
518
|
+
form.setValue("number", nextNumberData.number);
|
|
519
|
+
}
|
|
520
|
+
}, [nextNumberData?.number, form]);
|
|
506
521
|
|
|
507
522
|
// Auto-populate tax_clause from entity settings when transaction type changes
|
|
508
523
|
const effectiveTransactionType = transactionType ?? "domestic";
|
|
@@ -588,11 +603,7 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
588
603
|
|
|
589
604
|
// Build FINA options (skip for drafts; FINA can't be skipped)
|
|
590
605
|
const finaOptions =
|
|
591
|
-
!isDraft &&
|
|
592
|
-
isFinaEnabled &&
|
|
593
|
-
!isFinaNonDomestic &&
|
|
594
|
-
selectedFinaBusinessPremiseName &&
|
|
595
|
-
selectedFinaElectronicDeviceName
|
|
606
|
+
!isDraft && useFinaNumbering && selectedFinaBusinessPremiseName && selectedFinaElectronicDeviceName
|
|
596
607
|
? {
|
|
597
608
|
business_premise_name: selectedFinaBusinessPremiseName,
|
|
598
609
|
electronic_device_name: selectedFinaElectronicDeviceName,
|
|
@@ -621,8 +632,7 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
621
632
|
form,
|
|
622
633
|
isEslogAvailable,
|
|
623
634
|
isFursEnabled,
|
|
624
|
-
|
|
625
|
-
isFinaNonDomestic,
|
|
635
|
+
useFinaNumbering,
|
|
626
636
|
markAsPaid,
|
|
627
637
|
originalCustomer,
|
|
628
638
|
paymentTypes,
|
|
@@ -729,16 +739,24 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
729
739
|
<Skeleton className="h-7 w-24" />
|
|
730
740
|
<Skeleton className="h-10 w-full" />
|
|
731
741
|
</div>
|
|
732
|
-
<div className="flex-1 space-y-
|
|
742
|
+
<div className="flex-1 space-y-3">
|
|
733
743
|
<Skeleton className="h-7 w-20" />
|
|
734
|
-
<
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
<
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
744
|
+
<div className="flex items-center gap-3">
|
|
745
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
746
|
+
<Skeleton className="h-10 flex-1" />
|
|
747
|
+
</div>
|
|
748
|
+
<div className="flex items-center gap-3">
|
|
749
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
750
|
+
<Skeleton className="h-10 flex-1" />
|
|
751
|
+
</div>
|
|
752
|
+
<div className="flex items-center gap-3">
|
|
753
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
754
|
+
<Skeleton className="h-10 flex-1" />
|
|
755
|
+
</div>
|
|
756
|
+
<div className="flex items-center gap-3">
|
|
757
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
758
|
+
<Skeleton className="h-10 flex-1" />
|
|
759
|
+
</div>
|
|
742
760
|
<div className="space-y-3 rounded-md border p-4">
|
|
743
761
|
<div className="flex items-center gap-3">
|
|
744
762
|
<Skeleton className="h-4 w-4 rounded" />
|
|
@@ -764,8 +782,6 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
764
782
|
<Skeleton className="h-5 w-12" />
|
|
765
783
|
<Skeleton className="h-24 w-full" />
|
|
766
784
|
</div>
|
|
767
|
-
|
|
768
|
-
<Skeleton className="h-10 w-24" />
|
|
769
785
|
</div>
|
|
770
786
|
);
|
|
771
787
|
}
|
|
@@ -821,7 +837,7 @@ export default function CreateAdvanceInvoiceForm({
|
|
|
821
837
|
: undefined
|
|
822
838
|
}
|
|
823
839
|
finaInline={
|
|
824
|
-
|
|
840
|
+
useFinaNumbering
|
|
825
841
|
? {
|
|
826
842
|
premises: activeFinaPremises.map((p: any) => ({
|
|
827
843
|
id: p.id,
|
|
@@ -9,7 +9,7 @@ import { Form } from "@/ui/components/ui/form";
|
|
|
9
9
|
import { Skeleton } from "@/ui/components/ui/skeleton";
|
|
10
10
|
import { createCreditNoteSchema } from "@/ui/generated/schemas";
|
|
11
11
|
import { useNextDocumentNumber } from "@/ui/hooks/use-next-document-number";
|
|
12
|
-
import {
|
|
12
|
+
import { useTransactionTypeCheck } from "@/ui/hooks/use-transaction-type-check";
|
|
13
13
|
import type { ComponentTranslationProps } from "@/ui/lib/translation";
|
|
14
14
|
import { createTranslation } from "@/ui/lib/translation";
|
|
15
15
|
import { useEntities } from "@/ui/providers/entities-context";
|
|
@@ -254,19 +254,6 @@ export default function CreateCreditNoteForm({
|
|
|
254
254
|
const isFinaActive =
|
|
255
255
|
isFinaEnabled && hasFinaPremises && selectedFinaBusinessPremiseName && selectedFinaElectronicDeviceName;
|
|
256
256
|
|
|
257
|
-
// ============================================================================
|
|
258
|
-
// Next Credit Note Number Preview
|
|
259
|
-
// ============================================================================
|
|
260
|
-
const { data: nextNumberData, isLoading: isNextNumberLoading } = useNextDocumentNumber(entityId, "credit_note", {
|
|
261
|
-
businessPremiseName: isFursActive ? selectedPremiseName : undefined,
|
|
262
|
-
electronicDeviceName: isFursActive ? selectedDeviceName : undefined,
|
|
263
|
-
enabled: !!entityId && !isFursLoading && isFursSelectionReady && !isFinaLoading && isFinaSelectionReady,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Overall loading state
|
|
267
|
-
const isFormDataLoading =
|
|
268
|
-
isFursLoading || !isFursSelectionReady || isFinaLoading || !isFinaSelectionReady || isNextNumberLoading;
|
|
269
|
-
|
|
270
257
|
// No header action for credit notes - FINA can't be skipped
|
|
271
258
|
|
|
272
259
|
// Get default payment terms and footer from entity settings
|
|
@@ -324,20 +311,48 @@ export default function CreateCreditNoteForm({
|
|
|
324
311
|
const {
|
|
325
312
|
reverseChargeApplies,
|
|
326
313
|
transactionType,
|
|
327
|
-
customerCountryCode: viesCustomerCountryCode,
|
|
328
314
|
isFetching: isViesFetching,
|
|
329
315
|
warning: viesWarning,
|
|
330
|
-
} =
|
|
316
|
+
} = useTransactionTypeCheck({
|
|
331
317
|
issuerCountryCode: activeEntity?.country_code,
|
|
332
318
|
isTaxSubject: activeEntity?.is_tax_subject ?? true,
|
|
333
319
|
customerCountry: formValues.customer?.country,
|
|
334
320
|
customerCountryCode: formValues.customer?.country_code,
|
|
335
321
|
customerTaxNumber: formValues.customer?.tax_number,
|
|
322
|
+
customerIsEndConsumer: (formValues.customer as any)?.is_end_consumer,
|
|
336
323
|
enabled: !!activeEntity,
|
|
337
324
|
});
|
|
338
325
|
|
|
339
|
-
// FINA
|
|
340
|
-
const
|
|
326
|
+
// FINA numbering guard: use FINA numbering for domestic transactions (or all if unified numbering is on)
|
|
327
|
+
const finaUnifiedNumbering = finaSettings?.unified_numbering !== false;
|
|
328
|
+
const useFinaNumbering =
|
|
329
|
+
!!isFinaActive && (finaUnifiedNumbering || transactionType == null || transactionType === "domestic");
|
|
330
|
+
const isFinaNonDomestic = !!isFinaActive && !useFinaNumbering;
|
|
331
|
+
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// Next Credit Note Number Preview
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// Use same premise/device params for both FURS and FINA (entity is either one, never both)
|
|
336
|
+
const activePremiseNameForNumber = isFursActive
|
|
337
|
+
? selectedPremiseName
|
|
338
|
+
: useFinaNumbering
|
|
339
|
+
? selectedFinaBusinessPremiseName
|
|
340
|
+
: undefined;
|
|
341
|
+
const activeDeviceNameForNumber = isFursActive
|
|
342
|
+
? selectedDeviceName
|
|
343
|
+
: useFinaNumbering
|
|
344
|
+
? selectedFinaElectronicDeviceName
|
|
345
|
+
: undefined;
|
|
346
|
+
|
|
347
|
+
const { data: nextNumberData, isLoading: isNextNumberLoading } = useNextDocumentNumber(entityId, "credit_note", {
|
|
348
|
+
businessPremiseName: activePremiseNameForNumber,
|
|
349
|
+
electronicDeviceName: activeDeviceNameForNumber,
|
|
350
|
+
enabled: !!entityId && !isFursLoading && isFursSelectionReady && !isFinaLoading && isFinaSelectionReady,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Overall loading state
|
|
354
|
+
const isFormDataLoading =
|
|
355
|
+
isFursLoading || !isFursSelectionReady || isFinaLoading || !isFinaSelectionReady || isNextNumberLoading;
|
|
341
356
|
|
|
342
357
|
// Auto-populate tax_clause from entity settings when transaction type changes
|
|
343
358
|
const effectiveTransactionType = transactionType ?? "domestic";
|
|
@@ -408,11 +423,7 @@ export default function CreateCreditNoteForm({
|
|
|
408
423
|
|
|
409
424
|
// Build FINA options (skip for drafts; FINA can't be skipped)
|
|
410
425
|
const finaOptions =
|
|
411
|
-
!isDraft &&
|
|
412
|
-
isFinaEnabled &&
|
|
413
|
-
!isFinaNonDomestic &&
|
|
414
|
-
selectedFinaBusinessPremiseName &&
|
|
415
|
-
selectedFinaElectronicDeviceName
|
|
426
|
+
!isDraft && useFinaNumbering && selectedFinaBusinessPremiseName && selectedFinaElectronicDeviceName
|
|
416
427
|
? {
|
|
417
428
|
business_premise_name: selectedFinaBusinessPremiseName,
|
|
418
429
|
electronic_device_name: selectedFinaElectronicDeviceName,
|
|
@@ -445,8 +456,7 @@ export default function CreateCreditNoteForm({
|
|
|
445
456
|
[
|
|
446
457
|
createCreditNote,
|
|
447
458
|
isFursEnabled,
|
|
448
|
-
|
|
449
|
-
isFinaNonDomestic,
|
|
459
|
+
useFinaNumbering,
|
|
450
460
|
markAsPaid,
|
|
451
461
|
originalCustomer,
|
|
452
462
|
paymentTypes,
|
|
@@ -558,21 +568,23 @@ export default function CreateCreditNoteForm({
|
|
|
558
568
|
<Skeleton className="h-7 w-24" />
|
|
559
569
|
<Skeleton className="h-10 w-full" />
|
|
560
570
|
</div>
|
|
561
|
-
<div className="flex-1 space-y-
|
|
571
|
+
<div className="flex-1 space-y-3">
|
|
562
572
|
<Skeleton className="h-7 w-20" />
|
|
563
|
-
<
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
<
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
<div className="
|
|
572
|
-
<
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
573
|
+
<div className="flex items-center gap-3">
|
|
574
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
575
|
+
<Skeleton className="h-10 flex-1" />
|
|
576
|
+
</div>
|
|
577
|
+
<div className="flex items-center gap-3">
|
|
578
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
579
|
+
<Skeleton className="h-10 flex-1" />
|
|
580
|
+
</div>
|
|
581
|
+
<div className="flex items-center gap-3">
|
|
582
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
583
|
+
<Skeleton className="h-10 flex-1" />
|
|
584
|
+
</div>
|
|
585
|
+
<div className="flex items-center gap-3">
|
|
586
|
+
<Skeleton className="h-5 w-[6.5rem] shrink-0" />
|
|
587
|
+
<Skeleton className="h-10 flex-1" />
|
|
576
588
|
</div>
|
|
577
589
|
</div>
|
|
578
590
|
</div>
|
|
@@ -593,8 +605,6 @@ export default function CreateCreditNoteForm({
|
|
|
593
605
|
<Skeleton className="h-5 w-12" />
|
|
594
606
|
<Skeleton className="h-24 w-full" />
|
|
595
607
|
</div>
|
|
596
|
-
|
|
597
|
-
<Skeleton className="h-10 w-24" />
|
|
598
608
|
</div>
|
|
599
609
|
);
|
|
600
610
|
}
|
|
@@ -631,7 +641,7 @@ export default function CreateCreditNoteForm({
|
|
|
631
641
|
: undefined
|
|
632
642
|
}
|
|
633
643
|
finaInline={
|
|
634
|
-
|
|
644
|
+
useFinaNumbering
|
|
635
645
|
? {
|
|
636
646
|
premises: activeFinaPremises.map((p: any) => ({
|
|
637
647
|
id: p.id,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collection rate hook using the entity stats API.
|
|
3
3
|
* Server-side aggregation for accurate totals.
|
|
4
|
+
* Sends 4 queries in a single batch request.
|
|
4
5
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
|
|
6
|
+
import type { StatsQueryRequest } from "@spaceinvoices/js-sdk";
|
|
7
|
+
import { useStatsBatchQuery } from "../shared/use-stats-query";
|
|
8
8
|
|
|
9
9
|
export const COLLECTION_RATE_CACHE_KEY = "dashboard-collection-rate";
|
|
10
10
|
|
|
@@ -16,98 +16,54 @@ export type CollectionRateData = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export function useCollectionRateData(entityId: string | undefined) {
|
|
19
|
-
const
|
|
19
|
+
const queries: StatsQueryRequest[] = [
|
|
20
|
+
// [0] Total invoiced (including voided — counter credit notes cancel them out)
|
|
21
|
+
{
|
|
22
|
+
metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
|
|
23
|
+
table: "invoices",
|
|
24
|
+
filters: { is_draft: false },
|
|
25
|
+
},
|
|
26
|
+
// [1] Invoice payments (credit_note_id IS NULL)
|
|
27
|
+
{
|
|
28
|
+
metrics: [{ type: "sum", field: "amount", alias: "total" }],
|
|
29
|
+
table: "payments",
|
|
30
|
+
filters: { credit_note_id: null },
|
|
31
|
+
},
|
|
32
|
+
// [2] Credit note payments / refunds (credit_note_id IS NOT NULL)
|
|
33
|
+
{
|
|
34
|
+
metrics: [{ type: "sum", field: "amount", alias: "total" }],
|
|
35
|
+
table: "payments",
|
|
36
|
+
filters: { credit_note_id: { not: null } },
|
|
37
|
+
},
|
|
38
|
+
// [3] Credit notes total (subtracted from invoiced to get net revenue)
|
|
39
|
+
{
|
|
40
|
+
metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
|
|
41
|
+
table: "credit_notes",
|
|
42
|
+
filters: { is_draft: false },
|
|
43
|
+
},
|
|
44
|
+
];
|
|
20
45
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
|
|
31
|
-
table: "invoices",
|
|
32
|
-
filters: { is_draft: false },
|
|
33
|
-
},
|
|
34
|
-
{ entity_id: entityId },
|
|
35
|
-
);
|
|
36
|
-
},
|
|
37
|
-
enabled: !!entityId && !!sdk,
|
|
38
|
-
staleTime: 120_000,
|
|
39
|
-
},
|
|
40
|
-
// Invoice payments (credit_note_id IS NULL)
|
|
41
|
-
{
|
|
42
|
-
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-payments"],
|
|
43
|
-
queryFn: async () => {
|
|
44
|
-
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
45
|
-
return sdk.entityStats.queryEntityStats(
|
|
46
|
-
{
|
|
47
|
-
metrics: [{ type: "sum", field: "amount", alias: "total" }],
|
|
48
|
-
table: "payments",
|
|
49
|
-
filters: { credit_note_id: null },
|
|
50
|
-
},
|
|
51
|
-
{ entity_id: entityId },
|
|
52
|
-
);
|
|
53
|
-
},
|
|
54
|
-
enabled: !!entityId && !!sdk,
|
|
55
|
-
staleTime: 120_000,
|
|
56
|
-
},
|
|
57
|
-
// Credit note payments / refunds (credit_note_id IS NOT NULL)
|
|
58
|
-
{
|
|
59
|
-
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-payments"],
|
|
60
|
-
queryFn: async () => {
|
|
61
|
-
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
62
|
-
return sdk.entityStats.queryEntityStats(
|
|
63
|
-
{
|
|
64
|
-
metrics: [{ type: "sum", field: "amount", alias: "total" }],
|
|
65
|
-
table: "payments",
|
|
66
|
-
filters: { credit_note_id: { not: null } },
|
|
67
|
-
},
|
|
68
|
-
{ entity_id: entityId },
|
|
69
|
-
);
|
|
70
|
-
},
|
|
71
|
-
enabled: !!entityId && !!sdk,
|
|
72
|
-
staleTime: 120_000,
|
|
73
|
-
},
|
|
74
|
-
// Credit notes total (subtracted from invoiced to get net revenue)
|
|
75
|
-
{
|
|
76
|
-
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "cn-total"],
|
|
77
|
-
queryFn: async () => {
|
|
78
|
-
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
79
|
-
return sdk.entityStats.queryEntityStats(
|
|
80
|
-
{
|
|
81
|
-
metrics: [{ type: "sum", field: "total_with_tax", alias: "total" }],
|
|
82
|
-
table: "credit_notes",
|
|
83
|
-
filters: { is_draft: false },
|
|
84
|
-
},
|
|
85
|
-
{ entity_id: entityId },
|
|
86
|
-
);
|
|
87
|
-
},
|
|
88
|
-
enabled: !!entityId && !!sdk,
|
|
89
|
-
staleTime: 120_000,
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const [invoicedQuery, invoicePaymentsQuery, cnPaymentsQuery, cnQuery] = queries;
|
|
46
|
+
const { data: results, isLoading } = useStatsBatchQuery(entityId, "collection-rate", queries, {
|
|
47
|
+
select: (batch) => {
|
|
48
|
+
const totalInvoiced = Number(batch[0].data?.[0]?.total) || 0;
|
|
49
|
+
const invoicePayments = Number(batch[1].data?.[0]?.total) || 0;
|
|
50
|
+
const cnPayments = Number(batch[2].data?.[0]?.total) || 0;
|
|
51
|
+
const cnTotal = Number(batch[3].data?.[0]?.total) || 0;
|
|
52
|
+
const netInvoiced = totalInvoiced - cnTotal;
|
|
53
|
+
const netCollected = invoicePayments - cnPayments;
|
|
54
|
+
const collectionRate = netInvoiced > 0 ? (netCollected / netInvoiced) * 100 : 0;
|
|
95
55
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
56
|
+
return {
|
|
57
|
+
collectionRate,
|
|
58
|
+
totalCollected: netCollected,
|
|
59
|
+
totalInvoiced: netInvoiced,
|
|
60
|
+
currency: "EUR", // TODO: Get from entity settings
|
|
61
|
+
} as CollectionRateData;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
103
64
|
|
|
104
65
|
return {
|
|
105
|
-
data: {
|
|
106
|
-
|
|
107
|
-
totalCollected: netCollected,
|
|
108
|
-
totalInvoiced: netInvoiced,
|
|
109
|
-
currency: "EUR", // TODO: Get from entity settings
|
|
110
|
-
} as CollectionRateData,
|
|
111
|
-
isLoading: queries.some((q) => q.isLoading),
|
|
66
|
+
data: results ?? { collectionRate: 0, totalCollected: 0, totalInvoiced: 0, currency: "EUR" },
|
|
67
|
+
isLoading,
|
|
112
68
|
};
|
|
113
69
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Invoice status hook using the entity stats API.
|
|
3
3
|
* Server-side counting by invoice status.
|
|
4
|
+
* Sends 3 queries in a single batch request.
|
|
4
5
|
*/
|
|
5
|
-
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";
|
|
6
|
+
import type { StatsQueryDataItem, StatsQueryRequest } from "@spaceinvoices/js-sdk";
|
|
7
|
+
import { useStatsBatchQuery } from "../shared/use-stats-query";
|
|
9
8
|
|
|
10
9
|
export const INVOICE_STATUS_CACHE_KEY = "dashboard-invoice-status";
|
|
11
10
|
|
|
@@ -17,89 +16,56 @@ export type InvoiceStatusData = {
|
|
|
17
16
|
};
|
|
18
17
|
|
|
19
18
|
export function useInvoiceStatusData(entityId: string | undefined) {
|
|
20
|
-
const
|
|
19
|
+
const queries: StatsQueryRequest[] = [
|
|
20
|
+
// [0] Paid invoices
|
|
21
|
+
{
|
|
22
|
+
metrics: [{ type: "count", alias: "count" }],
|
|
23
|
+
table: "invoices",
|
|
24
|
+
filters: { is_draft: false, voided_at: null, paid_in_full: true },
|
|
25
|
+
},
|
|
26
|
+
// [1] Unpaid invoices grouped by overdue bucket (current = pending, others = overdue)
|
|
27
|
+
{
|
|
28
|
+
metrics: [{ type: "count", alias: "count" }],
|
|
29
|
+
table: "invoices",
|
|
30
|
+
filters: { is_draft: false, voided_at: null, paid_in_full: false },
|
|
31
|
+
group_by: ["overdue_bucket"],
|
|
32
|
+
},
|
|
33
|
+
// [2] Total count to derive voided
|
|
34
|
+
{
|
|
35
|
+
metrics: [{ type: "count", alias: "count" }],
|
|
36
|
+
table: "invoices",
|
|
37
|
+
filters: { is_draft: false },
|
|
38
|
+
},
|
|
39
|
+
];
|
|
21
40
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-status-paid"],
|
|
27
|
-
queryFn: async () => {
|
|
28
|
-
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
29
|
-
return sdk.entityStats.queryEntityStats(
|
|
30
|
-
{
|
|
31
|
-
metrics: [{ type: "count", alias: "count" }],
|
|
32
|
-
table: "invoices",
|
|
33
|
-
filters: { is_draft: false, voided_at: null, paid_in_full: true },
|
|
34
|
-
},
|
|
35
|
-
{ entity_id: entityId },
|
|
36
|
-
);
|
|
37
|
-
},
|
|
38
|
-
enabled: !!entityId && !!sdk,
|
|
39
|
-
staleTime: 120_000,
|
|
40
|
-
},
|
|
41
|
-
// Unpaid invoices grouped by overdue bucket (current = pending, others = overdue)
|
|
42
|
-
{
|
|
43
|
-
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-status-unpaid"],
|
|
44
|
-
queryFn: async () => {
|
|
45
|
-
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
46
|
-
return sdk.entityStats.queryEntityStats(
|
|
47
|
-
{
|
|
48
|
-
metrics: [{ type: "count", alias: "count" }],
|
|
49
|
-
table: "invoices",
|
|
50
|
-
filters: { is_draft: false, voided_at: null, paid_in_full: false },
|
|
51
|
-
group_by: ["overdue_bucket"],
|
|
52
|
-
},
|
|
53
|
-
{ entity_id: entityId },
|
|
54
|
-
);
|
|
55
|
-
},
|
|
56
|
-
enabled: !!entityId && !!sdk,
|
|
57
|
-
staleTime: 120_000,
|
|
58
|
-
},
|
|
59
|
-
// Total count to derive voided
|
|
60
|
-
{
|
|
61
|
-
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "invoice-status-total"],
|
|
62
|
-
queryFn: async () => {
|
|
63
|
-
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
64
|
-
return sdk.entityStats.queryEntityStats(
|
|
65
|
-
{
|
|
66
|
-
metrics: [{ type: "count", alias: "count" }],
|
|
67
|
-
table: "invoices",
|
|
68
|
-
filters: { is_draft: false },
|
|
69
|
-
},
|
|
70
|
-
{ entity_id: entityId },
|
|
71
|
-
);
|
|
72
|
-
},
|
|
73
|
-
enabled: !!entityId && !!sdk,
|
|
74
|
-
staleTime: 120_000,
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const [paidQuery, unpaidQuery, totalQuery] = queries;
|
|
41
|
+
const { data: results, isLoading } = useStatsBatchQuery(entityId, "invoice-status", queries, {
|
|
42
|
+
select: (batch) => {
|
|
43
|
+
const paid = Number(batch[0].data?.[0]?.count) || 0;
|
|
80
44
|
|
|
81
|
-
|
|
45
|
+
// Parse unpaid buckets
|
|
46
|
+
const unpaidData = batch[1].data || [];
|
|
47
|
+
let pending = 0;
|
|
48
|
+
let overdue = 0;
|
|
49
|
+
for (const row of unpaidData as StatsQueryDataItem[]) {
|
|
50
|
+
const bucket = String(row.overdue_bucket);
|
|
51
|
+
const count = Number(row.count) || 0;
|
|
52
|
+
if (bucket === "current") {
|
|
53
|
+
pending = count;
|
|
54
|
+
} else {
|
|
55
|
+
overdue += count;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
82
58
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let overdue = 0;
|
|
87
|
-
for (const row of unpaidData as StatsQueryDataItem[]) {
|
|
88
|
-
const bucket = String(row.overdue_bucket);
|
|
89
|
-
const count = Number(row.count) || 0;
|
|
90
|
-
if (bucket === "current") {
|
|
91
|
-
pending = count;
|
|
92
|
-
} else {
|
|
93
|
-
overdue += count;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
59
|
+
// Calculate voided as: total - paid - pending - overdue
|
|
60
|
+
const total = Number(batch[2].data?.[0]?.count) || 0;
|
|
61
|
+
const voided = Math.max(0, total - paid - pending - overdue);
|
|
96
62
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
63
|
+
return { paid, pending, overdue, voided } as InvoiceStatusData;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
100
66
|
|
|
101
67
|
return {
|
|
102
|
-
data: { paid, pending, overdue, voided }
|
|
103
|
-
isLoading
|
|
68
|
+
data: results ?? { paid: 0, pending: 0, overdue: 0, voided: 0 },
|
|
69
|
+
isLoading,
|
|
104
70
|
};
|
|
105
71
|
}
|