@tuturuuu/ui 0.1.0 → 0.3.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 (128) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +82 -70
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
  12. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
  13. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
  14. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
  15. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
  16. package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
  17. package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
  18. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  19. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  20. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  21. package/src/components/ui/custom/common-footer.tsx +16 -1
  22. package/src/components/ui/custom/production-indicator.tsx +1 -1
  23. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  24. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  25. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  26. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  27. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  28. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  29. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  30. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  31. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  32. package/src/components/ui/custom/workspace-select.tsx +33 -12
  33. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  34. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  35. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  36. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  37. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  38. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  39. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  40. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  41. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  42. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  43. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  44. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  46. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  47. package/src/components/ui/finance/invoices/utils.ts +75 -17
  48. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  49. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  50. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  51. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  52. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  53. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  54. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  55. package/src/components/ui/finance/transactions/form.tsx +60 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  57. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  58. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  59. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  60. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  61. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  62. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  63. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  64. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  65. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  66. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  67. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  68. package/src/components/ui/legacy/meet/page.tsx +87 -39
  69. package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
  70. package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
  71. package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
  72. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  73. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  74. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  77. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  78. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  79. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  80. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  81. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  82. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  83. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  84. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  85. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  86. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  87. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  88. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  89. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  90. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  91. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  93. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  94. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  95. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  96. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  97. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  98. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  99. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  100. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  101. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  102. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  103. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  104. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  105. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  106. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  107. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  108. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  109. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  110. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  111. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  112. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  113. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
  114. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  115. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
  116. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  117. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  118. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  119. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  120. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  121. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  122. package/src/hooks/use-calendar-sync.tsx +22 -277
  123. package/src/hooks/use-calendar.tsx +95 -525
  124. package/src/hooks/use-task-actions.ts +43 -117
  125. package/src/hooks/use-user-config.ts +1 -1
  126. package/src/hooks/use-workspace-config.ts +6 -2
  127. package/src/hooks/use-workspace-presence.ts +1 -1
  128. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -5,6 +5,7 @@ import type { UserGroup } from '../utils';
5
5
  import {
6
6
  formatMonthLabel,
7
7
  getAttendanceStats,
8
+ getSubscriptionCoverageInvoiceForGroup,
8
9
  getTotalSessionsForGroups,
9
10
  parseLocalCalendarDate,
10
11
  } from '../utils';
