@spaceinvoices/react-ui 0.1.1
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/LICENSE +21 -0
- package/README.md +340 -0
- package/cli/dist/index.js +922 -0
- package/package.json +87 -0
- package/registry.json +600 -0
- package/spaceinvoices.schema.json +47 -0
- package/src/app.tsx +25 -0
- package/src/common/autocomplete.tsx +135 -0
- package/src/components/activities/activity-timeline.tsx +160 -0
- package/src/components/activities/index.ts +1 -0
- package/src/components/activities/locales/de.ts +30 -0
- package/src/components/activities/locales/sl.ts +30 -0
- package/src/components/advance-invoices/advance-invoices.hooks.ts +75 -0
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +702 -0
- package/src/components/advance-invoices/create/locales/de.ts +29 -0
- package/src/components/advance-invoices/create/locales/sl.ts +25 -0
- package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +74 -0
- package/src/components/advance-invoices/index.ts +5 -0
- package/src/components/advance-invoices/list/index.ts +3 -0
- package/src/components/advance-invoices/list/list-row-actions.tsx +119 -0
- package/src/components/advance-invoices/list/list-table.tsx +178 -0
- package/src/components/advance-invoices/list/locales/de.ts +32 -0
- package/src/components/advance-invoices/list/locales/sl.ts +32 -0
- package/src/components/advance-invoices/list/use-advance-invoice-download.ts +63 -0
- package/src/components/button-loader.tsx +11 -0
- package/src/components/combobox.tsx +96 -0
- package/src/components/company-registry/company-registry-autocomplete.tsx +151 -0
- package/src/components/company-registry/company-registry.hooks.ts +67 -0
- package/src/components/company-registry/index.ts +7 -0
- package/src/components/credit-notes/create/create-credit-note-form.tsx +332 -0
- package/src/components/credit-notes/create/index.ts +1 -0
- package/src/components/credit-notes/create/locales/de.ts +69 -0
- package/src/components/credit-notes/create/locales/sl.ts +67 -0
- package/src/components/credit-notes/credit-notes.hooks.ts +22 -0
- package/src/components/credit-notes/index.ts +10 -0
- package/src/components/credit-notes/list/index.ts +3 -0
- package/src/components/credit-notes/list/list-row-actions.tsx +116 -0
- package/src/components/credit-notes/list/list-table.tsx +183 -0
- package/src/components/credit-notes/list/locales/de.ts +33 -0
- package/src/components/credit-notes/list/locales/sl.ts +33 -0
- package/src/components/credit-notes/list/use-credit-note-download.ts +65 -0
- package/src/components/customers/create-customer-form/create-customer-form.tsx +134 -0
- package/src/components/customers/create-customer-form/locales/de.ts +20 -0
- package/src/components/customers/create-customer-form/locales/sl.ts +20 -0
- package/src/components/customers/customer-autocomplete.tsx +173 -0
- package/src/components/customers/customer-combobox.tsx +130 -0
- package/src/components/customers/customer-list-table/customer-list-row-actions.tsx +48 -0
- package/src/components/customers/customer-list-table/customer-list-table.tsx +124 -0
- package/src/components/customers/customer-list-table/index.ts +2 -0
- package/src/components/customers/customer-list-table/locales/de.ts +16 -0
- package/src/components/customers/customer-list-table/locales/sl.ts +16 -0
- package/src/components/customers/customers.hooks.test.ts +348 -0
- package/src/components/customers/customers.hooks.ts +57 -0
- package/src/components/customers/index.ts +5 -0
- package/src/components/dashboard/chart-empty-state.tsx +29 -0
- package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +80 -0
- package/src/components/dashboard/collection-rate-card/index.ts +4 -0
- package/src/components/dashboard/collection-rate-card/locales/sl.ts +3 -0
- package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +74 -0
- package/src/components/dashboard/index.ts +54 -0
- package/src/components/dashboard/invoice-status-chart/index.ts +4 -0
- package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +130 -0
- package/src/components/dashboard/invoice-status-chart/locales/sl.ts +9 -0
- package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +105 -0
- package/src/components/dashboard/loading-card.tsx +19 -0
- package/src/components/dashboard/payment-methods-chart/index.ts +4 -0
- package/src/components/dashboard/payment-methods-chart/locales/sl.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +152 -0
- package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +50 -0
- package/src/components/dashboard/payment-trend-chart/index.ts +4 -0
- package/src/components/dashboard/payment-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +137 -0
- package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +92 -0
- package/src/components/dashboard/revenue-card.tsx +49 -0
- package/src/components/dashboard/revenue-trend-chart/index.ts +4 -0
- package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +137 -0
- package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +93 -0
- package/src/components/dashboard/shared/index.ts +5 -0
- package/src/components/dashboard/shared/use-revenue-data.ts +160 -0
- package/src/components/dashboard/shared/use-stats-counts.ts +89 -0
- package/src/components/dashboard/shared/use-stats-query.ts +38 -0
- package/src/components/dashboard/stat-card.tsx +41 -0
- package/src/components/dashboard/tax-collected-card/index.ts +2 -0
- package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +77 -0
- package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +145 -0
- package/src/components/dashboard/top-customers-chart/index.ts +4 -0
- package/src/components/dashboard/top-customers-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +130 -0
- package/src/components/dashboard/top-customers-chart/use-top-customers.ts +72 -0
- package/src/components/documents/create/document-add-item-form.tsx +379 -0
- package/src/components/documents/create/document-add-item-tax-rate-field.tsx +120 -0
- package/src/components/documents/create/document-details-section.tsx +597 -0
- package/src/components/documents/create/document-items-section.tsx +133 -0
- package/src/components/documents/create/document-recipient-section.tsx +101 -0
- package/src/components/documents/create/form-types.ts +36 -0
- package/src/components/documents/create/index.ts +9 -0
- package/src/components/documents/create/live-preview.tsx +235 -0
- package/src/components/documents/create/mark-as-paid-section.tsx +82 -0
- package/src/components/documents/create/prepare-document-submission.test.ts +132 -0
- package/src/components/documents/create/prepare-document-submission.ts +187 -0
- package/src/components/documents/create/prepare-preview-data.test.ts +155 -0
- package/src/components/documents/create/prepare-preview-data.ts +16 -0
- package/src/components/documents/create/smart-code-insert-button.tsx +139 -0
- package/src/components/documents/create/use-document-customer-form.ts +161 -0
- package/src/components/documents/document-preview.tsx +13 -0
- package/src/components/documents/documents.hooks.ts +146 -0
- package/src/components/documents/index.ts +23 -0
- package/src/components/documents/shared/document-preview-display.tsx +172 -0
- package/src/components/documents/shared/index.ts +3 -0
- package/src/components/documents/shared/scaled-document-preview.tsx +70 -0
- package/src/components/documents/shared/use-a4-scaling.ts +62 -0
- package/src/components/documents/types.ts +61 -0
- package/src/components/documents/view/document-actions-bar.tsx +328 -0
- package/src/components/documents/view/document-details-card.tsx +179 -0
- package/src/components/documents/view/document-payments-list.tsx +256 -0
- package/src/components/documents/view/index.ts +4 -0
- package/src/components/documents/view/locales/de.ts +85 -0
- package/src/components/documents/view/locales/sl.ts +84 -0
- package/src/components/documents/view/use-document-download.ts +125 -0
- package/src/components/entities/create-entity-form.tsx +105 -0
- package/src/components/entities/entities.hooks.ts +50 -0
- package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +103 -0
- package/src/components/entities/entity-settings-form/entity-settings-form.tsx +1326 -0
- package/src/components/entities/entity-settings-form/image-upload-with-crop.tsx +222 -0
- package/src/components/entities/entity-settings-form/index.ts +2 -0
- package/src/components/entities/entity-settings-form/input-with-preview.tsx +190 -0
- package/src/components/entities/entity-settings-form/locales/de.ts +192 -0
- package/src/components/entities/entity-settings-form/locales/sl.ts +188 -0
- package/src/components/entities/furs-settings-form/furs-settings-form.tsx +410 -0
- package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +320 -0
- package/src/components/entities/furs-settings-form/index.ts +3 -0
- package/src/components/entities/furs-settings-form/locales/de.ts +233 -0
- package/src/components/entities/furs-settings-form/locales/en.ts +194 -0
- package/src/components/entities/furs-settings-form/locales/sl.ts +196 -0
- package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +242 -0
- package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +139 -0
- package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +252 -0
- package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +370 -0
- package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +420 -0
- package/src/components/entities/keys.ts +2 -0
- package/src/components/entities/settings/branding-settings-form.tsx +274 -0
- package/src/components/entities/settings/company-settings-form.tsx +256 -0
- package/src/components/entities/settings/defaults-settings-form.tsx +501 -0
- package/src/components/entities/settings/email-settings-form.tsx +288 -0
- package/src/components/entities/settings/eslog-settings-form.tsx +113 -0
- package/src/components/entities/settings/index.ts +8 -0
- package/src/components/entities/settings/number-format-settings-form.tsx +244 -0
- package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +164 -0
- package/src/components/entities/settings/pdf-template-selector/index.ts +2 -0
- package/src/components/entities/settings/pdf-template-selector/locales/de.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/locales/sl.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/pdf-template-cards.tsx +49 -0
- package/src/components/entities/settings/settings-footer.tsx +16 -0
- package/src/components/entities/settings/tax-rules-settings-form.tsx +346 -0
- package/src/components/estimates/create/create-estimate-form.tsx +384 -0
- package/src/components/estimates/create/locales/de.ts +64 -0
- package/src/components/estimates/create/locales/sl.ts +63 -0
- package/src/components/estimates/create/prepare-estimate-submission.ts +39 -0
- package/src/components/estimates/create/use-estimate-customer-form.ts +5 -0
- package/src/components/estimates/estimates.hooks.ts +15 -0
- package/src/components/estimates/index.ts +6 -0
- package/src/components/estimates/list/index.ts +3 -0
- package/src/components/estimates/list/list-row-actions.tsx +103 -0
- package/src/components/estimates/list/list-table.tsx +171 -0
- package/src/components/estimates/list/locales/de.ts +26 -0
- package/src/components/estimates/list/locales/sl.ts +26 -0
- package/src/components/estimates/list/use-estimate-download.ts +63 -0
- package/src/components/export/document-export-form.tsx +288 -0
- package/src/components/export/index.ts +2 -0
- package/src/components/form/form-input.tsx +89 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/invoices/create/create-invoice-form.tsx +852 -0
- package/src/components/invoices/create/eslog-validation.test.ts +242 -0
- package/src/components/invoices/create/eslog-validation.ts +208 -0
- package/src/components/invoices/create/locales/de.ts +118 -0
- package/src/components/invoices/create/locales/sl.ts +114 -0
- package/src/components/invoices/create/prepare-invoice-submission.test.ts +777 -0
- package/src/components/invoices/create/prepare-invoice-submission.ts +79 -0
- package/src/components/invoices/create/use-invoice-customer-form.ts +5 -0
- package/src/components/invoices/index.ts +9 -0
- package/src/components/invoices/invoices-furs.hooks.ts +28 -0
- package/src/components/invoices/invoices.hooks.ts +110 -0
- package/src/components/invoices/list/index.ts +3 -0
- package/src/components/invoices/list/list-row-actions.tsx +132 -0
- package/src/components/invoices/list/list-table.tsx +165 -0
- package/src/components/invoices/list/locales/de.ts +33 -0
- package/src/components/invoices/list/locales/sl.ts +33 -0
- package/src/components/invoices/list/use-invoice-download.ts +62 -0
- package/src/components/invoices/send-email-dialog/index.ts +1 -0
- package/src/components/invoices/send-email-dialog/locales/de.ts +18 -0
- package/src/components/invoices/send-email-dialog/locales/sl.ts +17 -0
- package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +289 -0
- package/src/components/invoices/send-email-dialog.tsx +2 -0
- package/src/components/invoices/shared/index.ts +2 -0
- package/src/components/invoices/shared/scaled-document-preview.tsx +32 -0
- package/src/components/invoices/shared/use-a4-scaling.tsx +39 -0
- package/src/components/invoices/view/eslog-info-display.tsx +160 -0
- package/src/components/invoices/view/furs-info-display.tsx +213 -0
- package/src/components/items/create-item-form/create-item-form.tsx +155 -0
- package/src/components/items/create-item-form/locales/de.ts +14 -0
- package/src/components/items/create-item-form/locales/en.ts +9 -0
- package/src/components/items/create-item-form/locales/sl.ts +14 -0
- package/src/components/items/item-combobox.tsx +147 -0
- package/src/components/items/item-list-table/item-list-header.tsx +33 -0
- package/src/components/items/item-list-table/item-list-row-actions.tsx +48 -0
- package/src/components/items/item-list-table/item-list-row.tsx +32 -0
- package/src/components/items/item-list-table/item-list-table.tsx +76 -0
- package/src/components/items/item-list-table/locales/de.ts +10 -0
- package/src/components/items/item-list-table/locales/en.ts +10 -0
- package/src/components/items/item-list-table/locales/sl.ts +10 -0
- package/src/components/items/items.hooks.ts +63 -0
- package/src/components/loading-spinner.tsx +24 -0
- package/src/components/payments/create-payment-form/create-payment-form.tsx +222 -0
- package/src/components/payments/create-payment-form/locales/de.ts +20 -0
- package/src/components/payments/create-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/edit-payment-form/edit-payment-form.tsx +230 -0
- package/src/components/payments/edit-payment-form/index.ts +1 -0
- package/src/components/payments/edit-payment-form/locales/de.ts +20 -0
- package/src/components/payments/edit-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/index.ts +4 -0
- package/src/components/payments/list/index.ts +2 -0
- package/src/components/payments/list/list-row-actions.tsx +98 -0
- package/src/components/payments/list/list-table.tsx +186 -0
- package/src/components/payments/list/locales/de.ts +19 -0
- package/src/components/payments/list/locales/sl.ts +19 -0
- package/src/components/payments/payments.hooks.ts +15 -0
- package/src/components/request-logs/index.ts +3 -0
- package/src/components/request-logs/request-log-detail.tsx +242 -0
- package/src/components/request-logs/request-log-list-table.tsx +266 -0
- package/src/components/request-logs/request-logs-page.tsx +10 -0
- package/src/components/table/README.md +410 -0
- package/src/components/table/data-table.tsx +251 -0
- package/src/components/table/date-cell.tsx +35 -0
- package/src/components/table/filter-bar.tsx +114 -0
- package/src/components/table/filter-panel.tsx +407 -0
- package/src/components/table/hooks/use-table-fetch.ts +17 -0
- package/src/components/table/hooks/use-table-query.ts +36 -0
- package/src/components/table/hooks/use-table-state.ts +293 -0
- package/src/components/table/index.ts +35 -0
- package/src/components/table/search-input.tsx +85 -0
- package/src/components/table/sortable-header.tsx +56 -0
- package/src/components/table/table-empty-state.tsx +40 -0
- package/src/components/table/table-no-results.tsx +41 -0
- package/src/components/table/table-pagination.tsx +42 -0
- package/src/components/table/table-skeleton.tsx +54 -0
- package/src/components/table/types.ts +136 -0
- package/src/components/tax-reports/index.ts +1 -0
- package/src/components/tax-reports/kir-export-form.tsx +172 -0
- package/src/components/taxes/create-tax-form/create-tax-form.tsx +112 -0
- package/src/components/taxes/create-tax-form/locales/de.ts +8 -0
- package/src/components/taxes/create-tax-form/locales/en.ts +7 -0
- package/src/components/taxes/create-tax-form/locales/sl.ts +8 -0
- package/src/components/taxes/tax-list-table/locales/de.ts +11 -0
- package/src/components/taxes/tax-list-table/locales/en.ts +10 -0
- package/src/components/taxes/tax-list-table/locales/sl.ts +11 -0
- package/src/components/taxes/tax-list-table/tax-list-header.tsx +29 -0
- package/src/components/taxes/tax-list-table/tax-list-row-actions.tsx +43 -0
- package/src/components/taxes/tax-list-table/tax-list-row.tsx +46 -0
- package/src/components/taxes/tax-list-table/tax-list-table.tsx +59 -0
- package/src/components/taxes/taxes.hooks.ts +35 -0
- package/src/components/ui/alert-dialog.tsx +61 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +132 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +213 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/chart.tsx +380 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +56 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/dialog.tsx +187 -0
- package/src/components/ui/drawer.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/form.tsx +166 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/loading-spinner.tsx +16 -0
- package/src/components/ui/popover.tsx +108 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/select.tsx +200 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +145 -0
- package/src/components/ui/sidebar.tsx +771 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +60 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/sticky-form-footer.tsx +55 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +101 -0
- package/src/components/ui/tabs.tsx +80 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +89 -0
- package/src/components/wl-subscription/index.ts +2 -0
- package/src/components/wl-subscription/locked-feature.tsx +173 -0
- package/src/components/wl-subscription/upgrade-modal.tsx +209 -0
- package/src/frontend.tsx +28 -0
- package/src/generate-schemas.ts +265 -0
- package/src/generated/schemas/advanceinvoice.ts +177 -0
- package/src/generated/schemas/creditnote.ts +187 -0
- package/src/generated/schemas/customer.ts +29 -0
- package/src/generated/schemas/entity.ts +252 -0
- package/src/generated/schemas/estimate.ts +159 -0
- package/src/generated/schemas/furssettings.ts +25 -0
- package/src/generated/schemas/index.ts +24 -0
- package/src/generated/schemas/invoice.ts +167 -0
- package/src/generated/schemas/item.ts +38 -0
- package/src/generated/schemas/payment.ts +44 -0
- package/src/generated/schemas/previewadvanceinvoice_body.ts +354 -0
- package/src/generated/schemas/previewestimate_body.ts +309 -0
- package/src/generated/schemas/registerfursmovablepremise_body.ts +22 -0
- package/src/generated/schemas/registerfursrealestatepremise_body.ts +32 -0
- package/src/generated/schemas/renderdocument_body.ts +594 -0
- package/src/generated/schemas/sendemail_body.ts +26 -0
- package/src/generated/schemas/startpdfexport_body.ts +20 -0
- package/src/generated/schemas/tax.ts +48 -0
- package/src/generated/schemas/uploadfile_body.ts +23 -0
- package/src/generated/schemas/uploadfurscertificate_body.ts +20 -0
- package/src/generated/schemas/userfurssettings.ts +19 -0
- package/src/hooks/create-resource-hooks.test.ts +483 -0
- package/src/hooks/create-resource-hooks.ts +300 -0
- package/src/hooks/use-debounce.ts +12 -0
- package/src/hooks/use-duplicate-document.ts +185 -0
- package/src/hooks/use-media-query.tsx +19 -0
- package/src/hooks/use-mobile.ts +39 -0
- package/src/hooks/use-next-document-number.ts +57 -0
- package/src/hooks/use-resource-mutation.ts +118 -0
- package/src/hooks/use-vies-check.ts +130 -0
- package/src/index.css +11 -0
- package/src/index.html +13 -0
- package/src/index.tsx +12 -0
- package/src/lib/auth.ts +4 -0
- package/src/lib/browser-cookies.ts +70 -0
- package/src/lib/constants.ts +287 -0
- package/src/lib/cookies.ts +36 -0
- package/src/lib/schemas/advance-invoice.ts +43 -0
- package/src/lib/schemas/credit-note.ts +32 -0
- package/src/lib/schemas/estimate.ts +31 -0
- package/src/lib/schemas/index.ts +18 -0
- package/src/lib/schemas/invoice.ts +43 -0
- package/src/lib/schemas/shared.ts +79 -0
- package/src/lib/translation.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/entities-context.tsx +41 -0
- package/src/providers/entities-provider.tsx +201 -0
- package/src/providers/form-footer-context.tsx +72 -0
- package/src/providers/sdk-provider.tsx +164 -0
- package/src/providers/white-label-provider.tsx +91 -0
- package/src/providers/wl-subscription-provider.tsx +277 -0
- package/src/utils/string-helpers.ts +111 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment trend hook using the entity stats API.
|
|
3
|
+
* Server-side aggregation by month for accurate trend data.
|
|
4
|
+
*/
|
|
5
|
+
import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
|
|
6
|
+
import { useQuery } from "@tanstack/react-query";
|
|
7
|
+
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
8
|
+
import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
|
|
9
|
+
|
|
10
|
+
export const PAYMENT_TREND_CACHE_KEY = "dashboard-payment-trend";
|
|
11
|
+
|
|
12
|
+
function getLastMonths(count: number): { months: string[]; startDate: string; endDate: string } {
|
|
13
|
+
const months: string[] = [];
|
|
14
|
+
const now = new Date();
|
|
15
|
+
|
|
16
|
+
// Start of the month 'count-1' months ago
|
|
17
|
+
const startDate = new Date(now.getFullYear(), now.getMonth() - (count - 1), 1);
|
|
18
|
+
// End of current month
|
|
19
|
+
const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
20
|
+
|
|
21
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
22
|
+
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
23
|
+
months.push(d.toISOString().substring(0, 7));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
months,
|
|
28
|
+
startDate: startDate.toISOString().substring(0, 10),
|
|
29
|
+
endDate: endDate.toISOString().substring(0, 10),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type PaymentTrendData = { month: string; amount: number }[];
|
|
34
|
+
|
|
35
|
+
export function usePaymentTrendData(entityId: string | undefined) {
|
|
36
|
+
const { sdk } = useSDK();
|
|
37
|
+
|
|
38
|
+
const { months, startDate, endDate } = getLastMonths(6);
|
|
39
|
+
|
|
40
|
+
const query = useQuery({
|
|
41
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "payment-trend", startDate, endDate],
|
|
42
|
+
queryFn: async () => {
|
|
43
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
44
|
+
return sdk.entityStats.queryEntityStats(
|
|
45
|
+
{
|
|
46
|
+
metrics: [{ type: "sum", field: "amount_converted", alias: "amount" }],
|
|
47
|
+
table: "payments",
|
|
48
|
+
date_from: startDate,
|
|
49
|
+
date_to: endDate,
|
|
50
|
+
group_by: ["month", "currency_code"], // Include currency for display
|
|
51
|
+
order_by: [{ field: "month", direction: "asc" }],
|
|
52
|
+
},
|
|
53
|
+
{ entity_id: entityId },
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
enabled: !!entityId && !!sdk,
|
|
57
|
+
staleTime: 30_000,
|
|
58
|
+
select: (response) => {
|
|
59
|
+
// Build a map of all months with 0 amount
|
|
60
|
+
const monthMap: Record<string, number> = {};
|
|
61
|
+
for (const month of months) {
|
|
62
|
+
monthMap[month] = 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fill in the actual amounts from the API response
|
|
66
|
+
// Sum up amounts per month (in case of multiple rows due to currency_code grouping)
|
|
67
|
+
const data = response.data || [];
|
|
68
|
+
let currency = "EUR";
|
|
69
|
+
for (const row of data as StatsQueryDataItem[]) {
|
|
70
|
+
const month = String(row.month);
|
|
71
|
+
if (month in monthMap) {
|
|
72
|
+
monthMap[month] += Number(row.amount) || 0;
|
|
73
|
+
}
|
|
74
|
+
// Get currency from first row with data
|
|
75
|
+
if (row.currency_code && currency === "EUR") {
|
|
76
|
+
currency = String(row.currency_code);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
data: months.map((month) => ({ month, amount: monthMap[month] })),
|
|
82
|
+
currency, // Currency from payment data
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
data: query.data?.data || [],
|
|
89
|
+
currency: query.data?.currency || "EUR",
|
|
90
|
+
isLoading: query.isLoading,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle, TrendingDown, TrendingUp } from "lucide-react";
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/ui/components/ui/card";
|
|
5
|
+
|
|
6
|
+
export type RevenueCardProps = {
|
|
7
|
+
title: string;
|
|
8
|
+
value: number;
|
|
9
|
+
currency: string;
|
|
10
|
+
variant?: "default" | "success" | "warning" | "danger";
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
locale?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const variantStyles = {
|
|
16
|
+
default: "",
|
|
17
|
+
success: "text-green-600 dark:text-green-400",
|
|
18
|
+
warning: "text-yellow-600 dark:text-yellow-400",
|
|
19
|
+
danger: "text-red-600 dark:text-red-400",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const variantIcons = {
|
|
23
|
+
default: null,
|
|
24
|
+
success: TrendingUp,
|
|
25
|
+
warning: AlertTriangle,
|
|
26
|
+
danger: TrendingDown,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function RevenueCard({ title, value, currency, variant = "default", subtitle, locale }: RevenueCardProps) {
|
|
30
|
+
const Icon = variantIcons[variant];
|
|
31
|
+
const formattedValue = new Intl.NumberFormat(locale, {
|
|
32
|
+
style: "currency",
|
|
33
|
+
currency,
|
|
34
|
+
minimumFractionDigits: 2,
|
|
35
|
+
}).format(value);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Card>
|
|
39
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
40
|
+
<CardTitle className="font-medium text-muted-foreground text-sm">{title}</CardTitle>
|
|
41
|
+
{Icon && <Icon className={`h-4 w-4 ${variantStyles[variant]}`} />}
|
|
42
|
+
</CardHeader>
|
|
43
|
+
<CardContent>
|
|
44
|
+
<div className={`font-bold text-2xl ${variantStyles[variant]}`}>{formattedValue}</div>
|
|
45
|
+
{subtitle && <p className="mt-1 text-muted-foreground text-xs">{subtitle}</p>}
|
|
46
|
+
</CardContent>
|
|
47
|
+
</Card>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { RevenueTrendChartData, RevenueTrendChartProps } from "./revenue-trend-chart";
|
|
2
|
+
export { RevenueTrendChart } from "./revenue-trend-chart";
|
|
3
|
+
export type { RevenueTrendData } from "./use-revenue-trend";
|
|
4
|
+
export { REVENUE_TREND_CACHE_KEY, useRevenueTrendData } from "./use-revenue-trend";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/ui/components/ui/card";
|
|
5
|
+
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/ui/components/ui/chart";
|
|
6
|
+
import { createTranslation } from "@/ui/lib/translation";
|
|
7
|
+
import { ChartEmptyState } from "../chart-empty-state";
|
|
8
|
+
import { LoadingCard } from "../loading-card";
|
|
9
|
+
import sl from "./locales/sl";
|
|
10
|
+
import { useRevenueTrendData } from "./use-revenue-trend";
|
|
11
|
+
|
|
12
|
+
const translations = { sl } as const;
|
|
13
|
+
|
|
14
|
+
export type RevenueTrendChartData = { month: string; revenue: number }[];
|
|
15
|
+
|
|
16
|
+
type BaseProps = {
|
|
17
|
+
locale?: string;
|
|
18
|
+
t?: (key: string) => string;
|
|
19
|
+
namespace?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type DataProps = BaseProps & {
|
|
23
|
+
data: RevenueTrendChartData;
|
|
24
|
+
currency: string;
|
|
25
|
+
entityId?: never;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type TurnkeyProps = BaseProps & {
|
|
29
|
+
entityId: string;
|
|
30
|
+
data?: never;
|
|
31
|
+
currency?: never;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RevenueTrendChartProps = DataProps | TurnkeyProps;
|
|
35
|
+
|
|
36
|
+
const chartConfig = {
|
|
37
|
+
revenue: {
|
|
38
|
+
label: "Revenue",
|
|
39
|
+
color: "var(--chart-1)",
|
|
40
|
+
},
|
|
41
|
+
} satisfies ChartConfig;
|
|
42
|
+
|
|
43
|
+
function formatMonth(month: string, locale?: string): string {
|
|
44
|
+
const date = new Date(`${month}-01`);
|
|
45
|
+
return date.toLocaleDateString(locale, { month: "short" });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatCurrency(value: number, currency: string, locale?: string): string {
|
|
49
|
+
return new Intl.NumberFormat(locale, {
|
|
50
|
+
style: "currency",
|
|
51
|
+
currency,
|
|
52
|
+
minimumFractionDigits: 0,
|
|
53
|
+
maximumFractionDigits: 0,
|
|
54
|
+
}).format(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function RevenueTrendChart(props: RevenueTrendChartProps) {
|
|
58
|
+
const { locale, t: externalT, namespace } = props;
|
|
59
|
+
const t = createTranslation({ t: externalT, namespace, locale, translations });
|
|
60
|
+
|
|
61
|
+
// Turnkey mode - fetch own data
|
|
62
|
+
const hookResult = useRevenueTrendData("entityId" in props ? props.entityId : undefined);
|
|
63
|
+
|
|
64
|
+
// Determine data source
|
|
65
|
+
const data = "entityId" in props ? hookResult.data : props.data;
|
|
66
|
+
const currency = "entityId" in props ? hookResult.currency : props.currency;
|
|
67
|
+
const isLoading = "entityId" in props ? hookResult.isLoading : false;
|
|
68
|
+
|
|
69
|
+
if (isLoading) {
|
|
70
|
+
return <LoadingCard className="h-[280px]" />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hasData = data.some((d) => d.revenue > 0);
|
|
74
|
+
|
|
75
|
+
// Placeholder data for empty state
|
|
76
|
+
const placeholderData =
|
|
77
|
+
data.length > 0
|
|
78
|
+
? data.map((d) => ({ ...d, revenue: 100 }))
|
|
79
|
+
: [
|
|
80
|
+
{ month: "2024-01", revenue: 80 },
|
|
81
|
+
{ month: "2024-02", revenue: 120 },
|
|
82
|
+
{ month: "2024-03", revenue: 90 },
|
|
83
|
+
{ month: "2024-04", revenue: 140 },
|
|
84
|
+
{ month: "2024-05", revenue: 100 },
|
|
85
|
+
{ month: "2024-06", revenue: 130 },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const chartContent = (
|
|
89
|
+
<ChartContainer config={chartConfig} className="h-[200px] w-full">
|
|
90
|
+
<AreaChart data={hasData ? data : placeholderData} margin={{ left: 12, right: 12 }}>
|
|
91
|
+
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
92
|
+
<XAxis
|
|
93
|
+
dataKey="month"
|
|
94
|
+
tickLine={false}
|
|
95
|
+
axisLine={false}
|
|
96
|
+
tickMargin={8}
|
|
97
|
+
tickFormatter={(m) => formatMonth(m, locale)}
|
|
98
|
+
/>
|
|
99
|
+
<YAxis
|
|
100
|
+
tickLine={false}
|
|
101
|
+
axisLine={false}
|
|
102
|
+
tickMargin={8}
|
|
103
|
+
tickFormatter={(value) => formatCurrency(value, currency, locale)}
|
|
104
|
+
width={80}
|
|
105
|
+
/>
|
|
106
|
+
<defs>
|
|
107
|
+
<linearGradient id="fillRevenue" x1="0" y1="0" x2="0" y2="1">
|
|
108
|
+
<stop offset="5%" stopColor="var(--chart-1)" stopOpacity={0.8} />
|
|
109
|
+
<stop offset="95%" stopColor="var(--chart-1)" stopOpacity={0.1} />
|
|
110
|
+
</linearGradient>
|
|
111
|
+
</defs>
|
|
112
|
+
<ChartTooltip
|
|
113
|
+
cursor={false}
|
|
114
|
+
content={
|
|
115
|
+
<ChartTooltipContent
|
|
116
|
+
labelFormatter={(label) => formatMonth(label, locale)}
|
|
117
|
+
formatter={(value) => formatCurrency(Number(value), currency, locale)}
|
|
118
|
+
/>
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
<Area dataKey="revenue" type="monotone" fill="url(#fillRevenue)" stroke="var(--chart-1)" strokeWidth={2} />
|
|
122
|
+
</AreaChart>
|
|
123
|
+
</ChartContainer>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Card>
|
|
128
|
+
<CardHeader>
|
|
129
|
+
<CardTitle>{t("Revenue Trend")}</CardTitle>
|
|
130
|
+
<CardDescription>{t("Monthly revenue over the last 6 months")}</CardDescription>
|
|
131
|
+
</CardHeader>
|
|
132
|
+
<CardContent>
|
|
133
|
+
{hasData ? chartContent : <ChartEmptyState label={t("No data available")}>{chartContent}</ChartEmptyState>}
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revenue trend hook using the entity stats API.
|
|
3
|
+
* Server-side aggregation by month for accurate trend data.
|
|
4
|
+
*/
|
|
5
|
+
import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
|
|
6
|
+
import { useQuery } from "@tanstack/react-query";
|
|
7
|
+
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
8
|
+
import { STATS_QUERY_CACHE_KEY } from "../shared/use-stats-query";
|
|
9
|
+
|
|
10
|
+
export const REVENUE_TREND_CACHE_KEY = "dashboard-revenue-trend";
|
|
11
|
+
|
|
12
|
+
function getLastMonths(count: number): { months: string[]; startDate: string; endDate: string } {
|
|
13
|
+
const months: string[] = [];
|
|
14
|
+
const now = new Date();
|
|
15
|
+
|
|
16
|
+
// Start of the month 'count-1' months ago
|
|
17
|
+
const startDate = new Date(now.getFullYear(), now.getMonth() - (count - 1), 1);
|
|
18
|
+
// End of current month
|
|
19
|
+
const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
20
|
+
|
|
21
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
22
|
+
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
23
|
+
months.push(d.toISOString().substring(0, 7));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
months,
|
|
28
|
+
startDate: startDate.toISOString().substring(0, 10),
|
|
29
|
+
endDate: endDate.toISOString().substring(0, 10),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RevenueTrendData = { month: string; revenue: number }[];
|
|
34
|
+
|
|
35
|
+
export function useRevenueTrendData(entityId: string | undefined) {
|
|
36
|
+
const { sdk } = useSDK();
|
|
37
|
+
|
|
38
|
+
const { months, startDate, endDate } = getLastMonths(6);
|
|
39
|
+
|
|
40
|
+
const query = useQuery({
|
|
41
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "revenue-trend", startDate, endDate],
|
|
42
|
+
queryFn: async () => {
|
|
43
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
44
|
+
return sdk.entityStats.queryEntityStats(
|
|
45
|
+
{
|
|
46
|
+
metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
|
|
47
|
+
table: "invoices",
|
|
48
|
+
date_from: startDate,
|
|
49
|
+
date_to: endDate,
|
|
50
|
+
filters: { is_draft: false, voided_at: null },
|
|
51
|
+
group_by: ["month", "quote_currency"], // Include currency for display
|
|
52
|
+
order_by: [{ field: "month", direction: "asc" }],
|
|
53
|
+
},
|
|
54
|
+
{ entity_id: entityId },
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
enabled: !!entityId && !!sdk,
|
|
58
|
+
staleTime: 30_000,
|
|
59
|
+
select: (response) => {
|
|
60
|
+
// Build a map of all months with 0 revenue
|
|
61
|
+
const monthMap: Record<string, number> = {};
|
|
62
|
+
for (const month of months) {
|
|
63
|
+
monthMap[month] = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fill in the actual revenue from the API response
|
|
67
|
+
// Sum up revenues per month (in case of multiple rows due to quote_currency grouping)
|
|
68
|
+
const data = response.data || [];
|
|
69
|
+
let currency = "EUR";
|
|
70
|
+
for (const row of data as StatsQueryDataItem[]) {
|
|
71
|
+
const month = String(row.month);
|
|
72
|
+
if (month in monthMap) {
|
|
73
|
+
monthMap[month] += Number(row.revenue) || 0;
|
|
74
|
+
}
|
|
75
|
+
// Get currency from first row with data
|
|
76
|
+
if (row.quote_currency && currency === "EUR") {
|
|
77
|
+
currency = String(row.quote_currency);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
data: months.map((month) => ({ month, revenue: monthMap[month] })),
|
|
83
|
+
currency, // Currency from document data
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
data: query.data?.data || [],
|
|
90
|
+
currency: query.data?.currency || "EUR",
|
|
91
|
+
isLoading: query.isLoading,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { RevenueData } from "./use-revenue-data";
|
|
2
|
+
export { REVENUE_DATA_CACHE_KEY, useRevenueData } from "./use-revenue-data";
|
|
3
|
+
export type { StatsCountsData } from "./use-stats-counts";
|
|
4
|
+
export { STATS_COUNTS_CACHE_KEY, useStatsCountsData } from "./use-stats-counts";
|
|
5
|
+
export { STATS_QUERY_CACHE_KEY, useStatsQuery } from "./use-stats-query";
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revenue data hook using the entity stats API.
|
|
3
|
+
* Server-side aggregation for accurate calculations.
|
|
4
|
+
*/
|
|
5
|
+
import type { StatsQueryDataItem } from "@spaceinvoices/js-sdk";
|
|
6
|
+
import { useQueries } from "@tanstack/react-query";
|
|
7
|
+
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
8
|
+
import { STATS_QUERY_CACHE_KEY } from "./use-stats-query";
|
|
9
|
+
|
|
10
|
+
export const REVENUE_DATA_CACHE_KEY = "dashboard-revenue-data";
|
|
11
|
+
|
|
12
|
+
function getMonthDateRange() {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
15
|
+
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
16
|
+
return {
|
|
17
|
+
from: firstDay.toISOString().split("T")[0],
|
|
18
|
+
to: lastDay.toISOString().split("T")[0],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getYearDateRange() {
|
|
23
|
+
const year = new Date().getFullYear();
|
|
24
|
+
return {
|
|
25
|
+
from: `${year}-01-01`,
|
|
26
|
+
to: `${year}-12-31`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RevenueData = {
|
|
31
|
+
thisMonth: number;
|
|
32
|
+
thisYear: number;
|
|
33
|
+
outstanding: number;
|
|
34
|
+
overdue: number;
|
|
35
|
+
overdueCount: number;
|
|
36
|
+
currency: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function useRevenueData(entityId: string | undefined) {
|
|
40
|
+
const { sdk } = useSDK();
|
|
41
|
+
const monthRange = getMonthDateRange();
|
|
42
|
+
const yearRange = getYearDateRange();
|
|
43
|
+
|
|
44
|
+
const queries = useQueries({
|
|
45
|
+
queries: [
|
|
46
|
+
// This month revenue (using converted amounts for multi-currency support)
|
|
47
|
+
{
|
|
48
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "revenue-this-month", monthRange.from],
|
|
49
|
+
queryFn: async () => {
|
|
50
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
51
|
+
return sdk.entityStats.queryEntityStats(
|
|
52
|
+
{
|
|
53
|
+
metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
|
|
54
|
+
table: "invoices",
|
|
55
|
+
date_from: monthRange.from,
|
|
56
|
+
date_to: monthRange.to,
|
|
57
|
+
filters: { is_draft: false, voided_at: null },
|
|
58
|
+
group_by: ["quote_currency"], // Get the currency for display
|
|
59
|
+
},
|
|
60
|
+
{ entity_id: entityId },
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
enabled: !!entityId && !!sdk,
|
|
64
|
+
staleTime: 30_000,
|
|
65
|
+
},
|
|
66
|
+
// This year revenue (using converted amounts for multi-currency support)
|
|
67
|
+
{
|
|
68
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "revenue-this-year", yearRange.from],
|
|
69
|
+
queryFn: async () => {
|
|
70
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
71
|
+
return sdk.entityStats.queryEntityStats(
|
|
72
|
+
{
|
|
73
|
+
metrics: [{ type: "sum", field: "total_with_tax_converted", alias: "revenue" }],
|
|
74
|
+
table: "invoices",
|
|
75
|
+
date_from: yearRange.from,
|
|
76
|
+
date_to: yearRange.to,
|
|
77
|
+
filters: { is_draft: false, voided_at: null },
|
|
78
|
+
},
|
|
79
|
+
{ entity_id: entityId },
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
enabled: !!entityId && !!sdk,
|
|
83
|
+
staleTime: 30_000,
|
|
84
|
+
},
|
|
85
|
+
// Outstanding (unpaid, not voided)
|
|
86
|
+
{
|
|
87
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "outstanding"],
|
|
88
|
+
queryFn: async () => {
|
|
89
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
90
|
+
return sdk.entityStats.queryEntityStats(
|
|
91
|
+
{
|
|
92
|
+
metrics: [{ type: "sum", field: "total_due", alias: "outstanding" }],
|
|
93
|
+
table: "invoices",
|
|
94
|
+
filters: { is_draft: false, voided_at: null, paid_in_full: false },
|
|
95
|
+
},
|
|
96
|
+
{ entity_id: entityId },
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
enabled: !!entityId && !!sdk,
|
|
100
|
+
staleTime: 30_000,
|
|
101
|
+
},
|
|
102
|
+
// Overdue (past due date, unpaid)
|
|
103
|
+
{
|
|
104
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "overdue"],
|
|
105
|
+
queryFn: async () => {
|
|
106
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
107
|
+
return sdk.entityStats.queryEntityStats(
|
|
108
|
+
{
|
|
109
|
+
metrics: [
|
|
110
|
+
{ type: "sum", field: "total_due", alias: "overdue" },
|
|
111
|
+
{ type: "count", alias: "count" },
|
|
112
|
+
],
|
|
113
|
+
table: "invoices",
|
|
114
|
+
filters: { is_draft: false, voided_at: null, paid_in_full: false },
|
|
115
|
+
group_by: ["overdue_bucket"],
|
|
116
|
+
},
|
|
117
|
+
{ entity_id: entityId },
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
enabled: !!entityId && !!sdk,
|
|
121
|
+
staleTime: 30_000,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const [thisMonthQuery, thisYearQuery, outstandingQuery, overdueQuery] = queries;
|
|
127
|
+
|
|
128
|
+
// Extract this month revenue and currency (may have multiple rows if grouped by quote_currency)
|
|
129
|
+
const thisMonthData = thisMonthQuery.data?.data || [];
|
|
130
|
+
const thisMonthRevenue = thisMonthData.reduce(
|
|
131
|
+
(sum: number, row: StatsQueryDataItem) => sum + (Number(row.revenue) || 0),
|
|
132
|
+
0,
|
|
133
|
+
);
|
|
134
|
+
// Get currency from first row with data
|
|
135
|
+
const currency = (thisMonthData[0]?.quote_currency as string) || "EUR";
|
|
136
|
+
|
|
137
|
+
// Extract overdue data (buckets other than "current")
|
|
138
|
+
const overdueData = overdueQuery.data?.data || [];
|
|
139
|
+
const overdueBuckets = overdueData.filter((row: StatsQueryDataItem) => row.overdue_bucket !== "current");
|
|
140
|
+
const totalOverdue = overdueBuckets.reduce(
|
|
141
|
+
(sum: number, row: StatsQueryDataItem) => sum + (Number(row.overdue) || 0),
|
|
142
|
+
0,
|
|
143
|
+
);
|
|
144
|
+
const overdueCount = overdueBuckets.reduce(
|
|
145
|
+
(sum: number, row: StatsQueryDataItem) => sum + (Number(row.count) || 0),
|
|
146
|
+
0,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
data: {
|
|
151
|
+
thisMonth: thisMonthRevenue,
|
|
152
|
+
thisYear: Number(thisYearQuery.data?.data?.[0]?.revenue) || 0,
|
|
153
|
+
outstanding: Number(outstandingQuery.data?.data?.[0]?.outstanding) || 0,
|
|
154
|
+
overdue: totalOverdue,
|
|
155
|
+
overdueCount,
|
|
156
|
+
currency, // Currency from document data
|
|
157
|
+
} as RevenueData,
|
|
158
|
+
isLoading: queries.some((q) => q.isLoading),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats counts hook using the entity stats API.
|
|
3
|
+
* Server-side counting for accurate totals.
|
|
4
|
+
*/
|
|
5
|
+
import { useQueries } from "@tanstack/react-query";
|
|
6
|
+
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
7
|
+
import { STATS_QUERY_CACHE_KEY } from "./use-stats-query";
|
|
8
|
+
|
|
9
|
+
export const STATS_COUNTS_CACHE_KEY = "dashboard-stats-counts";
|
|
10
|
+
|
|
11
|
+
export type StatsCountsData = {
|
|
12
|
+
invoices: number;
|
|
13
|
+
estimates: number;
|
|
14
|
+
customers: number;
|
|
15
|
+
items: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function useStatsCountsData(entityId: string | undefined) {
|
|
19
|
+
const { sdk } = useSDK();
|
|
20
|
+
|
|
21
|
+
const queries = useQueries({
|
|
22
|
+
queries: [
|
|
23
|
+
// Invoices count
|
|
24
|
+
{
|
|
25
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-invoices"],
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
28
|
+
return sdk.entityStats.queryEntityStats(
|
|
29
|
+
{ metrics: [{ type: "count", alias: "total" }], table: "invoices" },
|
|
30
|
+
{ entity_id: entityId },
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
enabled: !!entityId && !!sdk,
|
|
34
|
+
staleTime: 30_000,
|
|
35
|
+
},
|
|
36
|
+
// Estimates count
|
|
37
|
+
{
|
|
38
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-estimates"],
|
|
39
|
+
queryFn: async () => {
|
|
40
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
41
|
+
return sdk.entityStats.queryEntityStats(
|
|
42
|
+
{ metrics: [{ type: "count", alias: "total" }], table: "estimates" },
|
|
43
|
+
{ entity_id: entityId },
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
enabled: !!entityId && !!sdk,
|
|
47
|
+
staleTime: 30_000,
|
|
48
|
+
},
|
|
49
|
+
// Customers count
|
|
50
|
+
{
|
|
51
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-customers"],
|
|
52
|
+
queryFn: async () => {
|
|
53
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
54
|
+
return sdk.entityStats.queryEntityStats(
|
|
55
|
+
{ metrics: [{ type: "count", alias: "total" }], table: "customers" },
|
|
56
|
+
{ entity_id: entityId },
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
enabled: !!entityId && !!sdk,
|
|
60
|
+
staleTime: 30_000,
|
|
61
|
+
},
|
|
62
|
+
// Items count
|
|
63
|
+
{
|
|
64
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, "count-items"],
|
|
65
|
+
queryFn: async () => {
|
|
66
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
67
|
+
return sdk.entityStats.queryEntityStats(
|
|
68
|
+
{ metrics: [{ type: "count", alias: "total" }], table: "items" },
|
|
69
|
+
{ entity_id: entityId },
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
enabled: !!entityId && !!sdk,
|
|
73
|
+
staleTime: 30_000,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const [invoicesQuery, estimatesQuery, customersQuery, itemsQuery] = queries;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
data: {
|
|
82
|
+
invoices: Number(invoicesQuery.data?.data?.[0]?.total) || 0,
|
|
83
|
+
estimates: Number(estimatesQuery.data?.data?.[0]?.total) || 0,
|
|
84
|
+
customers: Number(customersQuery.data?.data?.[0]?.total) || 0,
|
|
85
|
+
items: Number(itemsQuery.data?.data?.[0]?.total) || 0,
|
|
86
|
+
} as StatsCountsData,
|
|
87
|
+
isLoading: queries.some((q) => q.isLoading),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base hook for stats queries using the entity stats API.
|
|
3
|
+
* Provides server-side aggregation instead of client-side calculation.
|
|
4
|
+
*/
|
|
5
|
+
import type { StatsQueryRequest, StatsQueryResponse } from "@spaceinvoices/js-sdk";
|
|
6
|
+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
|
7
|
+
import { useSDK } from "@/ui/providers/sdk-provider";
|
|
8
|
+
|
|
9
|
+
export const STATS_QUERY_CACHE_KEY = "entity-stats-query";
|
|
10
|
+
|
|
11
|
+
export type StatsQueryOptions<TData = StatsQueryResponse> = Omit<
|
|
12
|
+
UseQueryOptions<StatsQueryResponse, Error, TData>,
|
|
13
|
+
"queryKey" | "queryFn"
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generic hook for executing stats queries.
|
|
18
|
+
* Use this as a base for specific stats hooks.
|
|
19
|
+
*/
|
|
20
|
+
export function useStatsQuery<TData = StatsQueryResponse>(
|
|
21
|
+
entityId: string | undefined,
|
|
22
|
+
query: StatsQueryRequest,
|
|
23
|
+
options?: StatsQueryOptions<TData>,
|
|
24
|
+
) {
|
|
25
|
+
const { sdk } = useSDK();
|
|
26
|
+
|
|
27
|
+
return useQuery({
|
|
28
|
+
queryKey: [STATS_QUERY_CACHE_KEY, entityId, query],
|
|
29
|
+
queryFn: async () => {
|
|
30
|
+
if (!entityId || !sdk) throw new Error("Missing entity or SDK");
|
|
31
|
+
// SDK's wrapMethod already unwraps success response and throws on error
|
|
32
|
+
return await sdk.entityStats.queryEntityStats(query, { entity_id: entityId });
|
|
33
|
+
},
|
|
34
|
+
enabled: !!entityId && !!sdk,
|
|
35
|
+
staleTime: 30_000, // 30 seconds
|
|
36
|
+
...options,
|
|
37
|
+
});
|
|
38
|
+
}
|