@tuturuuu/ui 0.38.0 → 0.39.0

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/package.json +6 -6
  3. package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
  4. package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
  5. package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
  6. package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
  7. package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
  8. package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
  9. package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
  10. package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
  11. package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
  12. package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
  13. package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
  14. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
  15. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
  16. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
  17. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  18. package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
  19. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
  20. package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
  21. package/src/components/ui/finance/invoices/hooks.ts +3 -100
  22. package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
  23. package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
  24. package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
  25. package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
  26. package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
  27. package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
  28. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
  29. package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +438 -0
  30. package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
  31. package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
  32. package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
  33. package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
  34. package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
  35. package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
  36. package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
  37. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
  38. package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
  39. package/src/components/ui/finance/invoices/types.ts +3 -3
  40. package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
  41. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
  42. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
  43. package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
  44. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
  45. package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
  46. package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
  47. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
  48. package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
  49. package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
  50. package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
  51. package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1554
  52. package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
  53. package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
  54. package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
  55. package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
  56. package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
  57. package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
  58. package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
  59. package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
  60. package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
  61. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
  62. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
  63. package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
  64. package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
  65. package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
  66. package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
  67. package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
  68. package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
  69. package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +1 -1
  70. package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +1 -1
  71. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
  72. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
  73. package/src/components/ui/text-editor/editor.tsx +3 -0
  74. package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
  75. package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
  76. package/src/components/ui/text-editor/tool-bar.tsx +18 -74
  77. package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
  78. package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
  79. package/src/hooks/__tests__/supabase-provider.test.ts +101 -7
  80. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +27 -0
  81. package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
  82. package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
  83. package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +39 -0
  84. package/src/hooks/calendar-connection-types.ts +49 -0
  85. package/src/hooks/calendar-event-update-queue-types.ts +12 -0
  86. package/src/hooks/calendar-event-write-payload.ts +33 -0
  87. package/src/hooks/collaboration-document-hydration.ts +35 -0
  88. package/src/hooks/supabase-provider.ts +39 -41
  89. package/src/hooks/use-calendar-sync.tsx +41 -62
  90. package/src/hooks/use-calendar.tsx +24 -38
  91. package/src/hooks/use-local-storage.ts +9 -11
  92. package/src/hooks/use-native-calendar-occurrences.ts +85 -0
  93. package/src/hooks/use-yjs-collaboration.ts +23 -0