@@ -72,8 +73,9 @@ export function useSubscriptionInvoiceContent({
72
73
  if (!group) return;
73
74
 
74
75
  const groupName = group.workspace_user_groups?.name || 'Unknown Group';
75
- const latestInvoice = latestSubscriptionInvoices.find(
76
- (inv) => inv.group_id === groupId
76
+ const latestInvoice = getSubscriptionCoverageInvoiceForGroup(
77
+ latestSubscriptionInvoices,
78
+ groupId
77
79
  );
78
80
 
79
81
  const startMonth = latestInvoice?.valid_until
@@ -126,9 +128,12 @@ export function useSubscriptionInvoiceContent({
126
128
  let autoNotes: string | null = null;
127
129
  if (selectedGroupIds.length > 0 && userAttendance.length > 0) {
128
130
  const filteredAttendance = userAttendance.filter((a) => {
129
- const latestInvoice = latestSubscriptionInvoices.find(
130
- (inv) => inv.group_id === a.group_id
131
- );
131
+ const latestInvoice = a.group_id
132
+ ? getSubscriptionCoverageInvoiceForGroup(
133
+ latestSubscriptionInvoices,
134
+ a.group_id
135
+ )
136
+ : undefined;
132
137
  if (!latestInvoice?.valid_until) return true;
133
138
  const validUntil = parseLocalCalendarDate(latestInvoice.valid_until);
134
139
  const attendanceDate = parseLocalCalendarDate(a.date);
@@ -173,6 +173,16 @@ const INVOICE_BLOCKED_GROUP_IDS_FOR_CREATION_CONFIG_ID =
173
173
  'INVOICE_BLOCKED_GROUP_IDS_FOR_CREATION';
174
174
  const INVOICE_USE_ATTENDANCE_BASED_CALCULATION_CONFIG_ID =
175
175
  'INVOICE_USE_ATTENDANCE_BASED_CALCULATION';
176
+ const INVOICE_STATIC_QUERY_STALE_TIME = 5 * 60 * 1000;
177
+ const INVOICE_STATIC_QUERY_GC_TIME = 10 * 60 * 1000;
178
+
179
+ type InvoiceQueryOptions = {
180
+ enabled?: boolean;
181
+ };
182
+
183
+ function isInvoiceQueryEnabled(options?: InvoiceQueryOptions) {
184
+ return options?.enabled !== false;
185
+ }
176
186
 
177
187
  // ==================== INVOICES DATA FETCHING ====================
178
188
 
@@ -376,34 +386,50 @@ export const useUsersWithSelectableGroups = (wsId: string) => {
376
386
  });
377
387
  };
378
388
 
379
- export const useProducts = (wsId: string) => {
389
+ export const useProducts = (wsId: string, options?: InvoiceQueryOptions) => {
380
390
  return useQuery({
381
391
  queryKey: ['products', wsId],
382
392
  queryFn: () => listInvoiceProductsWithInternalApi(wsId),
393
+ enabled: !!wsId && isInvoiceQueryEnabled(options),
394
+ staleTime: INVOICE_STATIC_QUERY_STALE_TIME,
395
+ gcTime: INVOICE_STATIC_QUERY_GC_TIME,
396
+ refetchOnWindowFocus: false,
383
397
  });
384
398
  };
385
399
 
386
- export const usePromotions = (wsId: string) => {
400
+ export const usePromotions = (wsId: string, options?: InvoiceQueryOptions) => {
387
401
  return useQuery({
388
402
  queryKey: ['promotions', wsId],
389
403
  queryFn: async () => {
390
404
  const data = await listPromotionsWithInternalApi(wsId);
391
405
  return data.filter((promotion) => promotion.promo_type !== 'REFERRAL');
392
406
  },
407
+ enabled: !!wsId && isInvoiceQueryEnabled(options),
408
+ staleTime: INVOICE_STATIC_QUERY_STALE_TIME,
409
+ gcTime: INVOICE_STATIC_QUERY_GC_TIME,
410
+ refetchOnWindowFocus: false,
393
411
  });
394
412
  };
395
413
 
396
- export const useWallets = (wsId: string) => {
414
+ export const useWallets = (wsId: string, options?: InvoiceQueryOptions) => {
397
415
  return useQuery({
398
416
  queryKey: ['wallets', wsId],
399
417
  queryFn: () => listWallets(wsId),
418
+ enabled: !!wsId && isInvoiceQueryEnabled(options),
419
+ staleTime: INVOICE_STATIC_QUERY_STALE_TIME,
420
+ gcTime: INVOICE_STATIC_QUERY_GC_TIME,
421
+ refetchOnWindowFocus: false,
400
422
  });
401
423
  };
402
424
 
403
- export const useCategories = (wsId: string) => {
425
+ export const useCategories = (wsId: string, options?: InvoiceQueryOptions) => {
404
426
  return useQuery({
405
427
  queryKey: ['categories', wsId],
406
428
  queryFn: () => listTransactionCategories(wsId),
429
+ enabled: !!wsId && isInvoiceQueryEnabled(options),
430
+ staleTime: INVOICE_STATIC_QUERY_STALE_TIME,
431
+ gcTime: INVOICE_STATIC_QUERY_GC_TIME,
432
+ refetchOnWindowFocus: false,
407
433
  });
408
434
  };
409
435
 
@@ -511,7 +537,10 @@ export const useSubscriptionInvoiceContext = (
511
537
 
512
538
  // Get workspace config for attendance-based invoice calculation
513
539
  // Returns true if attendance-based calculation should be used (default), false if all sessions should be included
514
- export const useInvoiceAttendanceConfig = (wsId: string) => {
540
+ export const useInvoiceAttendanceConfig = (
541
+ wsId: string,
542
+ options?: InvoiceQueryOptions
543
+ ) => {
515
544
  return useQuery({
516
545
  queryKey: ['invoice-attendance-config', wsId],
517
546
  queryFn: async () => {
@@ -533,7 +562,7 @@ export const useInvoiceAttendanceConfig = (wsId: string) => {
533
562
  return true;
534
563
  }
535
564
  },
536
- enabled: !!wsId,
565
+ enabled: !!wsId && isInvoiceQueryEnabled(options),
537
566
  staleTime: 5 * 60 * 1000, // 5 minutes - config doesn't change often
538
567
  gcTime: 10 * 60 * 1000, // 10 minutes
539
568
  refetchOnWindowFocus: false,
@@ -543,7 +572,10 @@ export const useInvoiceAttendanceConfig = (wsId: string) => {
543
572
 
544
573
  // Get workspace config for allowing promotions for standard invoices
545
574
  // Returns true if promotions are allowed for standard invoices (default), false otherwise
546
- export const useInvoicePromotionConfig = (wsId: string) => {
575
+ export const useInvoicePromotionConfig = (
576
+ wsId: string,
577
+ options?: InvoiceQueryOptions
578
+ ) => {
547
579
  return useQuery({
548
580
  queryKey: ['invoice-promotion-config', wsId],
549
581
  queryFn: async () => {
@@ -565,7 +597,7 @@ export const useInvoicePromotionConfig = (wsId: string) => {
565
597
  return true;
566
598
  }
567
599
  },
568
- enabled: !!wsId,
600
+ enabled: !!wsId && isInvoiceQueryEnabled(options),
569
601
  staleTime: 5 * 60 * 1000, // 5 minutes
570
602
  gcTime: 10 * 60 * 1000, // 10 minutes
571
603
  refetchOnWindowFocus: false,
@@ -575,7 +607,10 @@ export const useInvoicePromotionConfig = (wsId: string) => {
575
607
 
576
608
  // Get workspace config for blocked groups from creating invoices
577
609
  // Returns array of blocked group IDs
578
- export const useInvoiceBlockedGroups = (wsId: string) => {
610
+ export const useInvoiceBlockedGroups = (
611
+ wsId: string,
612
+ options?: InvoiceQueryOptions
613
+ ) => {
579
614
  return useQuery({
580
615
  queryKey: ['invoice-blocked-groups', wsId],
581
616
  queryFn: async () => {
@@ -592,7 +627,7 @@ export const useInvoiceBlockedGroups = (wsId: string) => {
592
627
  return [];
593
628
  }
594
629
  },
595
- enabled: !!wsId,
630
+ enabled: !!wsId && isInvoiceQueryEnabled(options),
596
631
  staleTime: 5 * 60 * 1000, // 5 minutes
597
632
  gcTime: 10 * 60 * 1000, // 10 minutes
598
633
  refetchOnWindowFocus: false,
@@ -601,11 +636,15 @@ export const useInvoiceBlockedGroups = (wsId: string) => {
601
636
  };
602
637
 
603
638
  // Get User's Group Products with improved caching
604
- export const useUserGroupProducts = (wsId: string, groupId: string) => {
639
+ export const useUserGroupProducts = (
640
+ wsId: string,
641
+ groupId: string,
642
+ options?: InvoiceQueryOptions
643
+ ) => {
605
644
  return useQuery({
606
645
  queryKey: ['user-group-products', wsId, groupId],
607
646
  queryFn: () => listUserGroupProductsWithInternalApi(wsId, groupId),
608
- enabled: !!wsId && !!groupId,
647
+ enabled: !!wsId && !!groupId && isInvoiceQueryEnabled(options),
609
648
  staleTime: 5 * 60 * 1000, // 5 minutes
610
649
  gcTime: 10 * 60 * 1000, // 10 minutes
611
650
  refetchOnWindowFocus: false,
@@ -614,14 +653,18 @@ export const useUserGroupProducts = (wsId: string, groupId: string) => {
614
653
  };
615
654
 
616
655
  // Get multiple groups' linked products combined (for multi-group selection)
617
- export const useMultiGroupProducts = (wsId: string, groupIds: string[]) => {
656
+ export const useMultiGroupProducts = (
657
+ wsId: string,
658
+ groupIds: string[],
659
+ options?: InvoiceQueryOptions
660
+ ) => {
618
661
  return useQuery({
619
662
  queryKey: ['multi-group-products', wsId, groupIds],
620
663
  queryFn: async () => {
621
664
  if (groupIds.length === 0) return [];
622
665
  return listMultiGroupProductsWithInternalApi(wsId, groupIds);
623
666
  },
624
- enabled: !!wsId && groupIds.length > 0,
667
+ enabled: !!wsId && groupIds.length > 0 && isInvoiceQueryEnabled(options),
625
668
  staleTime: 5 * 60 * 1000, // 5 minutes
626
669
  gcTime: 10 * 60 * 1000, // 10 minutes
627
670
  refetchOnWindowFocus: false,
@@ -630,11 +673,15 @@ export const useMultiGroupProducts = (wsId: string, groupIds: string[]) => {
630
673
  };
631
674
 
632
675
  // Get User's Linked Promotion
633
- export const useUserLinkedPromotions = (wsId: string, userId: string) => {
676
+ export const useUserLinkedPromotions = (
677
+ wsId: string,
678
+ userId: string,
679
+ options?: InvoiceQueryOptions
680
+ ) => {
634
681
  return useQuery({
635
682
  queryKey: ['user-linked-promotions', wsId, userId],
636
683
  queryFn: () => listUserLinkedPromotionsWithInternalApi(wsId, userId),
637
- enabled: !!wsId && !!userId,
684
+ enabled: !!wsId && !!userId && isInvoiceQueryEnabled(options),
638
685
  staleTime: 5 * 60 * 1000, // 5 minutes
639
686
  gcTime: 10 * 60 * 1000, // 10 minutes
640
687
  refetchOnWindowFocus: false,
@@ -643,7 +690,11 @@ export const useUserLinkedPromotions = (wsId: string, userId: string) => {
643
690
  };
644
691
 
645
692
  // Per-user referral discounts (percent) from view
646
- export const useUserReferralDiscounts = (wsId: string, userId: string) => {
693
+ export const useUserReferralDiscounts = (
694
+ wsId: string,
695
+ userId: string,
696
+ options?: InvoiceQueryOptions
697
+ ) => {
647
698
  return useQuery({
648
699
  queryKey: ['user-referral-discounts', wsId, userId],
649
700
  queryFn: async () => {
@@ -658,7 +709,7 @@ export const useUserReferralDiscounts = (wsId: string, userId: string) => {
658
709
  })) || []
659
710
  );
660
711
  },
661
- enabled: !!wsId && !!userId,
712
+ enabled: !!wsId && !!userId && isInvoiceQueryEnabled(options),
662
713
  staleTime: 5 * 60 * 1000, // 5 minutes
663
714
  gcTime: 10 * 60 * 1000, // 10 minutes
664
715
  refetchOnWindowFocus: false,
@@ -678,7 +729,11 @@ export type AvailablePromotion = {
678
729
  current_uses?: number | null;
679
730
  };
680
731
 
681
- export const useAvailablePromotions = (wsId: string, userId: string) => {
732
+ export const useAvailablePromotions = (
733
+ wsId: string,
734
+ userId: string,
735
+ options?: InvoiceQueryOptions
736
+ ) => {
682
737
  return useQuery({
683
738
  queryKey: ['available-promotions', wsId, userId],
684
739
  queryFn: async () => {
@@ -743,7 +798,7 @@ export const useAvailablePromotions = (wsId: string, userId: string) => {
743
798
 
744
799
  return Array.from(resultMap.values()) as AvailablePromotion[];
745
800
  },
746
- enabled: !!wsId && !!userId,
801
+ enabled: !!wsId && !!userId && isInvoiceQueryEnabled(options),
747
802
  staleTime: 5 * 60 * 1000,
748
803
  gcTime: 10 * 60 * 1000,
749
804
  refetchOnWindowFocus: false,
@@ -0,0 +1,137 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { render, waitFor } from '@testing-library/react';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import NewInvoicePage from './new-invoice-page';
5
+
6
+ const invoiceMocks = vi.hoisted(() => ({
7
+ StandardInvoice: vi.fn(),
8
+ SubscriptionInvoice: vi.fn(),
9
+ }));
10
+
11
+ const nuqsState = vi.hoisted(() => ({
12
+ invoiceType: 'standard' as 'standard' | 'subscription',
13
+ }));
14
+
15
+ vi.mock('next-intl', () => ({
16
+ useTranslations: () => (key: string) => key,
17
+ }));
18
+
19
+ vi.mock('nuqs', () => ({
20
+ useQueryState: (key: string, options?: { defaultValue?: unknown }) => {
21
+ if (key === 'type') {
22
+ return [nuqsState.invoiceType, vi.fn()];
23
+ }
24
+
25
+ if (key === 'amount') {
26
+ return [null, vi.fn()];
27
+ }
28
+
29
+ return [options?.defaultValue ?? '', vi.fn()];
30
+ },
31
+ }));
32
+
33
+ vi.mock('../../../../hooks/use-local-storage', () => ({
34
+ useLocalStorage: (_key: string, initialValue: boolean) => [
35
+ initialValue,
36
+ vi.fn(),
37
+ true,
38
+ ],
39
+ }));
40
+
41
+ vi.mock('./standard-invoice', () => ({
42
+ StandardInvoice: (props: unknown) => {
43
+ invoiceMocks.StandardInvoice(props);
44
+ return null;
45
+ },
46
+ }));
47
+
48
+ vi.mock('./subscription-invoice', () => ({
49
+ SubscriptionInvoice: (props: unknown) => {
50
+ invoiceMocks.SubscriptionInvoice(props);
51
+ return null;
52
+ },
53
+ }));
54
+
55
+ function createQueryClient() {
56
+ return new QueryClient({
57
+ defaultOptions: {
58
+ queries: {
59
+ retry: false,
60
+ },
61
+ },
62
+ });
63
+ }
64
+
65
+ function renderPage() {
66
+ return render(
67
+ <QueryClientProvider client={createQueryClient()}>
68
+ <NewInvoicePage wsId="ws-1" />
69
+ </QueryClientProvider>
70
+ );
71
+ }
72
+
73
+ describe('NewInvoicePage', () => {
74
+ let fetchMock: ReturnType<typeof vi.fn>;
75
+
76
+ beforeEach(() => {
77
+ invoiceMocks.StandardInvoice.mockClear();
78
+ invoiceMocks.SubscriptionInvoice.mockClear();
79
+ nuqsState.invoiceType = 'standard';
80
+ fetchMock = vi.fn().mockResolvedValue({
81
+ ok: true,
82
+ json: async () => ({
83
+ DEFAULT_CURRENCY: 'VND',
84
+ DEFAULT_SUBSCRIPTION_CATEGORY_ID: 'category-1',
85
+ default_wallet_id: 'wallet-1',
86
+ }),
87
+ });
88
+ vi.stubGlobal('fetch', fetchMock);
89
+ });
90
+
91
+ afterEach(() => {
92
+ vi.unstubAllGlobals();
93
+ });
94
+
95
+ it('batches default invoice config reads and mounts only the standard invoice tab', async () => {
96
+ renderPage();
97
+
98
+ await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
99
+
100
+ expect(fetchMock).toHaveBeenCalledWith(
101
+ '/api/v1/workspaces/ws-1/settings/configs?ids=default_wallet_id,DEFAULT_SUBSCRIPTION_CATEGORY_ID,DEFAULT_CURRENCY',
102
+ { cache: 'no-store' }
103
+ );
104
+ expect(invoiceMocks.StandardInvoice).toHaveBeenCalled();
105
+ expect(invoiceMocks.SubscriptionInvoice).not.toHaveBeenCalled();
106
+
107
+ await waitFor(() =>
108
+ expect(invoiceMocks.StandardInvoice).toHaveBeenLastCalledWith(
109
+ expect.objectContaining({
110
+ defaultCurrency: 'VND',
111
+ defaultWalletId: 'wallet-1',
112
+ })
113
+ )
114
+ );
115
+ });
116
+
117
+ it('mounts only the subscription invoice tab when it is active', async () => {
118
+ nuqsState.invoiceType = 'subscription';
119
+
120
+ renderPage();
121
+
122
+ await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
123
+
124
+ expect(invoiceMocks.StandardInvoice).not.toHaveBeenCalled();
125
+ expect(invoiceMocks.SubscriptionInvoice).toHaveBeenCalled();
126
+
127
+ await waitFor(() =>
128
+ expect(invoiceMocks.SubscriptionInvoice).toHaveBeenLastCalledWith(
129
+ expect.objectContaining({
130
+ defaultCategoryId: 'category-1',
131
+ defaultCurrency: 'VND',
132
+ defaultWalletId: 'wallet-1',
133
+ })
134
+ )
135
+ );
136
+ });
137
+ });
@@ -16,20 +16,40 @@ import { useTranslations } from 'next-intl';
16
16
  import { useQueryState } from 'nuqs';
17
17
  import { useState } from 'react';
18
18
  import { useLocalStorage } from '../../../../hooks/use-local-storage';
19
- import { useWorkspaceConfig } from '../../../../hooks/use-workspace-config';
19
+ import { useWorkspaceConfigs } from '../../../../hooks/use-workspace-config';
20
+ import {
21
+ type FinancePermissionRequestUser,
22
+ FinancePermissionWarningDialog,
23
+ } from '../shared/finance-permission-warning-dialog';
20
24
  import { StandardInvoice } from './standard-invoice';
21
25
  import { SubscriptionInvoice } from './subscription-invoice';
22
26
 
27
+ const INVOICE_DEFAULT_CONFIG_IDS = [
28
+ 'default_wallet_id',
29
+ 'DEFAULT_SUBSCRIPTION_CATEGORY_ID',
30
+ 'DEFAULT_CURRENCY',
31
+ ] as const;
32
+
23
33
  interface Props {
24
34
  wsId: string;
35
+ canCreateInvoices?: boolean;
25
36
  canChangeFinanceWallets?: boolean;
26
37
  canSetFinanceWalletsOnCreate?: boolean;
38
+ canReadInvoiceProducts?: boolean;
39
+ canReadInvoiceProductStock?: boolean;
40
+ canReadGroupLinkedProducts?: boolean;
41
+ permissionRequestUser?: FinancePermissionRequestUser | null;
27
42
  }
28
43
 
29
44
  export default function NewInvoicePage({
30
45
  wsId,
46
+ canCreateInvoices = true,
31
47
  canChangeFinanceWallets = true,
32
48
  canSetFinanceWalletsOnCreate = true,
49
+ canReadInvoiceProducts = true,
50
+ canReadInvoiceProductStock = true,
51
+ canReadGroupLinkedProducts = true,
52
+ permissionRequestUser,
33
53
  }: Props) {
34
54
  const t = useTranslations();
35
55
  const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -47,21 +67,15 @@ export default function NewInvoicePage({
47
67
  },
48
68
  });
49
69
 
50
- const { data: defaultWalletId } = useWorkspaceConfig<string>(
51
- wsId,
52
- 'default_wallet_id'
53
- );
54
-
55
- const { data: defaultCategoryId } = useWorkspaceConfig<string>(
56
- wsId,
57
- 'DEFAULT_SUBSCRIPTION_CATEGORY_ID'
58
- );
59
-
60
- const { data: defaultCurrency } = useWorkspaceConfig<'VND' | 'USD'>(
70
+ const { data: defaultConfigs = {} } = useWorkspaceConfigs(
61
71
  wsId,
62
- 'DEFAULT_CURRENCY',
63
- 'USD'
72
+ INVOICE_DEFAULT_CONFIG_IDS
64
73
  );
74
+ const defaultWalletId = defaultConfigs.default_wallet_id ?? undefined;
75
+ const defaultCategoryId =
76
+ defaultConfigs.DEFAULT_SUBSCRIPTION_CATEGORY_ID ?? undefined;
77
+ const defaultCurrency =
78
+ defaultConfigs.DEFAULT_CURRENCY === 'VND' ? 'VND' : 'USD';
65
79
 
66
80
  const [
67
81
  createMultipleInvoices,
@@ -81,13 +95,37 @@ export default function NewInvoicePage({
81
95
  printAfterCreateInitialized &&
82
96
  downloadImageAfterCreateInitialized;
83
97
 
84
- return (
98
+ const pageHeader = (
85
99
  <>
86
100
  <FeatureSummary
87
101
  pluralTitle={t('ws-invoices.new_invoice')}
88
102
  singularTitle={t('ws-invoices.new_invoice')}
89
103
  />
90
104
  <Separator className="my-4" />
105
+ </>
106
+ );
107
+
108
+ if (!canCreateInvoices) {
109
+ return (
110
+ <>
111
+ {pageHeader}
112
+ <FinancePermissionWarningDialog
113
+ defaultOpen
114
+ missingPermissions={['create_invoices']}
115
+ user={permissionRequestUser}
116
+ trigger={
117
+ <Button variant="outline">
118
+ {t('finance-permission-warning.open_request')}
119
+ </Button>
120
+ }
121
+ />
122
+ </>
123
+ );
124
+ }
125
+
126
+ return (
127
+ <>
128
+ {pageHeader}
91
129
  <Tabs
92
130
  value={invoiceType}
93
131
  className="w-full"
@@ -184,30 +222,41 @@ export default function NewInvoicePage({
184
222
  </div>
185
223
 
186
224
  <TabsContent value="standard" className="mt-4">
187
- <StandardInvoice
188
- wsId={wsId}
189
- defaultWalletId={defaultWalletId ?? undefined}
190
- defaultCurrency={defaultCurrency ?? undefined}
191
- canChangeFinanceWallets={canChangeFinanceWallets}
192
- canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
193
- createMultipleInvoices={createMultipleInvoices}
194
- printAfterCreate={printAfterCreate}
195
- downloadImageAfterCreate={downloadImageAfterCreate}
196
- />
225
+ {invoiceType === 'standard' ? (
226
+ <StandardInvoice
227
+ wsId={wsId}
228
+ defaultWalletId={defaultWalletId}
229
+ defaultCurrency={defaultCurrency}
230
+ canChangeFinanceWallets={canChangeFinanceWallets}
231
+ canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
232
+ canReadInvoiceProducts={canReadInvoiceProducts}
233
+ canReadInvoiceProductStock={canReadInvoiceProductStock}
234
+ createMultipleInvoices={createMultipleInvoices}
235
+ printAfterCreate={printAfterCreate}
236
+ downloadImageAfterCreate={downloadImageAfterCreate}
237
+ permissionRequestUser={permissionRequestUser}
238
+ />
239
+ ) : null}
197
240
  </TabsContent>
198
241
  <TabsContent value="subscription" className="mt-4">
199
- <SubscriptionInvoice
200
- wsId={wsId}
201
- prefillAmount={prefillAmount}
202
- defaultWalletId={defaultWalletId ?? undefined}
203
- defaultCategoryId={defaultCategoryId ?? undefined}
204
- defaultCurrency={defaultCurrency ?? undefined}
205
- canChangeFinanceWallets={canChangeFinanceWallets}
206
- canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
207
- createMultipleInvoices={createMultipleInvoices}
208
- printAfterCreate={printAfterCreate}
209
- downloadImageAfterCreate={downloadImageAfterCreate}
210
- />
242
+ {invoiceType === 'subscription' ? (
243
+ <SubscriptionInvoice
244
+ wsId={wsId}
245
+ prefillAmount={prefillAmount}
246
+ defaultWalletId={defaultWalletId}
247
+ defaultCategoryId={defaultCategoryId}
248
+ defaultCurrency={defaultCurrency}
249
+ canChangeFinanceWallets={canChangeFinanceWallets}
250
+ canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
251
+ canReadInvoiceProducts={canReadInvoiceProducts}
252
+ canReadInvoiceProductStock={canReadInvoiceProductStock}
253
+ canReadGroupLinkedProducts={canReadGroupLinkedProducts}
254
+ createMultipleInvoices={createMultipleInvoices}
255
+ printAfterCreate={printAfterCreate}
256
+ downloadImageAfterCreate={downloadImageAfterCreate}
257
+ permissionRequestUser={permissionRequestUser}
258
+ />
259
+ ) : null}
211
260
  </TabsContent>
212
261
  </Tabs>
213
262
  </>
@@ -1,25 +1,12 @@
1
1
  import { render, screen } from '@testing-library/react';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { describe, expect, it, vi } from 'vitest';
3
3
  import { ProductSelection } from './product-selection';
4
4
  import type { Product, SelectedProductItem } from './types';
5
5
 
6
- const mocks = vi.hoisted(() => ({
7
- useFinanceConfidentialVisibility: vi.fn(() => ({
8
- isConfidential: true,
9
- })),
10
- }));
11
-
12
6
  vi.mock('next-intl', () => ({
13
7
  useTranslations: () => (key: string) => key,
14
8
  }));
15
9
 
16
- vi.mock('../shared/use-finance-confidential-visibility', () => ({
17
- FINANCE_HIDDEN_AMOUNT: '•••••',
18
- useFinanceConfidentialVisibility: (
19
- ...args: Parameters<typeof mocks.useFinanceConfidentialVisibility>
20
- ) => mocks.useFinanceConfidentialVisibility(...args),
21
- }));
22
-
23
10
  const product: Product = {
24
11
  category: null,
25
12
  category_id: 'category-1',
@@ -52,14 +39,7 @@ const selectedProducts: SelectedProductItem[] = [
52
39
  ];
53
40
 
54
41
  describe('ProductSelection', () => {
55
- beforeEach(() => {
56
- vi.clearAllMocks();
57
- mocks.useFinanceConfidentialVisibility.mockReturnValue({
58
- isConfidential: true,
59
- });
60
- });
61
-
62
- it('masks selected invoice product prices when finance numbers are hidden', () => {
42
+ it('keeps selected invoice product prices visible on creation flows', () => {
63
43
  render(
64
44
  <ProductSelection
65
45
  products={[product]}
@@ -69,10 +49,12 @@ describe('ProductSelection', () => {
69
49
  />
70
50
  );
71
51
 
52
+ expect(screen.queryByText('•••••')).not.toBeInTheDocument();
53
+ expect(
54
+ screen.getAllByText((content) => /100[,.]000/.test(content)).length
55
+ ).toBeGreaterThanOrEqual(1);
72
56
  expect(
73
- screen.getAllByText((content) => content.includes('•••••')).length
74
- ).toBeGreaterThanOrEqual(2);
75
- expect(screen.queryByText(/100.000/)).not.toBeInTheDocument();
76
- expect(screen.queryByText(/50.000/)).not.toBeInTheDocument();
57
+ screen.getAllByText((content) => /50[,.]000/.test(content)).length
58
+ ).toBeGreaterThanOrEqual(1);
77
59
  });
78
60
  });
@@ -16,10 +16,6 @@ import { Label } from '@tuturuuu/ui/label';
16
16
  import { formatCurrency } from '@tuturuuu/utils/format';
17
17
  import { useTranslations } from 'next-intl';
18
18
  import { useState } from 'react';
19
- import {
20
- FINANCE_HIDDEN_AMOUNT,
21
- useFinanceConfidentialVisibility,
22
- } from '../shared/use-finance-confidential-visibility';
23
19
  import type { Product, ProductInventory, SelectedProductItem } from './types';
24
20
 
25
21
  interface Props {
@@ -41,11 +37,9 @@ export function ProductSelection({
41
37
  currency = 'USD',
42
38
  }: Props) {
43
39
  const t = useTranslations();
44
- const { isConfidential: areNumbersHidden } =
45
- useFinanceConfidentialVisibility();
46
40
  const [selectedProductId, setSelectedProductId] = useState<string>('');
47
41
  const formatVisibleCurrency = (amount: number) =>
48
- areNumbersHidden ? FINANCE_HIDDEN_AMOUNT : formatCurrency(amount, currency);
42
+ formatCurrency(amount, currency);
49
43
 
50
44
  const selectedProduct = products.find((p) => p.id === selectedProductId);
51
45
  const availableInventory =
@@ -283,11 +277,9 @@ interface StockItemProps {
283
277
 
284
278
  function StockItem({ inventory, onAdd, currency = 'USD' }: StockItemProps) {
285
279
  const t = useTranslations();
286
- const { isConfidential: areNumbersHidden } =
287
- useFinanceConfidentialVisibility();
288
280
  const [quantity, setQuantity] = useState(1);
289
281
  const formatVisibleCurrency = (amount: number) =>
290
- areNumbersHidden ? FINANCE_HIDDEN_AMOUNT : formatCurrency(amount, currency);
282
+ formatCurrency(amount, currency);
291
283
 
292
284
  const handleAdd = () => {
293
285
  if (