@tuturuuu/ui 0.2.0 → 0.3.2
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 +60 -0
- package/package.json +79 -67
- package/src/components/ui/__tests__/avatar.test.tsx +8 -5
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
- package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
- package/src/components/ui/chart.test.tsx +29 -0
- package/src/components/ui/chart.tsx +12 -3
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +396 -2
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +36 -8
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +14 -0
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +5 -0
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +21 -7
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +73 -0
- package/src/components/ui/chat/chat-agent-details-utils.tsx +100 -26
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +517 -0
- package/src/components/ui/chat/chat-workspace.tsx +31 -1
- package/src/components/ui/chat/hooks-messages.test.tsx +45 -1
- package/src/components/ui/chat/hooks-messages.ts +1 -1
- package/src/components/ui/chat/hooks-realtime.ts +13 -16
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
- package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
- package/src/components/ui/custom/common-footer.tsx +16 -1
- package/src/components/ui/custom/production-indicator.tsx +1 -1
- package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
- package/src/components/ui/custom/settings/task-settings.tsx +18 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
- package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
- package/src/components/ui/custom/sidebar-context.tsx +61 -61
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
- package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
- package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
- package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
- package/src/components/ui/custom/workspace-select.tsx +33 -12
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
- package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
- package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
- package/src/components/ui/finance/invoices/hooks.ts +75 -20
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
- package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
- package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
- package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
- package/src/components/ui/finance/invoices/utils.test.ts +50 -0
- package/src/components/ui/finance/invoices/utils.ts +75 -17
- package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/form.test.tsx +43 -0
- package/src/components/ui/finance/transactions/form.tsx +60 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
- package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
- package/src/components/ui/legacy/meet/page.test.ts +180 -0
- package/src/components/ui/legacy/meet/page.tsx +87 -39
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
- package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
- package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
- package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
- package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
- package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
- package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
- package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
- package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
- package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
- package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
- package/src/hooks/use-calendar-sync.tsx +22 -277
- package/src/hooks/use-calendar.tsx +95 -525
- package/src/hooks/use-semantic-task-search.ts +10 -33
- package/src/hooks/use-task-actions.ts +43 -117
- package/src/hooks/use-user-config.ts +1 -1
- package/src/hooks/use-workspace-config.ts +6 -2
- package/src/hooks/use-workspace-presence.ts +1 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
|
@@ -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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
74
|
-
).toBeGreaterThanOrEqual(
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
formatCurrency(amount, currency);
|
|
291
283
|
|
|
292
284
|
const handleAdd = () => {
|
|
293
285
|
if (
|