@tuturuuu/ui 0.34.0 → 0.35.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.35.1](https://github.com/tutur3u/platform/compare/ui-v0.35.0...ui-v0.35.1) (2026-09-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **education:** label satellite navigation in Learn and Teach ([7728149](https://github.com/tutur3u/platform/commit/77281496b86def6f02afaaabea0fb39a89557717)) ([#5536](https://github.com/tutur3u/platform/issues/5536)) ([d76aa49](https://github.com/tutur3u/platform/commit/d76aa49d93303156d900c872ada803c43e1c8530))
9
+ * **finance:** count recurring sessions in subscription previews ([94119f5](https://github.com/tutur3u/platform/commit/94119f5126e2c48d7e096a572c5957610ce67724)) ([#5541](https://github.com/tutur3u/platform/issues/5541)) ([e645a51](https://github.com/tutur3u/platform/commit/e645a51b0c9006cc1baf66e9846c5656b2063220))
10
+
11
+ ## [0.35.0](https://github.com/tutur3u/platform/compare/ui-v0.34.0...ui-v0.35.0) (2026-09-25)
12
+
13
+
14
+ ### Features
15
+
16
+ * **auth:** enforce required account MFA lifecycle ([e08f27d](https://github.com/tutur3u/platform/commit/e08f27dbc082f7d9c951bb0792357638f3dcfb0a)) ([#5448](https://github.com/tutur3u/platform/issues/5448)) ([d7a5ed4](https://github.com/tutur3u/platform/commit/d7a5ed453963e619bd1dc3bd8c8a18c5e027317c))
17
+ * **finance:** add granular invoice audit trail and recovery review ([#5376](https://github.com/tutur3u/platform/issues/5376)) ([b8df666](https://github.com/tutur3u/platform/commit/b8df666804848d96d35ba693d126fe611a3ebf2b))
18
+ * **finance:** add invoice history and recoverable deletion ([3d6dea5](https://github.com/tutur3u/platform/commit/3d6dea5bdbb055073b9b5d5680e391d094874ad2))
19
+ * **finance:** clarify category charts and add promotions management ([88f85ea](https://github.com/tutur3u/platform/commit/88f85eaadb722b42b99537be3c75fabb1715bf09)) ([#5384](https://github.com/tutur3u/platform/issues/5384)) ([b6fd666](https://github.com/tutur3u/platform/commit/b6fd666bccca6aa929166210fb1c6abdb91c1448))
20
+ * **finance:** expand invoice audit review and filters ([c346cac](https://github.com/tutur3u/platform/commit/c346cac5a2dfcdcd944d1fcdfa72f40ec27aa720))
21
+ * **finance:** track tuition coverage and harden invoice exports ([72e07be](https://github.com/tutur3u/platform/commit/72e07bebef58dfee398bcd0d896304d13a2d9aad)) ([#5447](https://github.com/tutur3u/platform/issues/5447)) ([6e822d8](https://github.com/tutur3u/platform/commit/6e822d89d079d9ea8b2677f208c16364cd70d586))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **calendar:** treat working locations as non-reminder events ([454e666](https://github.com/tutur3u/platform/commit/454e666a6465e3debd06260aedc1de7b96ab5d67)) ([#5511](https://github.com/tutur3u/platform/issues/5511)) ([6508ad6](https://github.com/tutur3u/platform/commit/6508ad681aaa174b186022fcbd526e20be682973))
27
+ * **finance:** expose audit amount visibility control ([d3ef238](https://github.com/tutur3u/platform/commit/d3ef2389b1fd189f5b7cd327d1e0b65d03b7b136))
28
+ * **finance:** preserve discount display across invoice views ([11eda37](https://github.com/tutur3u/platform/commit/11eda372d53c60041c676e1e8f9ca0ad06809b13))
29
+ * **finance:** preserve discounts in checkout and saved invoice views ([#5378](https://github.com/tutur3u/platform/issues/5378)) ([2297145](https://github.com/tutur3u/platform/commit/2297145c59d6dbec174e5e22bf69f2fd2b54695b))
30
+ * **finance:** retain actors across invoice audit writes ([75e1139](https://github.com/tutur3u/platform/commit/75e11390aa30493ee1f83f95660cfaf7547e13c0)) ([#5383](https://github.com/tutur3u/platform/issues/5383)) ([a1fbb54](https://github.com/tutur3u/platform/commit/a1fbb54532f652116064ab7d7b8d11b217b2b9d4))
31
+ * **finance:** speed up invoice audit history and improve pagination ([b6c165e](https://github.com/tutur3u/platform/commit/b6c165e88e8888facec7744ee16c1d9378bb90e5)) ([#5380](https://github.com/tutur3u/platform/issues/5380)) ([c829653](https://github.com/tutur3u/platform/commit/c8296532b5282324b28c09d3c06a64894694b46c))
32
+ * **notifications:** replace denied realtime filters with API refresh ([35293ac](https://github.com/tutur3u/platform/commit/35293ac06517c63689780351facec22a93c8da3b))
33
+ * **notifications:** stop invalid user_id realtime filters ([#5377](https://github.com/tutur3u/platform/issues/5377)) ([1c4ab1f](https://github.com/tutur3u/platform/commit/1c4ab1fe9830cfd18fbe98c10b0fbe1d6de168a3))
34
+ * **tasks:** keep synced checklists and board state interactive ([63761b5](https://github.com/tutur3u/platform/commit/63761b59aa8abe99c0e48f1694a1fa0573bdcffa)) ([#5379](https://github.com/tutur3u/platform/issues/5379)) ([c647460](https://github.com/tutur3u/platform/commit/c64746019a2e18798e71f0c5cfbf8bff13a58485))
35
+ * **tasks:** restore checklist controls and board drag UX ([72d2495](https://github.com/tutur3u/platform/commit/72d2495717f7550f2ca4c26234abd2ceebbc280c)) ([#5375](https://github.com/tutur3u/platform/issues/5375)) ([fd9ed4e](https://github.com/tutur3u/platform/commit/fd9ed4ef63272cc2a434ad72aa1081fa641055c2))
36
+ * **ui:** ensure report pagination always consumes text ([2f91cb9](https://github.com/tutur3u/platform/commit/2f91cb9e03cf9fd9432daeb76244e33805bb3da6))
37
+ * **ui:** prevent report pagination from freezing editors ([#5392](https://github.com/tutur3u/platform/issues/5392)) ([efe6e95](https://github.com/tutur3u/platform/commit/efe6e95205134df4fc5b10b9e48b4dafb7dce52b))
38
+
3
39
  ## [0.34.0](https://github.com/tutur3u/platform/compare/ui-v0.33.0...ui-v0.34.0) (2026-09-25)
4
40
 
5
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.34.0",
3
+ "version": "0.35.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -87,13 +87,13 @@
87
87
  "@tiptap/pm": "3.31.3",
88
88
  "@tiptap/react": "3.31.3",
89
89
  "@tiptap/starter-kit": "3.31.3",
90
- "@tuturuuu/ai": "0.15.0",
90
+ "@tuturuuu/ai": "0.16.1",
91
91
  "@tuturuuu/apis": "0.11.4",
92
92
  "@tuturuuu/hooks": "0.1.2",
93
93
  "@tuturuuu/icons": "0.1.1",
94
- "@tuturuuu/internal-api": "0.41.0",
94
+ "@tuturuuu/internal-api": "0.43.0",
95
95
  "@tuturuuu/supabase": "0.5.2",
96
- "@tuturuuu/utils": "0.30.0",
96
+ "@tuturuuu/utils": "0.31.0",
97
97
  "@types/debug": "^4.1.13",
98
98
  "browser-image-compression": "^2.0.2",
99
99
  "class-variance-authority": "^0.7.1",
@@ -154,7 +154,7 @@
154
154
  "@tanstack/react-table": "^9.2.4",
155
155
  "@testing-library/jest-dom": "^7.0.1",
156
156
  "@testing-library/react": "^16.3.3",
157
- "@tuturuuu/types": "0.32.0",
157
+ "@tuturuuu/types": "0.33.0",
158
158
  "@tuturuuu/typescript-config": "0.1.2",
159
159
  "@types/html2canvas": "^1.0.0",
160
160
  "@types/lodash": "^4.17.25",
@@ -26,7 +26,10 @@ vi.mock('@tuturuuu/internal-api', () => ({
26
26
  function renderNavLink(
27
27
  link: ComponentProps<typeof NavLink>['link'],
28
28
  props: Partial<
29
- Pick<ComponentProps<typeof NavLink>, 'onClick' | 'onSubMenuClick'>
29
+ Pick<
30
+ ComponentProps<typeof NavLink>,
31
+ 'isCollapsed' | 'onClick' | 'onSubMenuClick'
32
+ >
30
33
  > = {}
31
34
  ) {
32
35
  const queryClient = new QueryClient({
@@ -42,7 +45,7 @@ function renderNavLink(
42
45
  <NavLink
43
46
  wsId="personal"
44
47
  link={link}
45
- isCollapsed={false}
48
+ isCollapsed={props.isCollapsed ?? false}
46
49
  onClick={props.onClick ?? vi.fn()}
47
50
  onSubMenuClick={props.onSubMenuClick ?? vi.fn()}
48
51
  />
@@ -75,6 +78,18 @@ describe('NavLink', () => {
75
78
  );
76
79
  });
77
80
 
81
+ it('gives collapsed icon links a spoken navigation label', () => {
82
+ renderNavLink(
83
+ { href: '/personal/tasks/boards', title: 'Boards' },
84
+ { isCollapsed: true }
85
+ );
86
+
87
+ expect(screen.getByRole('link', { name: 'Boards' })).toHaveAttribute(
88
+ 'aria-label',
89
+ 'Boards'
90
+ );
91
+ });
92
+
78
93
  it('does not prefetch external routes', () => {
79
94
  renderNavLink({
80
95
  external: true,
@@ -362,6 +362,7 @@ export function NavLink({
362
362
  };
363
363
 
364
364
  const commonProps = {
365
+ 'aria-label': isCollapsed ? title : undefined,
365
366
  className: cn(
366
367
  satelliteNavigationItemClass({
367
368
  isCollapsed,
@@ -373,7 +373,6 @@ export const useInfiniteUserInvoices = (
373
373
  });
374
374
  };
375
375
 
376
- // Subscription-specific hooks
377
376
  export const useUserGroups = (wsId: string, userId: string) => {
378
377
  return useQuery({
379
378
  queryKey: ['user-groups', wsId, userId],
@@ -421,6 +420,7 @@ export const useSubscriptionInvoiceContext = (
421
420
  valid_until?: string | null;
422
421
  created_at?: string | null;
423
422
  }>,
423
+ scheduledSessionsByGroupId: {} as Record<string, string[]>,
424
424
  };
425
425
  }
426
426
 
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mergeBillingSessionDates } from './subscription-billing-groups';
3
+ import { getBillableQuantityForGroupRange, type UserGroup } from './utils';
4
+
5
+ describe('subscription billing session dates', () => {
6
+ it('uses projected classes for scheduled billing when older materialized dates stop before the invoice month', () => {
7
+ const groupId = 'group-1';
8
+ const groups = [
9
+ {
10
+ workspace_user_groups: {
11
+ id: groupId,
12
+ name: 'Class 7',
13
+ sessions: ['2026-05-30'],
14
+ } as NonNullable<UserGroup['workspace_user_groups']>,
15
+ },
16
+ ];
17
+ const planned = [
18
+ '2026-09-05',
19
+ '2026-09-12',
20
+ '2026-09-13',
21
+ '2026-09-19',
22
+ '2026-09-20',
23
+ '2026-09-26',
24
+ '2026-09-27',
25
+ ];
26
+ const billingGroups = mergeBillingSessionDates(groups, {
27
+ [groupId]: planned,
28
+ });
29
+
30
+ expect(groups[0]?.workspace_user_groups?.sessions).toEqual(['2026-05-30']);
31
+ expect(
32
+ getBillableQuantityForGroupRange({
33
+ groupId,
34
+ now: new Date('2026-09-26T10:00:00.000Z'),
35
+ selectedMonth: '2026-09',
36
+ useAttendanceBased: false,
37
+ userAttendance: [
38
+ { date: '2026-09-20', group_id: groupId, status: 'PRESENT' },
39
+ { date: '2026-09-26', group_id: groupId, status: 'PRESENT' },
40
+ ],
41
+ userGroups: billingGroups,
42
+ workspaceTimezone: 'Asia/Ho_Chi_Minh',
43
+ })
44
+ ).toBe(7);
45
+ });
46
+ });
@@ -0,0 +1,45 @@
1
+ import { useMemo } from 'react';
2
+ import type { UserGroup } from './utils';
3
+
4
+ export function mergeBillingSessionDates(
5
+ userGroups: UserGroup[],
6
+ scheduledSessionsByGroupId?: Record<string, string[]>
7
+ ): UserGroup[] {
8
+ return userGroups.map((membership) => {
9
+ const group = membership.workspace_user_groups;
10
+ if (!group) return membership;
11
+ const planned = scheduledSessionsByGroupId?.[group.id] ?? [];
12
+ if (planned.length === 0) return membership;
13
+ const existing = Array.isArray(group.sessions) ? group.sessions : [];
14
+ return {
15
+ ...membership,
16
+ workspace_user_groups: {
17
+ ...group,
18
+ sessions: Array.from(new Set([...existing, ...planned])),
19
+ },
20
+ };
21
+ });
22
+ }
23
+
24
+ export function getGroupsWithScheduleIds(userGroups: UserGroup[]): string[] {
25
+ return userGroups.flatMap(({ workspace_user_groups: group }) =>
26
+ group?.id && Array.isArray(group.sessions) && group.sessions.length > 0
27
+ ? [group.id]
28
+ : []
29
+ );
30
+ }
31
+
32
+ export function useSubscriptionBillingGroups(
33
+ userGroups: UserGroup[],
34
+ scheduledSessionsByGroupId?: Record<string, string[]>
35
+ ) {
36
+ const billingUserGroups = useMemo(
37
+ () => mergeBillingSessionDates(userGroups, scheduledSessionsByGroupId),
38
+ [scheduledSessionsByGroupId, userGroups]
39
+ );
40
+ const groupsWithScheduleIds = useMemo(
41
+ () => getGroupsWithScheduleIds(userGroups),
42
+ [userGroups]
43
+ );
44
+ return { billingUserGroups, groupsWithScheduleIds };
45
+ }
@@ -62,6 +62,7 @@ import { createSubscriptionInvoiceWithInternalApi } from './internal-api';
62
62
  import { formatInvoiceRecalculationDescription } from './invoice-visibility-format';
63
63
  import { ProductSelection } from './product-selection';
64
64
  import { invalidateInvoiceMutationQueries } from './query-invalidation';
65
+ import { useSubscriptionBillingGroups } from './subscription-billing-groups';
65
66
  import type { SubscriptionInvoiceProps } from './subscription-invoice-props';
66
67
  import type { SelectedProductItem } from './types';
67
68
  import {
@@ -193,7 +194,6 @@ export function SubscriptionInvoice({
193
194
  enabled: shouldPreloadPaymentSettings,
194
195
  });
195
196
 
196
- // Blocked groups check
197
197
  const { data: blockedGroupIds = [] } = useInvoiceBlockedGroups(wsId, {
198
198
  enabled: hasSelectedGroups,
199
199
  });
@@ -245,7 +245,6 @@ export function SubscriptionInvoice({
245
245
  const [isCreating, setIsCreating] = useState(false);
246
246
  const [createPromotionOpen, setCreatePromotionOpen] = useState(false);
247
247
 
248
- // Track previous user ID to detect user changes (skip initial mount for reset)
249
248
  const prevUserIdRef = useRef<string | null>(null);
250
249
 
251
250
  const {
@@ -335,6 +334,11 @@ export function SubscriptionInvoice({
335
334
  const userAttendance = subscriptionInvoiceContext?.attendance ?? [];
336
335
  const latestSubscriptionInvoices =
337
336
  subscriptionInvoiceContext?.latestInvoices ?? [];
337
+ const { billingUserGroups, groupsWithScheduleIds } =
338
+ useSubscriptionBillingGroups(
339
+ userGroups,
340
+ subscriptionInvoiceContext?.scheduledSessionsByGroupId
341
+ );
338
342
  const userAttendanceError =
339
343
  subscriptionInvoiceContextError instanceof Error
340
344
  ? subscriptionInvoiceContextError
@@ -404,7 +408,7 @@ export function SubscriptionInvoice({
404
408
  const billableSessions = useMemo(
405
409
  () =>
406
410
  getBillableSessionsForGroupsInRange(
407
- userGroups,
411
+ billingUserGroups,
408
412
  selectedGroupIds,
409
413
  effectiveSelectedMonth,
410
414
  prepaidMonthCount,
@@ -415,7 +419,7 @@ export function SubscriptionInvoice({
415
419
  latestSubscriptionInvoices,
416
420
  prepaidMonthCount,
417
421
  selectedGroupIds,
418
- userGroups,
422
+ billingUserGroups,
419
423
  ]
420
424
  );
421
425
 
@@ -438,12 +442,17 @@ export function SubscriptionInvoice({
438
442
  const monthlySessions = useMemo(
439
443
  () =>
440
444
  getBillableSessionsForGroupsInRange(
441
- userGroups,
445
+ billingUserGroups,
442
446
  selectedGroupIds,
443
447
  effectiveSelectedMonth,
444
448
  prepaidMonthCount
445
449
  ),
446
- [effectiveSelectedMonth, prepaidMonthCount, selectedGroupIds, userGroups]
450
+ [
451
+ billingUserGroups,
452
+ effectiveSelectedMonth,
453
+ prepaidMonthCount,
454
+ selectedGroupIds,
455
+ ]
447
456
  );
448
457
 
449
458
  const {
@@ -537,7 +546,7 @@ export function SubscriptionInvoice({
537
546
  prefillAmount: prefillQuantity,
538
547
  groupProducts,
539
548
  products,
540
- userGroups,
549
+ userGroups: billingUserGroups,
541
550
  useAttendanceBased,
542
551
  userAttendance: billableAttendance,
543
552
  latestSubscriptionInvoices,
@@ -550,7 +559,7 @@ export function SubscriptionInvoice({
550
559
  enabled: true,
551
560
  selectedGroupIds,
552
561
  selectedMonth: effectiveSelectedMonth,
553
- userGroups,
562
+ userGroups: billingUserGroups,
554
563
  groupProducts,
555
564
  subscriptionSelectedProducts,
556
565
  userAttendance: billableAttendance,
@@ -608,8 +617,12 @@ export function SubscriptionInvoice({
608
617
  resetRounding: resetRoundingSubscription,
609
618
  } = useInvoiceRounding(totalBeforeRounding);
610
619
 
611
- // Reset subscription state when user changes (including switching to a different user)
612
620
  useEffect(() => {
621
+ // URL-prefilled customer, group and month are one initial selection.
622
+ if (prevUserIdRef.current === null) {
623
+ prevUserIdRef.current = selectedUserId;
624
+ return;
625
+ }
613
626
  const userChanged = prevUserIdRef.current !== selectedUserId;
614
627
  prevUserIdRef.current = selectedUserId;
615
628
 
@@ -632,20 +645,6 @@ export function SubscriptionInvoice({
632
645
  defaultCategoryId,
633
646
  ]);
634
647
 
635
- const groupsWithScheduleIds = useMemo(() => {
636
- const sessions = (g: (typeof userGroups)[0]) =>
637
- g.workspace_user_groups?.sessions;
638
- return userGroups
639
- .filter(
640
- (g) =>
641
- g.workspace_user_groups?.id &&
642
- Array.isArray(sessions(g)) &&
643
- (sessions(g)?.length ?? 0) > 0
644
- )
645
- .map((g) => g.workspace_user_groups!.id);
646
- }, [userGroups]);
647
-
648
- // Auto-select all groups with schedule when userGroups are loaded
649
648
  useEffect(() => {
650
649
  if (
651
650
  groupsWithScheduleIds.length > 0 &&