@tuturuuu/ui 0.38.0 → 0.39.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +8 -8
  3. package/src/components/ui/custom/public-profile-menu-item.tsx +25 -0
  4. package/src/components/ui/custom/satellite-user-menu-items.test.tsx +61 -0
  5. package/src/components/ui/custom/satellite-user-menu-items.tsx +8 -0
  6. package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
  7. package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
  8. package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
  9. package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
  10. package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
  11. package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
  12. package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
  13. package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
  14. package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
  15. package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
  16. package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
  17. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
  18. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
  19. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
  20. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  21. package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
  22. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
  23. package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
  24. package/src/components/ui/finance/invoices/hooks.ts +3 -100
  25. package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
  26. package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
  27. package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
  28. package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
  29. package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
  30. package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
  31. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
  32. package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +489 -0
  33. package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
  34. package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
  35. package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
  36. package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
  37. package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
  38. package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
  39. package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
  40. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
  41. package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
  42. package/src/components/ui/finance/invoices/types.ts +3 -3
  43. package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
  44. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
  45. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
  46. package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
  47. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
  48. package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
  49. package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
  50. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
  51. package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
  52. package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
  53. package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
  54. package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1554
  55. package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
  56. package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
  57. package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
  58. package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
  59. package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
  60. package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
  61. package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
  62. package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
  63. package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
  64. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
  65. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
  66. package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
  67. package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
  68. package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
  69. package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
  70. package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
  71. package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
  72. package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +1 -1
  73. package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +1 -1
  74. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
  75. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
  76. package/src/components/ui/public-link-button.test.tsx +116 -0
  77. package/src/components/ui/public-link-button.tsx +81 -0
  78. package/src/components/ui/text-editor/editor.tsx +3 -0
  79. package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
  80. package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
  81. package/src/components/ui/text-editor/tool-bar.tsx +18 -74
  82. package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
  83. package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
  84. package/src/hooks/__tests__/supabase-provider.test.ts +101 -7
  85. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +27 -0
  86. package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
  87. package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
  88. package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +39 -0
  89. package/src/hooks/calendar-connection-types.ts +49 -0
  90. package/src/hooks/calendar-event-update-queue-types.ts +12 -0
  91. package/src/hooks/calendar-event-write-payload.ts +33 -0
  92. package/src/hooks/collaboration-document-hydration.ts +35 -0
  93. package/src/hooks/supabase-provider.ts +39 -41
  94. package/src/hooks/use-calendar-sync.tsx +41 -62
  95. package/src/hooks/use-calendar.tsx +24 -38
  96. package/src/hooks/use-local-storage.ts +9 -11
  97. package/src/hooks/use-native-calendar-occurrences.ts +85 -0
  98. package/src/hooks/use-yjs-collaboration.ts +23 -0
@@ -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 = undefined;
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.id;
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;