@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.
- package/CHANGELOG.md +27 -0
- package/package.json +6 -6
- package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
- package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
- package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
- package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
- package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
- package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
- package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
- package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
- package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
- package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
- package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
- package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
- package/src/components/ui/finance/invoices/hooks.ts +3 -100
- package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
- package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +438 -0
- package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
- package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
- package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
- package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
- package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
- package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
- package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
- package/src/components/ui/finance/invoices/types.ts +3 -3
- package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
- package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
- package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
- package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
- package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
- package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
- package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1554
- package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
- package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
- package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
- package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
- package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
- package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +1 -1
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +1 -1
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
- package/src/components/ui/text-editor/editor.tsx +3 -0
- package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
- package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
- package/src/components/ui/text-editor/tool-bar.tsx +18 -74
- package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
- package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
- package/src/hooks/__tests__/supabase-provider.test.ts +101 -7
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +27 -0
- package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
- package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
- package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +39 -0
- package/src/hooks/calendar-connection-types.ts +49 -0
- package/src/hooks/calendar-event-update-queue-types.ts +12 -0
- package/src/hooks/calendar-event-write-payload.ts +33 -0
- package/src/hooks/collaboration-document-hydration.ts +35 -0
- package/src/hooks/supabase-provider.ts +39 -41
- package/src/hooks/use-calendar-sync.tsx +41 -62
- package/src/hooks/use-calendar.tsx +24 -38
- package/src/hooks/use-local-storage.ts +9 -11
- package/src/hooks/use-native-calendar-occurrences.ts +85 -0
- 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
|
|
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'));
|