@@ -0,0 +1,169 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { cleanup, render, screen } from '@testing-library/react';
3
+ import { NextIntlClientProvider } from 'next-intl';
4
+ import { Component, type ComponentProps, type ReactNode } from 'react';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import en from '../../../../../../../../apps/contacts/messages/en.json';
7
+ import viMessages from '../../../../../../../../apps/contacts/messages/vi.json';
8
+ import { WorkspaceVisibilityProvider } from '../../../../../hooks/use-workspace-visibility';
9
+ import InvoiceCard from './invoice-card';
10
+
11
+ let caught: string[] = [];
12
+ class DateBoundary extends Component<
13
+ { children: ReactNode },
14
+ { failed: boolean }
15
+ > {
16
+ state = { failed: false };
17
+ static getDerivedStateFromError() {
18
+ return { failed: true };
19
+ }
20
+ componentDidCatch(error: Error) {
21
+ caught.push(error.name);
22
+ }
23
+ render() {
24
+ return this.state.failed ? (
25
+ <p>Synthetic date boundary</p>
26
+ ) : (
27
+ this.props.children
28
+ );
29
+ }
30
+ }
31
+ function invoice(id: string): ComponentProps<typeof InvoiceCard>['invoice'] {
32
+ return {
33
+ id,
34
+ category_id: 'synthetic-category',
35
+ completed_at: null,
36
+ creator_id: null,
37
+ customer_id: null,
38
+ note: null,
39
+ notice: null,
40
+ paid_amount: 0,
41
+ platform_creator_id: null,
42
+ subscription_months: null,
43
+ transaction_id: null,
44
+ valid_until: null,
45
+ wallet_id: 'synthetic-wallet',
46
+ ws_id: 'synthetic-workspace',
47
+ created_at: '2026-10-01T10:00:00Z',
48
+ price: 100,
49
+ total_diff: 0,
50
+ customer_display_name: `Customer ${id}`,
51
+ customer_full_name: null,
52
+ wallet: null,
53
+ creator: null,
54
+ };
55
+ }
56
+ function mount(locale: 'en' | 'vi', compact: boolean) {
57
+ window.localStorage.setItem('invoice-compact-view', JSON.stringify(compact));
58
+ const client = new QueryClient({
59
+ defaultOptions: { queries: { retry: false } },
60
+ });
61
+ const content = (created_at: string | null) => (
62
+ <QueryClientProvider client={client}>
63
+ <WorkspaceVisibilityProvider actorId="synthetic-account">
64
+ <NextIntlClientProvider
65
+ locale={locale}
66
+ messages={locale === 'en' ? en : viMessages}
67
+ timeZone="UTC"
68
+ now={new Date('2026-10-01T10:00:00Z')}
69
+ >
70
+ <DateBoundary>
71
+ <InvoiceCard
72
+ wsId="synthetic-workspace"
73
+ lang={locale}
74
+ configs={[]}
75
+ invoice={{ ...invoice('synthetic-invoice'), created_at }}
76
+ products={[]}
77
+ promotions={[]}
78
+ />
79
+ </DateBoundary>
80
+ </NextIntlClientProvider>
81
+ </WorkspaceVisibilityProvider>
82
+ </QueryClientProvider>
83
+ );
84
+ const view = render(content('2026-10-01T10:00:00Z'));
85
+ return {
86
+ ...view,
87
+ replaceDate: (value: string | null) => view.rerender(content(value)),
88
+ };
89
+ }
90
+ beforeEach(() => {
91
+ caught = [];
92
+ window.localStorage.clear();
93
+ vi.spyOn(console, 'error').mockImplementation(() => {});
94
+ vi.stubGlobal(
95
+ 'fetch',
96
+ vi.fn(() => {
97
+ throw new Error('Unexpected synthetic card request');
98
+ })
99
+ );
100
+ });
101
+ afterEach(() => {
102
+ cleanup();
103
+ vi.restoreAllMocks();
104
+ vi.unstubAllGlobals();
105
+ });
106
+ for (const locale of ['en', 'vi'] as const) {
107
+ for (const compact of [false, true]) {
108
+ describe(`${locale} actual ${compact ? 'compact' : 'full'} invoice card date`, () => {
109
+ const messages = locale === 'en' ? en : viMessages;
110
+ async function ready() {
111
+ expect(
112
+ await screen.findByRole('tab', {
113
+ name: messages.invoices[compact ? 'compact' : 'full'],
114
+ })
115
+ ).toHaveAttribute('aria-selected', 'true');
116
+ }
117
+ for (const date of ['not-a-date', ' ', '2026-13-40T10:00:00Z']) {
118
+ it(`keeps the actual card usable for unrenderable date ${JSON.stringify(date)}`, async () => {
119
+ const view = mount(locale, compact);
120
+ await ready();
121
+ view.replaceDate(date);
122
+ expect(caught).toEqual([]);
123
+ expect(
124
+ screen.queryByText('Synthetic date boundary')
125
+ ).not.toBeInTheDocument();
126
+ expect(screen.getByText('Customer synthetic-invoice')).toBeVisible();
127
+ expect(
128
+ screen.getByRole('button', { name: messages.common.export })
129
+ ).toBeEnabled();
130
+ expect(
131
+ document.getElementById('printable-area')?.textContent
132
+ ).not.toContain('Invalid Date');
133
+ if (date.trim())
134
+ expect(
135
+ document.getElementById('printable-area')?.textContent
136
+ ).not.toContain(date);
137
+ });
138
+ }
139
+ for (const date of [null, '']) {
140
+ it(`preserves the existing missing date omission for ${JSON.stringify(date)}`, async () => {
141
+ const view = mount(locale, compact);
142
+ await ready();
143
+ view.replaceDate(date);
144
+ expect(caught).toEqual([]);
145
+ expect(screen.getByText('Customer synthetic-invoice')).toBeVisible();
146
+ expect(
147
+ document.getElementById('printable-area')?.textContent
148
+ ).not.toContain(messages.invoices.invoice_date);
149
+ });
150
+ }
151
+ it('preserves locale formatting and preview controls for valid dates', async () => {
152
+ mount(locale, compact);
153
+ await ready();
154
+ const expected = new Intl.DateTimeFormat(locale, {
155
+ day: '2-digit',
156
+ month: '2-digit',
157
+ year: 'numeric',
158
+ }).format(new Date('2026-10-01T10:00:00Z'));
159
+ expect(
160
+ document.getElementById('printable-area')?.textContent
161
+ ).toContain(expected);
162
+ expect(caught).toEqual([]);
163
+ expect(
164
+ screen.getByRole('button', { name: messages.common.export })
165
+ ).toBeEnabled();
166
+ });
167
+ });
168
+ }
169
+ }
@@ -0,0 +1,120 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { cleanup, render, screen, waitFor } from '@testing-library/react';
3
+ import type { ComponentProps, ReactNode } from 'react';
4
+ import { afterEach, beforeEach, expect, it, vi } from 'vitest';
5
+ import { WorkspaceVisibilityProvider } from '../../../../../hooks/use-workspace-visibility';
6
+ import InvoiceCard from './invoice-card';
7
+
8
+ function renderCard(card: ReactNode) {
9
+ return render(
10
+ <QueryClientProvider client={new QueryClient()}>
11
+ <WorkspaceVisibilityProvider actorId="synthetic-account">
12
+ {card}
13
+ </WorkspaceVisibilityProvider>
14
+ </QueryClientProvider>
15
+ );
16
+ }
17
+
18
+ const capture = vi.hoisted(() => vi.fn());
19
+ vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key }));
20
+ vi.mock('html2canvas-pro', () => ({ default: capture }));
21
+ vi.mock('../../shared/use-finance-confidential-visibility', () => ({
22
+ useFinanceConfidentialVisibility: () => ({ isConfidential: false }),
23
+ }));
24
+ const invoice: ComponentProps<typeof InvoiceCard>['invoice'] = {
25
+ category_id: 'synthetic-category',
26
+ completed_at: null,
27
+ creator_id: null,
28
+ customer_id: null,
29
+ note: null,
30
+ notice: null,
31
+ paid_amount: 0,
32
+ platform_creator_id: null,
33
+ subscription_months: null,
34
+ transaction_id: null,
35
+ valid_until: null,
36
+ wallet_id: 'synthetic-wallet',
37
+ ws_id: 'synthetic-workspace',
38
+ id: 'synthetic-invoice',
39
+ created_at: '2026-10-01T10:00:00Z',
40
+ price: 0,
41
+ total_diff: 0,
42
+ customer_display_name: 'Synthetic customer',
43
+ customer_full_name: null,
44
+ wallet: null,
45
+ creator: null,
46
+ };
47
+ beforeEach(() => {
48
+ window.localStorage.clear();
49
+ vi.spyOn(console, 'error').mockImplementation(() => {});
50
+ capture.mockResolvedValue({
51
+ toBlob: (callback: BlobCallback) => callback(null),
52
+ });
53
+ });
54
+ afterEach(() => {
55
+ cleanup();
56
+ vi.restoreAllMocks();
57
+ capture.mockReset();
58
+ window.history.replaceState({}, '', '/');
59
+ });
60
+ for (const kind of ['getItem', 'getter'])
61
+ it(`renders the actual invoice and enters PNG export after ${kind} denial`, async () => {
62
+ const storage = window.localStorage;
63
+ if (kind === 'getItem')
64
+ vi.spyOn(storage, 'getItem').mockImplementation(() => {
65
+ throw new DOMException('Synthetic denied storage', 'SecurityError');
66
+ });
67
+ else
68
+ Object.defineProperty(window, 'localStorage', {
69
+ configurable: true,
70
+ get() {
71
+ throw new DOMException('Synthetic denied getter', 'SecurityError');
72
+ },
73
+ });
74
+ window.history.replaceState({}, '', '/?image=true');
75
+ try {
76
+ renderCard(
77
+ <InvoiceCard
78
+ wsId="synthetic-workspace"
79
+ lang="en"
80
+ configs={[]}
81
+ invoice={invoice}
82
+ products={[]}
83
+ promotions={[]}
84
+ />
85
+ );
86
+ expect(
87
+ await screen.findByRole('tab', { name: 'invoices.full' })
88
+ ).toHaveAttribute('aria-selected', 'true');
89
+ expect(screen.getByText('Synthetic customer')).toBeInTheDocument();
90
+ await waitFor(() => expect(capture).toHaveBeenCalledTimes(1));
91
+ expect(capture.mock.calls[0]?.[0]).toBe(
92
+ document.getElementById('printable-area')
93
+ );
94
+ await waitFor(() => expect(window.location.search).toBe(''));
95
+ } finally {
96
+ if (kind === 'getter')
97
+ Object.defineProperty(window, 'localStorage', {
98
+ configurable: true,
99
+ writable: true,
100
+ value: storage,
101
+ });
102
+ }
103
+ });
104
+ it('retains a valid saved compact preference in the actual invoice', async () => {
105
+ window.localStorage.setItem('invoice-compact-view', 'true');
106
+ renderCard(
107
+ <InvoiceCard
108
+ wsId="synthetic-workspace"
109
+ lang="en"
110
+ configs={[]}
111
+ invoice={invoice}
112
+ products={[]}
113
+ promotions={[]}
114
+ />
115
+ );
116
+ expect(
117
+ await screen.findByRole('tab', { name: 'invoices.compact' })
118
+ ).toHaveAttribute('aria-selected', 'true');
119
+ expect(capture).not.toHaveBeenCalled();
120
+ });
@@ -26,10 +26,13 @@ import { toast } from '@tuturuuu/ui/sonner';
26
26
  import { Tabs, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
27
27
  import { useTranslations } from 'next-intl';
28
28
  import { useCallback, useEffect, useRef, useState } from 'react';
29
+ import { useFinanceConfidentialVisibility } from '../../shared/use-finance-confidential-visibility';
29
30
  import { CompactInvoiceTemplate } from './compact-invoice-template';
30
31
  import { FullInvoiceTemplate } from './full-invoice-template';
32
+ import { useInvoicePngExport } from './use-invoice-png-export';
31
33
 
32
34
  export default function InvoiceCard({
35
+ wsId,
33
36
  lang,
34
37
  configs,
35
38
  invoice,
@@ -37,6 +40,7 @@ export default function InvoiceCard({
37
40
  promotions,
38
41
  currency = 'VND',
39
42
  }: {
43
+ wsId: string;
40
44
  lang: string;
41
45
  configs: WorkspaceConfig[];
42
46
  invoice: Invoice & {
@@ -56,13 +60,32 @@ export default function InvoiceCard({
56
60
  }) {
57
61
  const t = useTranslations();
58
62
 
63
+ const { isConfidential } = useFinanceConfidentialVisibility();
59
64
  const printableRef = useRef<HTMLDivElement>(null);
60
65
  const [isDarkPreview, setIsDarkPreview] = useState(false);
61
66
  const [isCompact, setIsCompact, isCompactInitialized] = useLocalStorage(
62
67
  'invoice-compact-view',
63
68
  false
64
69
  );
65
- const [isExporting, setIsExporting] = useState(false);
70
+ const { isExporting, handlePngExport } = useInvoicePngExport({
71
+ wsId,
72
+ invoice,
73
+ printableRef,
74
+ isDarkPreview,
75
+ ready: isCompactInitialized,
76
+ renderInputs: [
77
+ lang,
78
+ currency,
79
+ invoice,
80
+ configs,
81
+ products,
82
+ promotions,
83
+ isDarkPreview,
84
+ isCompact,
85
+ isCompactInitialized,
86
+ isConfidential,
87
+ ],
88
+ });
66
89
 
67
90
  const handlePrintExport = useCallback(() => {
68
91
  const printableArea = document.getElementById('printable-area');
@@ -155,66 +178,6 @@ export default function InvoiceCard({
155
178
  document.body.appendChild(iframe);
156
179
  }, [invoice.id, t]);
157
180
 
158
- const handlePngExport = useCallback(async () => {
159
- setIsExporting(true);
160
- try {
161
- const html2canvas = (await import('html2canvas-pro')).default;
162
- const element = document.getElementById('printable-area');
163
- if (!element) throw new Error('Printable area not found');
164
-
165
- const canvas = await html2canvas(element, {
166
- scale: 2,
167
- useCORS: true,
168
- backgroundColor: isDarkPreview ? '#1a1a1a' : '#ffffff',
169
- width: element.scrollWidth,
170
- height: element.scrollHeight,
171
- });
172
-
173
- await new Promise<void>((resolve, reject) => {
174
- canvas.toBlob(
175
- (blob) => {
176
- if (!blob) {
177
- reject(new Error('Failed to create image'));
178
- return;
179
- }
180
-
181
- try {
182
- const url = URL.createObjectURL(blob);
183
- const link = document.createElement('a');
184
- link.href = url;
185
-
186
- const sanitizedId = invoice.id
187
- .normalize('NFD')
188
- .replace(/[\u0300-\u036f]/g, '')
189
- .replace(/đ/g, 'd')
190
- .replace(/Đ/g, 'D')
191
- .replace(/[^a-z0-9]/gi, '_')
192
- .toLowerCase()
193
- .replace(/_+/g, '_')
194
- .replace(/^_|_$/g, '');
195
-
196
- link.download = `invoice-${sanitizedId}.png`;
197
- document.body.appendChild(link);
198
- link.click();
199
- document.body.removeChild(link);
200
- URL.revokeObjectURL(url);
201
- resolve();
202
- } catch (error) {
203
- reject(error);
204
- }
205
- },
206
- 'image/png',
207
- 1.0
208
- );
209
- });
210
- toast.success(t('common.export-success'));
211
- } catch {
212
- toast.error(t('common.export-error'));
213
- } finally {
214
- setIsExporting(false);
215
- }
216
- }, [invoice.id, isDarkPreview, t]);
217
-
218
181
  // Auto trigger print or image download when URL contains ?print=true or ?image=true
219
182
  useEffect(() => {
220
183
  if (!isCompactInitialized) return;
@@ -232,7 +195,7 @@ export default function InvoiceCard({
232
195
  }
233
196
 
234
197
  if (shouldDownloadImage) {
235
- await handlePngExport();
198
+ if (!(await handlePngExport())) return;
236
199
  }
237
200
  } catch {
238
201
  toast.error(t('common.export-error'));
@@ -153,6 +153,7 @@ export default async function InvoiceDetailsPage({
153
153
  </div>
154
154
 
155
155
  <InvoiceCard
156
+ wsId={wsId}
156
157
  lang={locale}
157
158
  configs={configs}
158
159
  invoice={{