@tuturuuu/ui 0.10.0 → 0.11.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 (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +47 -47
  3. package/src/components/ui/finance/invoices/components/subscription-attendance-summary.tsx +19 -5
  4. package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.test.tsx +41 -0
  5. package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.tsx +93 -0
  6. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +82 -70
  7. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +50 -25
  8. package/src/components/ui/finance/invoices/hooks.ts +11 -2
  9. package/src/components/ui/finance/invoices/internal-api.ts +5 -0
  10. package/src/components/ui/finance/invoices/subscription-invoice.tsx +92 -31
  11. package/src/components/ui/finance/invoices/utils.test.ts +82 -0
  12. package/src/components/ui/finance/invoices/utils.ts +240 -7
  13. package/src/components/ui/text-editor/__tests__/extensions.test.ts +22 -0
  14. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -0
  15. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +37 -0
  16. package/src/components/ui/tu-do/shared/board-client.tsx +3 -1
  17. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +55 -1
  18. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -0
  19. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +18 -2
  20. package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.test.ts +97 -0
  21. package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.ts +210 -0
  22. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +56 -8
  23. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.test.tsx +63 -0
  24. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.tsx +218 -0
  25. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-restore-banner.tsx +83 -0
  26. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.test.tsx +120 -0
  27. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.tsx +180 -0
  28. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +157 -4
  29. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +178 -14
  30. package/src/components/ui/tu-do/shared/task-edit-dialog/utils.test.ts +100 -0
  31. package/src/components/ui/tu-do/shared/task-edit-dialog/utils.ts +109 -0
  32. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +381 -34
  33. package/src/components/ui/tu-do/templates/task-template-api.ts +142 -0
  34. package/src/components/ui/tu-do/templates/task-template-card.tsx +118 -0
  35. package/src/components/ui/tu-do/templates/task-template-client.test.tsx +52 -0
  36. package/src/components/ui/tu-do/templates/task-template-client.tsx +258 -0
  37. package/src/components/ui/tu-do/templates/task-template-dialogs.test.tsx +167 -0
  38. package/src/components/ui/tu-do/templates/task-template-dialogs.tsx +376 -0
  39. package/src/components/ui/tu-do/templates/task-templates-hub.test.tsx +114 -0
  40. package/src/components/ui/tu-do/templates/task-templates-hub.tsx +50 -0
  41. package/src/components/ui/tu-do/templates/task-templates-page.tsx +6 -11
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.1](https://github.com/tutur3u/platform/compare/ui-v0.11.0...ui-v0.11.1) (2026-07-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **cli:** preserve markdown tables in task descriptions ([9faf8ba](https://github.com/tutur3u/platform/commit/9faf8ba44b92565a9dfc2949c496654fc1fea9b3))
9
+ * **tasks:** refresh task template placeholders ([0c3c309](https://github.com/tutur3u/platform/commit/0c3c30935f67e3e64d548c8a13584fd597e887b8))
10
+ * **ui:** stabilize task property popover handoff ([8fb2e62](https://github.com/tutur3u/platform/commit/8fb2e62904d34a3ae7894bfbbb66c285fcf1a1bd))
11
+
12
+ ## [0.11.0](https://github.com/tutur3u/platform/compare/ui-v0.10.0...ui-v0.11.0) (2026-06-29)
13
+
14
+
15
+ ### Features
16
+
17
+ * **finance:** support prepaid subscription invoice ranges ([fa0f338](https://github.com/tutur3u/platform/commit/fa0f3388dfb3d3ea539a8d8bdc3f100f268542c6))
18
+ * **tasks:** add board header loading skeleton ([73f1242](https://github.com/tutur3u/platform/commit/73f12421a9cd50c035a1c9940e152354b1974f26))
19
+ * **tasks:** add task templates ([8d0700a](https://github.com/tutur3u/platform/commit/8d0700ad255c7b5874bfa065575df6b1cde34063))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **tasks:** pad board header skeleton ([b0c5c0f](https://github.com/tutur3u/platform/commit/b0c5c0fea8871cac2603da6239b980c752b1c9f6))
25
+ * **tasks:** prevent description wipes during yjs hydration ([947f38c](https://github.com/tutur3u/platform/commit/947f38c6474d07b1fac1c44e166638bfc87260c0))
26
+ * **tasks:** restore tracked task descriptions ([f892ae2](https://github.com/tutur3u/platform/commit/f892ae23dfec41c2d25649b97a628d8cdcd1fa5d))
27
+
3
28
  ## [0.10.0](https://github.com/tutur3u/platform/compare/ui-v0.9.0...ui-v0.10.0) (2026-06-26)
4
29
 
5
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,42 +24,42 @@
24
24
  "@hookform/resolvers": "^5.4.0",
25
25
  "@json-render/core": "^0.19.0",
26
26
  "@json-render/react": "^0.19.0",
27
- "@radix-ui/react-accordion": "^1.2.14",
28
- "@radix-ui/react-alert-dialog": "^1.1.17",
29
- "@radix-ui/react-aspect-ratio": "^1.1.10",
30
- "@radix-ui/react-avatar": "^1.2.0",
31
- "@radix-ui/react-checkbox": "^1.3.5",
32
- "@radix-ui/react-collapsible": "^1.1.14",
33
- "@radix-ui/react-context-menu": "^2.3.1",
34
- "@radix-ui/react-dialog": "^1.1.17",
35
- "@radix-ui/react-dropdown-menu": "^2.1.18",
36
- "@radix-ui/react-hover-card": "^1.1.17",
37
- "@radix-ui/react-label": "^2.1.10",
38
- "@radix-ui/react-menubar": "^1.1.18",
39
- "@radix-ui/react-navigation-menu": "^1.2.16",
40
- "@radix-ui/react-popover": "^1.1.17",
41
- "@radix-ui/react-progress": "^1.1.10",
42
- "@radix-ui/react-radio-group": "^1.4.1",
43
- "@radix-ui/react-scroll-area": "^1.2.12",
44
- "@radix-ui/react-select": "^2.3.1",
45
- "@radix-ui/react-separator": "^1.1.10",
46
- "@radix-ui/react-slider": "^1.4.1",
27
+ "@radix-ui/react-accordion": "^1.2.15",
28
+ "@radix-ui/react-alert-dialog": "^1.1.18",
29
+ "@radix-ui/react-aspect-ratio": "^1.1.11",
30
+ "@radix-ui/react-avatar": "^1.2.1",
31
+ "@radix-ui/react-checkbox": "^1.3.6",
32
+ "@radix-ui/react-collapsible": "^1.1.15",
33
+ "@radix-ui/react-context-menu": "^2.3.2",
34
+ "@radix-ui/react-dialog": "^1.1.18",
35
+ "@radix-ui/react-dropdown-menu": "^2.1.19",
36
+ "@radix-ui/react-hover-card": "^1.1.18",
37
+ "@radix-ui/react-label": "^2.1.11",
38
+ "@radix-ui/react-menubar": "^1.1.19",
39
+ "@radix-ui/react-navigation-menu": "^1.2.17",
40
+ "@radix-ui/react-popover": "^1.1.18",
41
+ "@radix-ui/react-progress": "^1.1.11",
42
+ "@radix-ui/react-radio-group": "^1.4.2",
43
+ "@radix-ui/react-scroll-area": "^1.2.13",
44
+ "@radix-ui/react-select": "^2.3.2",
45
+ "@radix-ui/react-separator": "^1.1.11",
46
+ "@radix-ui/react-slider": "^1.4.2",
47
47
  "@radix-ui/react-slot": "^1.3.0",
48
- "@radix-ui/react-switch": "^1.3.1",
49
- "@radix-ui/react-tabs": "^1.1.15",
50
- "@radix-ui/react-toast": "^1.2.17",
51
- "@radix-ui/react-toggle": "^1.1.12",
52
- "@radix-ui/react-toggle-group": "^1.1.13",
53
- "@radix-ui/react-tooltip": "^1.2.10",
48
+ "@radix-ui/react-switch": "^1.3.2",
49
+ "@radix-ui/react-tabs": "^1.1.16",
50
+ "@radix-ui/react-toast": "^1.2.18",
51
+ "@radix-ui/react-toggle": "^1.1.13",
52
+ "@radix-ui/react-toggle-group": "^1.1.14",
53
+ "@radix-ui/react-tooltip": "^1.2.11",
54
54
  "@streamdown/cjk": "^1.0.3",
55
55
  "@streamdown/code": "^1.1.1",
56
56
  "@streamdown/math": "^1.0.2",
57
57
  "@streamdown/mermaid": "^1.0.2",
58
58
  "@tanstack/react-hotkeys": "^0.10.0",
59
59
  "@tanstack/react-pacer": "^0.22.1",
60
- "@tanstack/react-query": "^5.101.1",
60
+ "@tanstack/react-query": "^5.101.2",
61
61
  "@tanstack/react-table": "^8.21.3",
62
- "@tanstack/react-virtual": "^3.14.3",
62
+ "@tanstack/react-virtual": "^3.14.5",
63
63
  "@tiptap/core": "3.27.1",
64
64
  "@tiptap/extension-collaboration": "3.27.1",
65
65
  "@tiptap/extension-collaboration-caret": "3.27.1",
@@ -88,9 +88,9 @@
88
88
  "@tuturuuu/apis": "0.7.0",
89
89
  "@tuturuuu/hooks": "0.0.2",
90
90
  "@tuturuuu/icons": "0.0.6",
91
- "@tuturuuu/internal-api": "0.11.0",
91
+ "@tuturuuu/internal-api": "0.13.0",
92
92
  "@tuturuuu/supabase": "0.4.0",
93
- "@tuturuuu/utils": "0.10.0",
93
+ "@tuturuuu/utils": "0.12.0",
94
94
  "@types/debug": "^4.1.13",
95
95
  "browser-image-compression": "^2.0.2",
96
96
  "class-variance-authority": "^0.7.1",
@@ -102,18 +102,18 @@
102
102
  "debug": "^4.4.3",
103
103
  "embla-carousel-react": "^8.6.0",
104
104
  "eventemitter3": "^5.0.4",
105
- "framer-motion": "^12.41.0",
105
+ "framer-motion": "^12.42.2",
106
106
  "gsap": "^3.15.0",
107
- "html2canvas-pro": "^2.1.1",
107
+ "html2canvas-pro": "^2.2.1",
108
108
  "input-otp": "^1.4.2",
109
109
  "katex": "^0.17.0",
110
110
  "lodash": "4.18.1",
111
111
  "moment": "^2.30.1",
112
- "motion": "^12.41.0",
113
- "next": "^16.2.9",
114
- "next-intl": "^4.13.0",
112
+ "motion": "^12.42.2",
113
+ "next": "^16.3.0-preview.5",
114
+ "next-intl": "^4.13.1",
115
115
  "next-themes": "^0.4.6",
116
- "nuqs": "^2.8.9",
116
+ "nuqs": "^2.9.0",
117
117
  "prosemirror-state": "^1.4.4",
118
118
  "qrcode.react": "^4.2.0",
119
119
  "react": "^19.2.7",
@@ -126,9 +126,9 @@
126
126
  "react-papaparse": "^4.4.0",
127
127
  "react-pdf": "^10.4.1",
128
128
  "react-resizable": "^4.0.2",
129
- "react-resizable-panels": "^4.11.0",
129
+ "react-resizable-panels": "^4.12.0",
130
130
  "react-syntax-highlighter": "^16.1.1",
131
- "recharts": "3.9.0",
131
+ "recharts": "3.9.1",
132
132
  "sonner": "^2.0.7",
133
133
  "streamdown": "^2.5.0",
134
134
  "tailwind-scrollbar": "^4.0.2",
@@ -143,34 +143,34 @@
143
143
  "zod": "^4.4.3"
144
144
  },
145
145
  "devDependencies": {
146
- "@tailwindcss/postcss": "^4.3.1",
146
+ "@tailwindcss/postcss": "^4.3.2",
147
147
  "@tailwindcss/typography": "^0.5.20",
148
- "@tanstack/react-query": "^5.101.1",
148
+ "@tanstack/react-query": "^5.101.2",
149
149
  "@tanstack/react-table": "^8.21.3",
150
150
  "@testing-library/jest-dom": "^6.9.1",
151
151
  "@testing-library/react": "^16.3.2",
152
- "@tuturuuu/types": "0.12.0",
152
+ "@tuturuuu/types": "0.13.1",
153
153
  "@tuturuuu/typescript-config": "0.1.1",
154
154
  "@types/html2canvas": "^1.0.0",
155
155
  "@types/lodash": "^4.17.24",
156
- "@types/node": "^26.0.0",
156
+ "@types/node": "^26.0.1",
157
157
  "@types/react": "^19.2.17",
158
158
  "@types/react-dom": "^19.2.3",
159
159
  "@types/react-resizable": "^4.0.0",
160
160
  "@types/react-syntax-highlighter": "^15.5.13",
161
161
  "@types/uuid": "^11.0.0",
162
162
  "@vitejs/plugin-react": "^6.0.3",
163
- "autoprefixer": "^10.5.0",
163
+ "autoprefixer": "^10.5.2",
164
164
  "jsdom": "^29.1.1",
165
- "postcss": "^8.5.14",
165
+ "postcss": "^8.5.16",
166
166
  "postcss-load-config": "^6.0.1",
167
- "tailwindcss": "^4.3.1",
167
+ "tailwindcss": "^4.3.2",
168
168
  "typescript": "7.0.1-rc",
169
169
  "uuid": "^14.0.1",
170
170
  "vitest": "^4.1.9"
171
171
  },
172
172
  "peerDependencies": {
173
- "@tanstack/react-query": "^5.101.1",
173
+ "@tanstack/react-query": "^5.101.2",
174
174
  "@tanstack/react-table": "^8.21.3"
175
175
  },
176
176
  "exports": {
@@ -54,6 +54,8 @@ interface SubscriptionAttendanceSummaryProps {
54
54
  };
55
55
  totalSessions: number;
56
56
  attendanceRate: number;
57
+ coverageRangeLabel: string;
58
+ prepaidMonthCount: number;
57
59
  }
58
60
 
59
61
  export function SubscriptionAttendanceSummary({
@@ -73,6 +75,8 @@ export function SubscriptionAttendanceSummary({
73
75
  attendanceStats,
74
76
  totalSessions,
75
77
  attendanceRate,
78
+ coverageRangeLabel,
79
+ prepaidMonthCount,
76
80
  }: SubscriptionAttendanceSummaryProps): React.ReactElement {
77
81
  const t = useTranslations();
78
82
 
@@ -82,6 +86,8 @@ export function SubscriptionAttendanceSummary({
82
86
  ? selectedMonth
83
87
  : (availableMonths[0]?.value ?? selectedMonth);
84
88
  const monthLabel = formatMonthLabel(resolvedSelectedMonth, locale);
89
+ const displayedRangeLabel =
90
+ prepaidMonthCount > 1 ? coverageRangeLabel : monthLabel;
85
91
 
86
92
  return (
87
93
  <Card>
@@ -98,17 +104,25 @@ export function SubscriptionAttendanceSummary({
98
104
  <CardDescription className="flex flex-col gap-1">
99
105
  {selectedGroupIds.length === 1
100
106
  ? t('ws-invoices.attendance_for_month', {
101
- month: monthLabel,
107
+ month: displayedRangeLabel,
102
108
  })
103
109
  : t('ws-invoices.combined_attendance_for_month', {
104
110
  count: selectedGroupIds.length,
105
- month: monthLabel,
106
- default: `Combined attendance from ${selectedGroupIds.length} groups for ${monthLabel}`,
111
+ month: displayedRangeLabel,
112
+ default: `Combined attendance from ${selectedGroupIds.length} groups for ${displayedRangeLabel}`,
107
113
  })}
108
114
  <span className="text-muted-foreground text-xs">
109
115
  {isSelectedMonthPaid
110
- ? t('ws-invoices.all_groups_paid_for_month')
111
- : t('ws-invoices.some_groups_unpaid_for_month')}
116
+ ? t(
117
+ prepaidMonthCount > 1
118
+ ? 'ws-invoices.all_groups_paid_for_range'
119
+ : 'ws-invoices.all_groups_paid_for_month'
120
+ )
121
+ : t(
122
+ prepaidMonthCount > 1
123
+ ? 'ws-invoices.some_groups_unpaid_for_range'
124
+ : 'ws-invoices.some_groups_unpaid_for_month'
125
+ )}
112
126
  </span>
113
127
  </CardDescription>
114
128
  </div>
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { SubscriptionPrepaidControls } from './subscription-prepaid-controls';
4
+
5
+ vi.mock('next-intl', () => ({
6
+ useTranslations: () => (key: string, values?: Record<string, unknown>) => {
7
+ const messages: Record<string, string> = {
8
+ 'ws-invoices.coverage_range': 'Coverage',
9
+ 'ws-invoices.prepaid_month_count': 'Months to cover',
10
+ 'ws-invoices.prepaid_month_count_many': '{count} months',
11
+ 'ws-invoices.prepaid_month_count_one': '1 month',
12
+ 'ws-invoices.prepaid_months': 'Prepaid months',
13
+ 'ws-invoices.prepaid_months_description':
14
+ 'Choose how many months this subscription invoice should cover.',
15
+ 'ws-invoices.valid_until': 'Valid until',
16
+ };
17
+
18
+ return (messages[key] ?? key).replace(
19
+ '{count}',
20
+ String(values?.count ?? '')
21
+ );
22
+ },
23
+ }));
24
+
25
+ describe('SubscriptionPrepaidControls', () => {
26
+ it('renders the prepaid count selector and coverage summary', () => {
27
+ render(
28
+ <SubscriptionPrepaidControls
29
+ coverageRangeLabel="June 2026 - August 2026"
30
+ prepaidMonthCount={3}
31
+ validUntilLabel="September 2026"
32
+ onPrepaidMonthCountChange={vi.fn()}
33
+ />
34
+ );
35
+
36
+ expect(screen.getByText('Prepaid months')).toBeInTheDocument();
37
+ expect(screen.getByText('Months to cover')).toBeInTheDocument();
38
+ expect(screen.getByText('June 2026 - August 2026')).toBeInTheDocument();
39
+ expect(screen.getByText('September 2026')).toBeInTheDocument();
40
+ });
41
+ });
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '@tuturuuu/ui/card';
10
+ import { Label } from '@tuturuuu/ui/label';
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from '@tuturuuu/ui/select';
18
+ import { useTranslations } from 'next-intl';
19
+ import type React from 'react';
20
+ import { MAX_PREPAID_MONTH_COUNT } from '../utils';
21
+
22
+ interface SubscriptionPrepaidControlsProps {
23
+ coverageRangeLabel: string;
24
+ onPrepaidMonthCountChange: (monthCount: number) => void;
25
+ prepaidMonthCount: number;
26
+ validUntilLabel: string;
27
+ }
28
+
29
+ const PREPAID_MONTH_OPTIONS = Array.from(
30
+ { length: MAX_PREPAID_MONTH_COUNT },
31
+ (_, index) => index + 1
32
+ );
33
+
34
+ export function SubscriptionPrepaidControls({
35
+ coverageRangeLabel,
36
+ onPrepaidMonthCountChange,
37
+ prepaidMonthCount,
38
+ validUntilLabel,
39
+ }: SubscriptionPrepaidControlsProps): React.ReactElement {
40
+ const t = useTranslations();
41
+
42
+ return (
43
+ <Card>
44
+ <CardHeader>
45
+ <CardTitle>{t('ws-invoices.prepaid_months')}</CardTitle>
46
+ <CardDescription>
47
+ {t('ws-invoices.prepaid_months_description')}
48
+ </CardDescription>
49
+ </CardHeader>
50
+ <CardContent className="space-y-4">
51
+ <div className="space-y-2">
52
+ <Label htmlFor="prepaid-month-count">
53
+ {t('ws-invoices.prepaid_month_count')}
54
+ </Label>
55
+ <Select
56
+ value={String(prepaidMonthCount)}
57
+ onValueChange={(value) => onPrepaidMonthCountChange(Number(value))}
58
+ >
59
+ <SelectTrigger id="prepaid-month-count">
60
+ <SelectValue />
61
+ </SelectTrigger>
62
+ <SelectContent>
63
+ {PREPAID_MONTH_OPTIONS.map((monthCount) => (
64
+ <SelectItem key={monthCount} value={String(monthCount)}>
65
+ {monthCount === 1
66
+ ? t('ws-invoices.prepaid_month_count_one')
67
+ : t('ws-invoices.prepaid_month_count_many', {
68
+ count: monthCount,
69
+ })}
70
+ </SelectItem>
71
+ ))}
72
+ </SelectContent>
73
+ </Select>
74
+ </div>
75
+
76
+ <div className="grid gap-3 rounded-lg border bg-muted/30 p-3 text-sm">
77
+ <div className="flex items-center justify-between gap-3">
78
+ <span className="text-muted-foreground">
79
+ {t('ws-invoices.coverage_range')}
80
+ </span>
81
+ <span className="text-right font-medium">{coverageRangeLabel}</span>
82
+ </div>
83
+ <div className="flex items-center justify-between gap-3">
84
+ <span className="text-muted-foreground">
85
+ {t('ws-invoices.valid_until')}
86
+ </span>
87
+ <span className="text-right font-medium">{validUntilLabel}</span>
88
+ </div>
89
+ </div>
90
+ </CardContent>
91
+ </Card>
92
+ );
93
+ }
@@ -16,14 +16,9 @@ import type {
16
16
  } from '../types';
17
17
  import type { UserGroup } from '../utils';
18
18
  import {
19
- getEffectiveAttendanceDays,
19
+ getBillableQuantityMapForGroupsRange,
20
20
  getEffectiveDays,
21
- getSessionsForMonth,
22
- getSessionsUntilMonth,
23
- getSubscriptionCoverageInvoiceForGroup,
24
21
  getTotalSessionsForGroups,
25
- isSubscriptionMonthPaidForGroup,
26
- parseLocalCalendarDate,
27
22
  } from '../utils';
28
23
 
29
24
  interface UseSubscriptionAutoSelectionProps {
@@ -42,6 +37,7 @@ interface UseSubscriptionAutoSelectionProps {
42
37
  created_at?: string | null;
43
38
  }[];
44
39
  onSelectedProductsChange: Dispatch<SetStateAction<SelectedProductItem[]>>;
40
+ prepaidMonthCount?: number;
45
41
  }
46
42
 
47
43
  export type UseSubscriptionAutoSelectionResult = undefined;
@@ -165,54 +161,59 @@ const computeGroupAttendanceDaysMap = (
165
161
  }[],
166
162
  selectedMonth: string,
167
163
  userAttendance: { status: string; date: string; group_id?: string }[],
168
- useAttendanceBased: boolean
164
+ useAttendanceBased: boolean,
165
+ prepaidMonthCount = 1
169
166
  ) => {
170
- const groupAttendanceDaysMap: Record<string, number> = {};
171
-
172
- for (const groupId of selectedGroupIds) {
173
- const group = userGroups.find(
174
- (g) => g.workspace_user_groups?.id === groupId
175
- );
176
- if (!group) continue;
177
-
178
- const latestInvoice = getSubscriptionCoverageInvoiceForGroup(
179
- latestSubscriptionInvoices,
180
- groupId
181
- );
182
- const validUntil = latestInvoice?.valid_until
183
- ? parseLocalCalendarDate(latestInvoice.valid_until)
184
- : null;
167
+ return getBillableQuantityMapForGroupsRange({
168
+ groupIds: selectedGroupIds,
169
+ latestInvoices: latestSubscriptionInvoices,
170
+ prepaidMonthCount,
171
+ selectedMonth,
172
+ useAttendanceBased,
173
+ userAttendance,
174
+ userGroups,
175
+ });
176
+ };
185
177
 
186
- const isGroupPaid = isSubscriptionMonthPaidForGroup(
187
- groupId,
188
- selectedMonth,
189
- latestSubscriptionInvoices
190
- );
178
+ const sumQuantityMap = (quantityMap: Record<string, number>) =>
179
+ Object.values(quantityMap).reduce((total, value) => total + value, 0);
191
180
 
192
- const sessionsArray = group.workspace_user_groups?.sessions || [];
193
- const groupSessions = isGroupPaid
194
- ? 0
195
- : validUntil
196
- ? getSessionsUntilMonth(sessionsArray, selectedMonth, validUntil)
197
- : getSessionsForMonth(sessionsArray, selectedMonth);
198
-
199
- if (useAttendanceBased) {
200
- const groupAttendance = isGroupPaid
201
- ? []
202
- : userAttendance.filter((a) => {
203
- if (a.group_id !== groupId) return false;
204
- if (!validUntil) return true;
205
- const attendanceDate = parseLocalCalendarDate(a.date);
206
- return attendanceDate >= validUntil;
207
- });
208
- groupAttendanceDaysMap[groupId] =
209
- getEffectiveAttendanceDays(groupAttendance);
210
- } else {
211
- groupAttendanceDaysMap[groupId] = groupSessions;
212
- }
181
+ const computeFallbackAttendanceDays = ({
182
+ groupAttendanceDaysMap,
183
+ latestSubscriptionInvoices,
184
+ prepaidMonthCount,
185
+ selectedGroupIds,
186
+ selectedMonth,
187
+ useAttendanceBased,
188
+ userAttendance,
189
+ userGroups,
190
+ }: {
191
+ groupAttendanceDaysMap: Record<string, number>;
192
+ latestSubscriptionInvoices: {
193
+ group_id?: string;
194
+ valid_until?: string | null;
195
+ created_at?: string | null;
196
+ }[];
197
+ prepaidMonthCount: number;
198
+ selectedGroupIds: string[];
199
+ selectedMonth: string;
200
+ useAttendanceBased: boolean;
201
+ userAttendance: { status: string; date: string; group_id?: string }[];
202
+ userGroups: UserGroup[];
203
+ }): number => {
204
+ const mappedQuantity = sumQuantityMap(groupAttendanceDaysMap);
205
+ if (mappedQuantity > 0 || prepaidMonthCount > 1) {
206
+ return mappedQuantity;
213
207
  }
214
208
 
215
- return groupAttendanceDaysMap;
209
+ const totalSessions = getTotalSessionsForGroups(
210
+ userGroups,
211
+ selectedGroupIds,
212
+ selectedMonth,
213
+ latestSubscriptionInvoices
214
+ );
215
+
216
+ return getEffectiveDays(userAttendance, totalSessions, useAttendanceBased);
216
217
  };
217
218
 
218
219
  export function useSubscriptionAutoSelection({
@@ -227,6 +228,7 @@ export function useSubscriptionAutoSelection({
227
228
  userAttendance,
228
229
  latestSubscriptionInvoices,
229
230
  onSelectedProductsChange,
231
+ prepaidMonthCount = 1,
230
232
  }: UseSubscriptionAutoSelectionProps): UseSubscriptionAutoSelectionResult {
231
233
  const t = useTranslations();
232
234
  const previousGroupIdRef = useRef<string>('');
@@ -248,7 +250,8 @@ export function useSubscriptionAutoSelection({
248
250
  latestSubscriptionInvoices,
249
251
  selectedMonth,
250
252
  userAttendance,
251
- useAttendanceBased
253
+ useAttendanceBased,
254
+ prepaidMonthCount
252
255
  ),
253
256
  [
254
257
  sortedSelectedGroupIds,
@@ -257,6 +260,7 @@ export function useSubscriptionAutoSelection({
257
260
  selectedMonth,
258
261
  userAttendance,
259
262
  useAttendanceBased,
263
+ prepaidMonthCount,
260
264
  ]
261
265
  );
262
266
 
@@ -307,18 +311,22 @@ export function useSubscriptionAutoSelection({
307
311
 
308
312
  fallbackToastShownRef.current = false;
309
313
 
310
- const totalSessions = getTotalSessionsForGroups(
311
- userGroups,
312
- sortedSelectedGroupIds,
313
- selectedMonth,
314
- latestSubscriptionInvoices
315
- );
316
-
317
314
  const shouldUsePrefill =
318
- prefillAmount != null && !initialPrefillUsedRef.current;
315
+ prepaidMonthCount === 1 &&
316
+ prefillAmount != null &&
317
+ !initialPrefillUsedRef.current;
319
318
  const attendanceDays = shouldUsePrefill
320
319
  ? prefillAmount
321
- : getEffectiveDays(userAttendance, totalSessions, useAttendanceBased);
320
+ : computeFallbackAttendanceDays({
321
+ groupAttendanceDaysMap,
322
+ latestSubscriptionInvoices,
323
+ prepaidMonthCount,
324
+ selectedGroupIds: sortedSelectedGroupIds,
325
+ selectedMonth,
326
+ useAttendanceBased,
327
+ userAttendance,
328
+ userGroups,
329
+ });
322
330
 
323
331
  updateSelectedProducts(
324
332
  attendanceDays,
@@ -340,6 +348,7 @@ export function useSubscriptionAutoSelection({
340
348
  latestSubscriptionInvoices,
341
349
  updateSelectedProducts,
342
350
  groupAttendanceDaysMap,
351
+ prepaidMonthCount,
343
352
  ]);
344
353
 
345
354
  // Auto-add group products based on attendance when group is selected
@@ -352,22 +361,24 @@ export function useSubscriptionAutoSelection({
352
361
  return;
353
362
  }
354
363
 
355
- if (prefillAmount != null && !initialPrefillUsedRef.current) {
364
+ if (
365
+ prepaidMonthCount === 1 &&
366
+ prefillAmount != null &&
367
+ !initialPrefillUsedRef.current
368
+ ) {
356
369
  return;
357
370
  }
358
371
 
359
- const totalSessions = getTotalSessionsForGroups(
360
- userGroups,
361
- sortedSelectedGroupIds,
372
+ const attendanceDays = computeFallbackAttendanceDays({
373
+ groupAttendanceDaysMap,
374
+ latestSubscriptionInvoices,
375
+ prepaidMonthCount,
376
+ selectedGroupIds: sortedSelectedGroupIds,
362
377
  selectedMonth,
363
- latestSubscriptionInvoices
364
- );
365
-
366
- const attendanceDays = getEffectiveDays(
378
+ useAttendanceBased,
367
379
  userAttendance,
368
- totalSessions,
369
- useAttendanceBased
370
- );
380
+ userGroups,
381
+ });
371
382
 
372
383
  if (attendanceDays === 0) return;
373
384
 
@@ -409,5 +420,6 @@ export function useSubscriptionAutoSelection({
409
420
  onSelectedProductsChange,
410
421
  products,
411
422
  groupAttendanceDaysMap,
423
+ prepaidMonthCount,
412
424
  ]);
413
425
  }