@tuturuuu/ui 0.37.2 → 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 +55 -0
- package/package.json +10 -7
- package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +86 -0
- package/src/components/ui/custom/education-todo.test.tsx +131 -0
- package/src/components/ui/custom/education-todo.tsx +191 -0
- package/src/components/ui/custom/satellite-notification-polling.test.tsx +61 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
- package/src/components/ui/custom/workspace-picker-content.tsx +93 -30
- package/src/components/ui/custom/workspace-select.tsx +12 -4
- 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-draft-recovery.ts +34 -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 -1614
- package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
- package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
- package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
- package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
- 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-color-picker.test.tsx +147 -0
- package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -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/save-calendar-event-drafts.test.ts +35 -1
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
- 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/popover.tsx +3 -1
- package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
- package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
- package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
- package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
- package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
- package/src/components/ui/text-editor/editor.tsx +11 -3
- package/src/components/ui/text-editor/extensions.ts +8 -2
- 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/markdown-paste-extension.ts +1 -1
- package/src/components/ui/text-editor/markdown.ts +11 -0
- package/src/components/ui/text-editor/tool-bar.tsx +45 -102
- package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
- package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
- package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
- package/src/hooks/__tests__/supabase-provider.test.ts +110 -7
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +39 -2
- 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-notifications-subscription.test.tsx +31 -1
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
- package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +147 -0
- package/src/hooks/calendar-connection-types.ts +49 -0
- package/src/hooks/calendar-creation-request.test.ts +62 -0
- package/src/hooks/calendar-creation-request.ts +34 -0
- package/src/hooks/calendar-event-update-queue-types.ts +12 -0
- package/src/hooks/calendar-event-write-payload.ts +57 -0
- package/src/hooks/collaboration-document-hydration.ts +35 -0
- package/src/hooks/notification-subscription.ts +5 -2
- package/src/hooks/supabase-provider.ts +76 -112
- package/src/hooks/use-calendar-sync.tsx +41 -62
- package/src/hooks/use-calendar.tsx +44 -82
- package/src/hooks/use-exchange-rates.ts +4 -15
- package/src/hooks/use-local-storage.ts +9 -11
- package/src/hooks/use-native-calendar-occurrences.ts +85 -0
- package/src/hooks/use-workspace-presence.ts +9 -9
- package/src/hooks/use-yjs-collaboration.ts +68 -27
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useMemo,
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react';
|
|
11
|
+
import { hasInvalidSubscriptionLinks } from '../subscription-admission';
|
|
11
12
|
import type {
|
|
12
13
|
Product,
|
|
13
14
|
ProductInventory,
|
|
@@ -37,7 +38,7 @@ interface UseSubscriptionAutoSelectionProps {
|
|
|
37
38
|
workspaceTimezone?: string | null;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export type UseSubscriptionAutoSelectionResult =
|
|
41
|
+
export type UseSubscriptionAutoSelectionResult = { blocked: boolean };
|
|
41
42
|
|
|
42
43
|
const buildAutoSelectedProductsForGroup = (
|
|
43
44
|
groupLinked: UserGroupProducts[],
|
|
@@ -58,7 +59,8 @@ const buildAutoSelectedProductsForGroup = (
|
|
|
58
59
|
? groupAttendanceDaysMap[groupId]
|
|
59
60
|
: attendanceDays;
|
|
60
61
|
|
|
61
|
-
const desiredUnitId = linkItem.inventory_units
|
|
62
|
+
const desiredUnitId = linkItem.inventory_units?.id;
|
|
63
|
+
if (!desiredUnitId) continue;
|
|
62
64
|
const desiredWarehouseId = linkItem.warehouse_id;
|
|
63
65
|
|
|
64
66
|
const product = allProducts.find((p) => p.id === productId);
|
|
@@ -223,6 +225,7 @@ export function useSubscriptionAutoSelection({
|
|
|
223
225
|
workspaceTimezone,
|
|
224
226
|
}: UseSubscriptionAutoSelectionProps): UseSubscriptionAutoSelectionResult {
|
|
225
227
|
const t = useTranslations();
|
|
228
|
+
const blocked = hasInvalidSubscriptionLinks(groupProducts, products);
|
|
226
229
|
const previousGroupIdRef = useRef<string>('');
|
|
227
230
|
const fallbackToastShownRef = useRef<boolean>(false);
|
|
228
231
|
const initialPrefillUsedRef = useRef<boolean>(false);
|
|
@@ -291,6 +294,7 @@ export function useSubscriptionAutoSelection({
|
|
|
291
294
|
useEffect(() => {
|
|
292
295
|
if (
|
|
293
296
|
!enabled ||
|
|
297
|
+
blocked ||
|
|
294
298
|
sortedSelectedGroupIds.length === 0 ||
|
|
295
299
|
groupProducts.length === 0
|
|
296
300
|
) {
|
|
@@ -330,6 +334,7 @@ export function useSubscriptionAutoSelection({
|
|
|
330
334
|
}
|
|
331
335
|
}, [
|
|
332
336
|
enabled,
|
|
337
|
+
blocked,
|
|
333
338
|
sortedSelectedGroupIds,
|
|
334
339
|
selectedMonth,
|
|
335
340
|
prefillAmount,
|
|
@@ -342,4 +347,5 @@ export function useSubscriptionAutoSelection({
|
|
|
342
347
|
groupAttendanceDaysMap,
|
|
343
348
|
prepaidMonthCount,
|
|
344
349
|
]);
|
|
350
|
+
return { blocked };
|
|
345
351
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { toast } from '@tuturuuu/ui/sonner';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
7
|
+
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { useWorkspaceActor } from '../../../../../hooks/use-workspace-visibility';
|
|
9
|
+
import { useFinanceHref } from '../../finance-route-context';
|
|
10
|
+
import {
|
|
11
|
+
type CreateSubscriptionInvoicePayload,
|
|
12
|
+
createSubscriptionInvoiceWithInternalApi,
|
|
13
|
+
} from '../internal-api';
|
|
14
|
+
import { formatInvoiceRecalculationDescription } from '../invoice-visibility-format';
|
|
15
|
+
import { invalidateInvoiceMutationQueries } from '../query-invalidation';
|
|
16
|
+
import type { getSubscriptionBlockReason } from '../subscription-admission';
|
|
17
|
+
|
|
18
|
+
type Options = {
|
|
19
|
+
createMultipleInvoices: boolean;
|
|
20
|
+
printAfterCreate: boolean;
|
|
21
|
+
downloadImageAfterCreate: boolean;
|
|
22
|
+
};
|
|
23
|
+
type Scope = {
|
|
24
|
+
actor: ReturnType<typeof useWorkspaceActor>;
|
|
25
|
+
wsId: string;
|
|
26
|
+
customerId: string;
|
|
27
|
+
};
|
|
28
|
+
type Intent = {
|
|
29
|
+
scope: Scope;
|
|
30
|
+
revision: number;
|
|
31
|
+
payload: CreateSubscriptionInvoicePayload;
|
|
32
|
+
options: Options;
|
|
33
|
+
onReset: () => void;
|
|
34
|
+
areNumbersHidden: boolean;
|
|
35
|
+
currency: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Completion belongs to a draft; admission is never undone by leaving it. */
|
|
39
|
+
export function useSubscriptionCreate({
|
|
40
|
+
wsId,
|
|
41
|
+
payload,
|
|
42
|
+
options,
|
|
43
|
+
blockedReason,
|
|
44
|
+
checkoutReady,
|
|
45
|
+
areNumbersHidden,
|
|
46
|
+
currency,
|
|
47
|
+
onReset,
|
|
48
|
+
}: {
|
|
49
|
+
wsId: string;
|
|
50
|
+
payload: CreateSubscriptionInvoicePayload;
|
|
51
|
+
options: Options;
|
|
52
|
+
blockedReason: ReturnType<typeof getSubscriptionBlockReason>;
|
|
53
|
+
checkoutReady: boolean;
|
|
54
|
+
areNumbersHidden: boolean;
|
|
55
|
+
currency: string;
|
|
56
|
+
onReset: () => void;
|
|
57
|
+
}) {
|
|
58
|
+
const actor = useWorkspaceActor();
|
|
59
|
+
const client = useQueryClient();
|
|
60
|
+
const router = useRouter();
|
|
61
|
+
const financeHref = useFinanceHref();
|
|
62
|
+
const t = useTranslations();
|
|
63
|
+
const locale = useLocale();
|
|
64
|
+
const scope = useMemo(
|
|
65
|
+
() => ({ actor, wsId, customerId: payload.customer_id }),
|
|
66
|
+
[actor, wsId, payload.customer_id]
|
|
67
|
+
);
|
|
68
|
+
const draft = JSON.stringify({
|
|
69
|
+
payload,
|
|
70
|
+
options,
|
|
71
|
+
areNumbersHidden,
|
|
72
|
+
currency,
|
|
73
|
+
locale,
|
|
74
|
+
blockedReason,
|
|
75
|
+
checkoutReady,
|
|
76
|
+
});
|
|
77
|
+
const state = useRef<{
|
|
78
|
+
active: boolean;
|
|
79
|
+
scope: Scope;
|
|
80
|
+
revision: number;
|
|
81
|
+
draft: string;
|
|
82
|
+
owner: Intent | null;
|
|
83
|
+
}>({ active: false, scope, revision: 0, draft: '', owner: null });
|
|
84
|
+
const [creating, setCreating] = useState(false);
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
state.current = {
|
|
87
|
+
active: true,
|
|
88
|
+
scope,
|
|
89
|
+
revision: state.current.revision + 1,
|
|
90
|
+
draft: '',
|
|
91
|
+
owner: null,
|
|
92
|
+
};
|
|
93
|
+
setCreating(false);
|
|
94
|
+
return () => {
|
|
95
|
+
state.current.active = false;
|
|
96
|
+
state.current.revision++;
|
|
97
|
+
state.current.owner = null;
|
|
98
|
+
};
|
|
99
|
+
}, [scope]);
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
if (state.current.draft !== draft) {
|
|
102
|
+
state.current.draft = draft;
|
|
103
|
+
state.current.revision++;
|
|
104
|
+
}
|
|
105
|
+
}, [draft]);
|
|
106
|
+
const active = (intent: Intent) => {
|
|
107
|
+
if (
|
|
108
|
+
!state.current.active ||
|
|
109
|
+
state.current.scope !== intent.scope ||
|
|
110
|
+
state.current.owner !== intent ||
|
|
111
|
+
state.current.revision !== intent.revision ||
|
|
112
|
+
!intent.scope.actor
|
|
113
|
+
)
|
|
114
|
+
return false;
|
|
115
|
+
try {
|
|
116
|
+
intent.scope.actor.assertActive();
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const submit = async () => {
|
|
123
|
+
if (
|
|
124
|
+
state.current.owner ||
|
|
125
|
+
!state.current.active ||
|
|
126
|
+
state.current.scope !== scope
|
|
127
|
+
)
|
|
128
|
+
return;
|
|
129
|
+
if (blockedReason) {
|
|
130
|
+
toast(t(`ws-invoices.${blockedReason}`));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!checkoutReady) {
|
|
134
|
+
toast(t('ws-invoices.create_subscription_invoice_validation'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!payload.products.length) {
|
|
138
|
+
toast(t('ws-invoices.no_products_to_invoice'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const intent: Intent = {
|
|
142
|
+
scope,
|
|
143
|
+
revision: state.current.revision,
|
|
144
|
+
payload: structuredClone(payload),
|
|
145
|
+
options: { ...options },
|
|
146
|
+
onReset,
|
|
147
|
+
areNumbersHidden,
|
|
148
|
+
currency,
|
|
149
|
+
};
|
|
150
|
+
state.current.owner = intent;
|
|
151
|
+
if (!active(intent)) {
|
|
152
|
+
state.current.owner = null;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
setCreating(true);
|
|
156
|
+
try {
|
|
157
|
+
if (!active(intent)) return;
|
|
158
|
+
const result = await createSubscriptionInvoiceWithInternalApi(
|
|
159
|
+
intent.scope.wsId,
|
|
160
|
+
intent.payload
|
|
161
|
+
);
|
|
162
|
+
if (!active(intent)) return;
|
|
163
|
+
void invalidateInvoiceMutationQueries(client, intent.scope.wsId);
|
|
164
|
+
if (!active(intent)) return;
|
|
165
|
+
if (result.data?.values_recalculated) {
|
|
166
|
+
const { calculated_values, frontend_values } = result.data;
|
|
167
|
+
toast(t('ws-invoices.subscription_invoice_created_recalculated'), {
|
|
168
|
+
description: formatInvoiceRecalculationDescription({
|
|
169
|
+
areNumbersHidden: intent.areNumbersHidden,
|
|
170
|
+
calculatedTotal: calculated_values.total,
|
|
171
|
+
currency: intent.currency,
|
|
172
|
+
frontendTotal: frontend_values?.total || 0,
|
|
173
|
+
roundingApplied: calculated_values.rounding_applied,
|
|
174
|
+
t,
|
|
175
|
+
}),
|
|
176
|
+
duration: 5000,
|
|
177
|
+
});
|
|
178
|
+
} else
|
|
179
|
+
toast(
|
|
180
|
+
t('ws-invoices.subscription_invoice_created_success', {
|
|
181
|
+
invoiceId: result.invoice_id,
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
if (!active(intent)) return;
|
|
185
|
+
if (intent.options.createMultipleInvoices) intent.onReset();
|
|
186
|
+
else {
|
|
187
|
+
const params = new URLSearchParams();
|
|
188
|
+
if (intent.options.printAfterCreate) params.set('print', 'true');
|
|
189
|
+
if (intent.options.downloadImageAfterCreate)
|
|
190
|
+
params.set('image', 'true');
|
|
191
|
+
router.push(
|
|
192
|
+
`/${intent.scope.wsId}${financeHref(`/invoices/${result.invoice_id}`)}${params.toString() ? `?${params.toString()}` : ''}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (active(intent))
|
|
197
|
+
toast(
|
|
198
|
+
t('ws-invoices.error_creating_subscription_invoice', {
|
|
199
|
+
error:
|
|
200
|
+
error instanceof Error
|
|
201
|
+
? error.message
|
|
202
|
+
: t('ws-invoices.failed_to_create_subscription_invoice'),
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
} finally {
|
|
206
|
+
// A newer draft may expire effects, but only this still-owned pending write
|
|
207
|
+
// can release its indicator. An old scope can never clear a new owner.
|
|
208
|
+
if (
|
|
209
|
+
state.current.active &&
|
|
210
|
+
state.current.scope === intent.scope &&
|
|
211
|
+
state.current.owner === intent
|
|
212
|
+
) {
|
|
213
|
+
state.current.owner = null;
|
|
214
|
+
setCreating(false);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
return { submit, creating };
|
|
219
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import {
|
|
3
|
+
getSubscriptionInvoiceContext,
|
|
4
|
+
type SubscriptionInvoiceContextResponse,
|
|
5
|
+
} from '@tuturuuu/internal-api/finance';
|
|
6
|
+
import { useWorkspaceActor } from '../../../../../hooks/use-workspace-visibility';
|
|
7
|
+
|
|
8
|
+
import { invoiceActorLifetimeKey } from './invoice-actor-lifetime-key';
|
|
9
|
+
|
|
10
|
+
export function useSubscriptionInvoiceContext(
|
|
11
|
+
wsId: string,
|
|
12
|
+
userId: string,
|
|
13
|
+
groupIds: string[],
|
|
14
|
+
month: string,
|
|
15
|
+
monthCount = 1
|
|
16
|
+
) {
|
|
17
|
+
const actor = useWorkspaceActor();
|
|
18
|
+
return useQuery({
|
|
19
|
+
queryKey: [
|
|
20
|
+
'subscription-invoice-context',
|
|
21
|
+
wsId,
|
|
22
|
+
userId,
|
|
23
|
+
groupIds,
|
|
24
|
+
month,
|
|
25
|
+
monthCount,
|
|
26
|
+
actor?.actorId ?? null,
|
|
27
|
+
actor ? invoiceActorLifetimeKey(actor.lifetime) : null,
|
|
28
|
+
],
|
|
29
|
+
queryFn: async (): Promise<SubscriptionInvoiceContextResponse> => {
|
|
30
|
+
if (!actor) throw new Error('Workspace account unavailable');
|
|
31
|
+
actor.assertActive();
|
|
32
|
+
if (!wsId || !userId || groupIds.length === 0 || !month) {
|
|
33
|
+
return {
|
|
34
|
+
attendance: [],
|
|
35
|
+
latestInvoices: [],
|
|
36
|
+
scheduledSessionsByGroupId: {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const response = await getSubscriptionInvoiceContext(wsId, {
|
|
41
|
+
groupIds,
|
|
42
|
+
month,
|
|
43
|
+
monthCount,
|
|
44
|
+
userId,
|
|
45
|
+
});
|
|
46
|
+
actor.assertActive();
|
|
47
|
+
return response;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
actor.assertActive();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
enabled: !!actor && !!wsId && !!userId && groupIds.length > 0 && !!month,
|
|
54
|
+
staleTime: 5 * 60 * 1000,
|
|
55
|
+
gcTime: 10 * 60 * 1000,
|
|
56
|
+
refetchOnWindowFocus: false,
|
|
57
|
+
retry: false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useWorkspaceActor } from '../../../../../hooks/use-workspace-visibility';
|
|
3
|
+
import { listUserGroupsWithInternalApi } from '../internal-api';
|
|
4
|
+
import type { UserGroup } from '../utils';
|
|
5
|
+
import { invoiceActorLifetimeKey } from './invoice-actor-lifetime-key';
|
|
6
|
+
|
|
7
|
+
export function useUserGroups(wsId: string, userId: string) {
|
|
8
|
+
const actor = useWorkspaceActor();
|
|
9
|
+
return useQuery({
|
|
10
|
+
queryKey: [
|
|
11
|
+
'user-groups',
|
|
12
|
+
wsId,
|
|
13
|
+
userId,
|
|
14
|
+
actor?.actorId ?? null,
|
|
15
|
+
actor ? invoiceActorLifetimeKey(actor.lifetime) : null,
|
|
16
|
+
],
|
|
17
|
+
queryFn: async (): Promise<UserGroup[]> => {
|
|
18
|
+
if (!actor) throw new Error('Workspace account unavailable');
|
|
19
|
+
actor.assertActive();
|
|
20
|
+
if (!userId) return [];
|
|
21
|
+
try {
|
|
22
|
+
const groups = await listUserGroupsWithInternalApi(wsId, userId);
|
|
23
|
+
actor.assertActive();
|
|
24
|
+
return (groups || []).map((group) => ({
|
|
25
|
+
workspace_user_groups: group.workspace_user_groups ?? null,
|
|
26
|
+
}));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
actor.assertActive();
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
enabled: !!actor && !!wsId && !!userId,
|
|
33
|
+
staleTime: 5 * 60 * 1000,
|
|
34
|
+
gcTime: 10 * 60 * 1000,
|
|
35
|
+
refetchOnWindowFocus: false,
|
|
36
|
+
retry: false,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
keepPreviousData,
|
|
3
|
-
useInfiniteQuery,
|
|
4
3
|
useMutation,
|
|
5
4
|
useQuery,
|
|
6
5
|
useQueryClient,
|
|
7
6
|
} from '@tanstack/react-query';
|
|
8
7
|
import {
|
|
9
8
|
getPendingFinanceInvoicesCount,
|
|
10
|
-
getSubscriptionInvoiceContext,
|
|
11
9
|
listFinanceInvoices,
|
|
12
10
|
listPendingFinanceInvoices,
|
|
13
11
|
listTransactionCategories,
|
|
@@ -28,14 +26,15 @@ import {
|
|
|
28
26
|
listMultiGroupProductsWithInternalApi,
|
|
29
27
|
listPromotionsWithInternalApi,
|
|
30
28
|
listUserGroupProductsWithInternalApi,
|
|
31
|
-
listUserGroupsWithInternalApi,
|
|
32
29
|
listUserLinkedPromotionsWithInternalApi,
|
|
33
30
|
listUserReferralDiscountsWithInternalApi,
|
|
34
31
|
listWorkspaceUsersWithInternalApi,
|
|
35
32
|
} from './internal-api';
|
|
36
|
-
import type { UserGroup } from './utils';
|
|
37
33
|
|
|
34
|
+
export { useInfiniteUserInvoices } from './hooks/use-infinite-user-invoices';
|
|
38
35
|
export { useInvoiceCustomerSearch } from './hooks/use-invoice-customer-search';
|
|
36
|
+
export { useSubscriptionInvoiceContext } from './hooks/use-subscription-invoice-context';
|
|
37
|
+
export { useUserGroups } from './hooks/use-user-groups';
|
|
39
38
|
|
|
40
39
|
const invoiceSchema = z.object({
|
|
41
40
|
id: z.string(),
|
|
@@ -343,102 +342,6 @@ export const useUserInvoices = (wsId: string, userId: string) => {
|
|
|
343
342
|
});
|
|
344
343
|
};
|
|
345
344
|
|
|
346
|
-
export const useInfiniteUserInvoices = (
|
|
347
|
-
wsId: string,
|
|
348
|
-
userId: string,
|
|
349
|
-
pageSize = 10
|
|
350
|
-
) => {
|
|
351
|
-
return useInfiniteQuery({
|
|
352
|
-
queryKey: ['infinite-user-invoices', wsId, userId],
|
|
353
|
-
queryFn: async ({ pageParam = 1 }: { pageParam: number }) => {
|
|
354
|
-
const payload = await listFinanceInvoices(wsId, {
|
|
355
|
-
customerIds: [userId],
|
|
356
|
-
page: String(pageParam),
|
|
357
|
-
pageSize: String(pageSize),
|
|
358
|
-
});
|
|
359
|
-
const data = payload.data || [];
|
|
360
|
-
const count = payload.count ?? 0;
|
|
361
|
-
const fetchedCount = pageParam * pageSize;
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
data,
|
|
365
|
-
count,
|
|
366
|
-
nextPage: fetchedCount < count ? pageParam + 1 : null,
|
|
367
|
-
hasMore: fetchedCount < count,
|
|
368
|
-
};
|
|
369
|
-
},
|
|
370
|
-
initialPageParam: 1,
|
|
371
|
-
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
372
|
-
enabled: !!userId,
|
|
373
|
-
});
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
export const useUserGroups = (wsId: string, userId: string) => {
|
|
377
|
-
return useQuery({
|
|
378
|
-
queryKey: ['user-groups', wsId, userId],
|
|
379
|
-
queryFn: async (): Promise<UserGroup[]> => {
|
|
380
|
-
if (!userId) return [];
|
|
381
|
-
const groups = await listUserGroupsWithInternalApi(wsId, userId);
|
|
382
|
-
return (groups || []).map((group) => ({
|
|
383
|
-
workspace_user_groups: group.workspace_user_groups ?? null,
|
|
384
|
-
}));
|
|
385
|
-
},
|
|
386
|
-
enabled: !!wsId && !!userId,
|
|
387
|
-
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
388
|
-
gcTime: 10 * 60 * 1000, // 10 minutes
|
|
389
|
-
refetchOnWindowFocus: false,
|
|
390
|
-
retry: false,
|
|
391
|
-
});
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
export const useSubscriptionInvoiceContext = (
|
|
395
|
-
wsId: string,
|
|
396
|
-
userId: string,
|
|
397
|
-
groupIds: string[],
|
|
398
|
-
month: string,
|
|
399
|
-
monthCount = 1
|
|
400
|
-
) => {
|
|
401
|
-
return useQuery({
|
|
402
|
-
queryKey: [
|
|
403
|
-
'subscription-invoice-context',
|
|
404
|
-
wsId,
|
|
405
|
-
userId,
|
|
406
|
-
groupIds,
|
|
407
|
-
month,
|
|
408
|
-
monthCount,
|
|
409
|
-
],
|
|
410
|
-
queryFn: async () => {
|
|
411
|
-
if (!wsId || !userId || groupIds.length === 0 || !month) {
|
|
412
|
-
return {
|
|
413
|
-
attendance: [] as Array<{
|
|
414
|
-
status: string;
|
|
415
|
-
date: string;
|
|
416
|
-
group_id?: string;
|
|
417
|
-
}>,
|
|
418
|
-
latestInvoices: [] as Array<{
|
|
419
|
-
group_id?: string;
|
|
420
|
-
valid_until?: string | null;
|
|
421
|
-
created_at?: string | null;
|
|
422
|
-
}>,
|
|
423
|
-
scheduledSessionsByGroupId: {} as Record<string, string[]>,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return getSubscriptionInvoiceContext(wsId, {
|
|
428
|
-
groupIds,
|
|
429
|
-
month,
|
|
430
|
-
monthCount,
|
|
431
|
-
userId,
|
|
432
|
-
});
|
|
433
|
-
},
|
|
434
|
-
enabled: !!wsId && !!userId && groupIds.length > 0 && !!month,
|
|
435
|
-
staleTime: 5 * 60 * 1000,
|
|
436
|
-
gcTime: 10 * 60 * 1000,
|
|
437
|
-
refetchOnWindowFocus: false,
|
|
438
|
-
retry: false,
|
|
439
|
-
});
|
|
440
|
-
};
|
|
441
|
-
|
|
442
345
|
// Get workspace config for attendance-based invoice calculation
|
|
443
346
|
// Returns true if attendance-based calculation should be used (default), false if all sessions should be included
|
|
444
347
|
export const useInvoiceAttendanceConfig = (
|
|
@@ -3,6 +3,7 @@ import type { WorkspaceConfig } from '@tuturuuu/types/primitives/WorkspaceConfig
|
|
|
3
3
|
import { formatCurrency } from '@tuturuuu/utils/format';
|
|
4
4
|
import { useTranslations } from 'next-intl';
|
|
5
5
|
import { FinanceDisplayAmount } from '../../shared/finance-display-amount';
|
|
6
|
+
import { formatInvoiceTemplateDate } from './format-invoice-template-date';
|
|
6
7
|
|
|
7
8
|
export function CompactInvoiceTemplate({
|
|
8
9
|
invoice,
|
|
@@ -36,13 +37,7 @@ export function CompactInvoiceTemplate({
|
|
|
36
37
|
const brandLogoAlt = BRAND_NAME
|
|
37
38
|
? t('invoices.brand_logo_alt', { brand: BRAND_NAME })
|
|
38
39
|
: t('invoices.logo_alt');
|
|
39
|
-
const invoiceDate = invoice.created_at
|
|
40
|
-
? new Intl.DateTimeFormat(lang, {
|
|
41
|
-
day: '2-digit',
|
|
42
|
-
month: '2-digit',
|
|
43
|
-
year: 'numeric',
|
|
44
|
-
}).format(new Date(invoice.created_at))
|
|
45
|
-
: null;
|
|
40
|
+
const invoiceDate = formatInvoiceTemplateDate(invoice.created_at, lang);
|
|
46
41
|
const dividerClass = isDarkPreview
|
|
47
42
|
? 'border-foreground/30 print:border-black'
|
|
48
43
|
: 'border-black/30 print:border-black';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatInvoiceTemplateDate(
|
|
2
|
+
value: string | null | undefined,
|
|
3
|
+
locale: string
|
|
4
|
+
): string | null {
|
|
5
|
+
if (!value) return null;
|
|
6
|
+
const date = new Date(value);
|
|
7
|
+
if (!Number.isFinite(date.getTime())) return null;
|
|
8
|
+
return new Intl.DateTimeFormat(locale, {
|
|
9
|
+
day: '2-digit',
|
|
10
|
+
month: '2-digit',
|
|
11
|
+
year: 'numeric',
|
|
12
|
+
}).format(date);
|
|
13
|
+
}
|
|
@@ -9,6 +9,7 @@ import { formatCurrency } from '@tuturuuu/utils/format';
|
|
|
9
9
|
import { useTranslations } from 'next-intl';
|
|
10
10
|
import { FinanceDisplayAmount } from '../../shared/finance-display-amount';
|
|
11
11
|
import { getSavedInvoiceDiscount } from '../invoice-discount';
|
|
12
|
+
import { formatInvoiceTemplateDate } from './format-invoice-template-date';
|
|
12
13
|
|
|
13
14
|
export function FullInvoiceTemplate({
|
|
14
15
|
invoice,
|
|
@@ -43,13 +44,7 @@ export function FullInvoiceTemplate({
|
|
|
43
44
|
const brandLogoAlt = brandName
|
|
44
45
|
? t('invoices.brand_logo_alt', { brand: brandName })
|
|
45
46
|
: t('invoices.logo_alt');
|
|
46
|
-
const invoiceDate = invoice.created_at
|
|
47
|
-
? new Intl.DateTimeFormat(lang, {
|
|
48
|
-
day: '2-digit',
|
|
49
|
-
month: '2-digit',
|
|
50
|
-
year: 'numeric',
|
|
51
|
-
}).format(new Date(invoice.created_at))
|
|
52
|
-
: null;
|
|
47
|
+
const invoiceDate = formatInvoiceTemplateDate(invoice.created_at, lang);
|
|
53
48
|
|
|
54
49
|
const subtotal = products.reduce((total, product) => {
|
|
55
50
|
return total + product.price * product.amount;
|