@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
@@ -29,7 +29,16 @@ import { toast } from '@tuturuuu/ui/sonner';
29
29
  import { Textarea } from '@tuturuuu/ui/textarea';
30
30
  import { formatCurrency } from '@tuturuuu/utils/format';
31
31
  import { useTranslations } from 'next-intl';
32
- import { useEffect, useMemo, useState } from 'react';
32
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
33
+
34
+ type ReconciliationRequest = {
35
+ wsId: string;
36
+ walletId: string;
37
+ checkpointId: string;
38
+ payload: { category_id?: string; description: string };
39
+ onCreated: () => void;
40
+ onOpenChange: (open: boolean) => void;
41
+ };
33
42
 
34
43
  const NO_CATEGORY = 'none';
35
44
 
@@ -57,6 +66,10 @@ export function WalletCheckpointAdjustmentDialog({
57
66
  wsId: string;
58
67
  }) {
59
68
  const t = useTranslations('wallet-checkpoints');
69
+ const activeTarget = useRef({ wsId, walletId, checkpointId, open });
70
+ useLayoutEffect(() => {
71
+ activeTarget.current = { wsId, walletId, checkpointId, open };
72
+ }, [wsId, walletId, checkpointId, open]);
60
73
  const [categoryId, setCategoryId] = useState(NO_CATEGORY);
61
74
  const [description, setDescription] = useState(
62
75
  t('reconciliation_description', {
@@ -129,17 +142,28 @@ export function WalletCheckpointAdjustmentDialog({
129
142
  [checkedAt]
130
143
  );
131
144
  const mutation = useMutation({
132
- mutationFn: () =>
133
- createWalletCheckpointReconciliation(wsId, walletId, checkpointId, {
134
- category_id: categoryId === NO_CATEGORY ? undefined : categoryId,
135
- description,
136
- }),
137
- onSuccess: (result) => {
145
+ mutationFn: (request: ReconciliationRequest) =>
146
+ createWalletCheckpointReconciliation(
147
+ request.wsId,
148
+ request.walletId,
149
+ request.checkpointId,
150
+ request.payload
151
+ ),
152
+ onSuccess: (result, request) => {
153
+ // Invalidate the submitted target even if the dialog has since moved.
154
+ request.onCreated();
155
+ const current = activeTarget.current;
156
+ if (
157
+ !current.open ||
158
+ current.wsId !== request.wsId ||
159
+ current.walletId !== request.walletId ||
160
+ current.checkpointId !== request.checkpointId
161
+ )
162
+ return;
138
163
  toast.success(
139
164
  result.created ? t('reconciliation_created') : t('reconciliation_clean')
140
165
  );
141
- onCreated();
142
- onOpenChange(false);
166
+ request.onOpenChange(false);
143
167
  },
144
168
  onError: (error) => {
145
169
  toast.error(
@@ -208,7 +232,20 @@ export function WalletCheckpointAdjustmentDialog({
208
232
  </Button>
209
233
  <Button
210
234
  disabled={mutation.isPending || !categoryInitialized}
211
- onClick={() => mutation.mutate()}
235
+ onClick={() =>
236
+ mutation.mutate({
237
+ wsId,
238
+ walletId,
239
+ checkpointId,
240
+ payload: {
241
+ category_id:
242
+ categoryId === NO_CATEGORY ? undefined : categoryId,
243
+ description,
244
+ },
245
+ onCreated,
246
+ onOpenChange,
247
+ })
248
+ }
212
249
  >
213
250
  {mutation.isPending ? t('creating') : t('reconcile')}
214
251
  </Button>
@@ -1,5 +1,11 @@
1
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import {
3
+ act,
4
+ fireEvent,
5
+ render,
6
+ screen,
7
+ waitFor,
8
+ } from '@testing-library/react';
3
9
  import type { ReactElement, ReactNode } from 'react';
4
10
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
11
  import { WalletCheckpointAdjustmentDialog } from './wallet-checkpoint-adjustment-dialog';
@@ -385,6 +391,94 @@ describe('wallet checkpoint UI', () => {
385
391
  });
386
392
  });
387
393
 
394
+ it('submits the category visible at click time despite a subsequent edit', async () => {
395
+ mocks.defaultReconciliationCategoryId = 'category-reconcile';
396
+ mocks.listTransactionCategories.mockResolvedValue([
397
+ { id: 'category-reconcile', name: 'Audit' },
398
+ ]);
399
+ renderWithQueryClient(
400
+ <WalletCheckpointAdjustmentDialog
401
+ wsId="ws-1"
402
+ walletId="wallet-1"
403
+ checkpointId="checkpoint-1"
404
+ walletName="Cash"
405
+ checkedAt="2026-06-11T10:00:00.000Z"
406
+ currency="USD"
407
+ variance={-12.34}
408
+ open
409
+ onOpenChange={vi.fn()}
410
+ onCreated={vi.fn()}
411
+ />
412
+ );
413
+ const select = screen.getByRole('combobox');
414
+ await waitFor(() => expect(select).toHaveValue('category-reconcile'));
415
+ const button = screen.getByRole('button', { name: 'reconcile' });
416
+ await act(async () => {
417
+ fireEvent.click(button);
418
+ fireEvent.change(select, { target: { value: 'none' } });
419
+ });
420
+ await waitFor(() =>
421
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
422
+ 'ws-1',
423
+ 'wallet-1',
424
+ 'checkpoint-1',
425
+ expect.objectContaining({ category_id: 'category-reconcile' })
426
+ )
427
+ );
428
+ });
429
+
430
+ it('keeps a queued reconciliation and completion bound to its original target', async () => {
431
+ const oldCreated = vi.fn();
432
+ const newCreated = vi.fn();
433
+ const oldClose = vi.fn();
434
+ const newClose = vi.fn();
435
+ const client = new QueryClient({
436
+ defaultOptions: { queries: { retry: false } },
437
+ });
438
+ const dialog = (
439
+ wallet: string,
440
+ checkpoint: string,
441
+ created: () => void,
442
+ close: (open: boolean) => void
443
+ ) => (
444
+ <QueryClientProvider client={client}>
445
+ <WalletCheckpointAdjustmentDialog
446
+ wsId="ws-1"
447
+ walletId={wallet}
448
+ checkpointId={checkpoint}
449
+ walletName="Cash"
450
+ checkedAt="2026-06-11T10:00:00.000Z"
451
+ currency="USD"
452
+ variance={-12.34}
453
+ open
454
+ onOpenChange={close}
455
+ onCreated={created}
456
+ />
457
+ </QueryClientProvider>
458
+ );
459
+ const view = render(
460
+ dialog('wallet-1', 'checkpoint-1', oldCreated, oldClose)
461
+ );
462
+ const button = screen.getByRole('button', { name: 'reconcile' });
463
+ await waitFor(() => expect(button).not.toBeDisabled());
464
+ await act(async () => {
465
+ fireEvent.click(button);
466
+ view.rerender(dialog('wallet-2', 'checkpoint-2', newCreated, newClose));
467
+ });
468
+ await waitFor(() =>
469
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
470
+ 'ws-1',
471
+ 'wallet-1',
472
+ 'checkpoint-1',
473
+ expect.any(Object)
474
+ )
475
+ );
476
+ await waitFor(() => expect(oldCreated).toHaveBeenCalledOnce());
477
+ expect(newCreated).not.toHaveBeenCalled();
478
+ expect(oldClose).not.toHaveBeenCalled();
479
+ expect(newClose).not.toHaveBeenCalled();
480
+ });
481
+
388
482
  it('submits no reconciliation category after clearing the configured default', async () => {
389
483
  mocks.defaultReconciliationCategoryId = 'category-reconcile';
390
484
  mocks.listTransactionCategories.mockResolvedValue([
@@ -27,6 +27,7 @@ import {
27
27
  import { Button } from '../../button';
28
28
  import { Input } from '../../input';
29
29
  import { calendarDraftDate } from './calendar-period';
30
+ import { pastEventTreatment } from './past-event-treatment';
30
31
  import { useCalendarSettings } from './settings/settings-context';
31
32
 
32
33
  dayjs.extend(utc);
@@ -72,6 +73,7 @@ function EventCard({
72
73
  onOpen: (id: string) => void;
73
74
  }) {
74
75
  const { settings } = useCalendarSettings();
76
+ const { preservePastEventOpacity } = useCalendar();
75
77
  const isAllDay = isAllDayEvent(event);
76
78
 
77
79
  return (
@@ -80,7 +82,8 @@ function EventCard({
80
82
  onClick={() => onOpen(event.id)}
81
83
  style={calendarEventStyle(event)}
82
84
  className={cn(
83
- 'group flex w-full cursor-pointer items-start gap-3 rounded-lg border-l-[3px] px-3 py-2.5 text-left transition-colors hover:ring-1 hover:ring-current'
85
+ 'group relative flex w-full cursor-pointer items-start gap-3 rounded-lg border-l-[3px] px-3 py-2.5 text-left transition-colors hover:ring-1 hover:ring-current',
86
+ pastEventTreatment(event, preservePastEventOpacity)
84
87
  )}
85
88
  >
86
89
  {/* Time column */}