@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.
- package/CHANGELOG.md +25 -0
- package/package.json +47 -47
- package/src/components/ui/finance/invoices/components/subscription-attendance-summary.tsx +19 -5
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.test.tsx +41 -0
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.tsx +93 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +82 -70
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +50 -25
- package/src/components/ui/finance/invoices/hooks.ts +11 -2
- package/src/components/ui/finance/invoices/internal-api.ts +5 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +92 -31
- package/src/components/ui/finance/invoices/utils.test.ts +82 -0
- package/src/components/ui/finance/invoices/utils.ts +240 -7
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +22 -0
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +7 -0
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +37 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +3 -1
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +55 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.test.ts +97 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.ts +210 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +56 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.tsx +218 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-restore-banner.tsx +83 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.test.tsx +120 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.tsx +180 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +157 -4
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +178 -14
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.test.ts +100 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.ts +109 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +381 -34
- package/src/components/ui/tu-do/templates/task-template-api.ts +142 -0
- package/src/components/ui/tu-do/templates/task-template-card.tsx +118 -0
- package/src/components/ui/tu-do/templates/task-template-client.test.tsx +52 -0
- package/src/components/ui/tu-do/templates/task-template-client.tsx +258 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.test.tsx +167 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.tsx +376 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.test.tsx +114 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +50 -0
- 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.
|
|
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.
|
|
28
|
-
"@radix-ui/react-alert-dialog": "^1.1.
|
|
29
|
-
"@radix-ui/react-aspect-ratio": "^1.1.
|
|
30
|
-
"@radix-ui/react-avatar": "^1.2.
|
|
31
|
-
"@radix-ui/react-checkbox": "^1.3.
|
|
32
|
-
"@radix-ui/react-collapsible": "^1.1.
|
|
33
|
-
"@radix-ui/react-context-menu": "^2.3.
|
|
34
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
35
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
36
|
-
"@radix-ui/react-hover-card": "^1.1.
|
|
37
|
-
"@radix-ui/react-label": "^2.1.
|
|
38
|
-
"@radix-ui/react-menubar": "^1.1.
|
|
39
|
-
"@radix-ui/react-navigation-menu": "^1.2.
|
|
40
|
-
"@radix-ui/react-popover": "^1.1.
|
|
41
|
-
"@radix-ui/react-progress": "^1.1.
|
|
42
|
-
"@radix-ui/react-radio-group": "^1.4.
|
|
43
|
-
"@radix-ui/react-scroll-area": "^1.2.
|
|
44
|
-
"@radix-ui/react-select": "^2.3.
|
|
45
|
-
"@radix-ui/react-separator": "^1.1.
|
|
46
|
-
"@radix-ui/react-slider": "^1.4.
|
|
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.
|
|
49
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
50
|
-
"@radix-ui/react-toast": "^1.2.
|
|
51
|
-
"@radix-ui/react-toggle": "^1.1.
|
|
52
|
-
"@radix-ui/react-toggle-group": "^1.1.
|
|
53
|
-
"@radix-ui/react-tooltip": "^1.2.
|
|
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.
|
|
60
|
+
"@tanstack/react-query": "^5.101.2",
|
|
61
61
|
"@tanstack/react-table": "^8.21.3",
|
|
62
|
-
"@tanstack/react-virtual": "^3.14.
|
|
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.
|
|
91
|
+
"@tuturuuu/internal-api": "0.13.0",
|
|
92
92
|
"@tuturuuu/supabase": "0.4.0",
|
|
93
|
-
"@tuturuuu/utils": "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.
|
|
105
|
+
"framer-motion": "^12.42.2",
|
|
106
106
|
"gsap": "^3.15.0",
|
|
107
|
-
"html2canvas-pro": "^2.
|
|
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.
|
|
113
|
-
"next": "^16.
|
|
114
|
-
"next-intl": "^4.13.
|
|
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.
|
|
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.
|
|
129
|
+
"react-resizable-panels": "^4.12.0",
|
|
130
130
|
"react-syntax-highlighter": "^16.1.1",
|
|
131
|
-
"recharts": "3.9.
|
|
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.
|
|
146
|
+
"@tailwindcss/postcss": "^4.3.2",
|
|
147
147
|
"@tailwindcss/typography": "^0.5.20",
|
|
148
|
-
"@tanstack/react-query": "^5.101.
|
|
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.
|
|
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.
|
|
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.
|
|
163
|
+
"autoprefixer": "^10.5.2",
|
|
164
164
|
"jsdom": "^29.1.1",
|
|
165
|
-
"postcss": "^8.5.
|
|
165
|
+
"postcss": "^8.5.16",
|
|
166
166
|
"postcss-load-config": "^6.0.1",
|
|
167
|
-
"tailwindcss": "^4.3.
|
|
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.
|
|
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:
|
|
107
|
+
month: displayedRangeLabel,
|
|
102
108
|
})
|
|
103
109
|
: t('ws-invoices.combined_attendance_for_month', {
|
|
104
110
|
count: selectedGroupIds.length,
|
|
105
|
-
month:
|
|
106
|
-
default: `Combined attendance from ${selectedGroupIds.length} groups for ${
|
|
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(
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
+
prepaidMonthCount === 1 &&
|
|
316
|
+
prefillAmount != null &&
|
|
317
|
+
!initialPrefillUsedRef.current;
|
|
319
318
|
const attendanceDays = shouldUsePrefill
|
|
320
319
|
? prefillAmount
|
|
321
|
-
:
|
|
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 (
|
|
364
|
+
if (
|
|
365
|
+
prepaidMonthCount === 1 &&
|
|
366
|
+
prefillAmount != null &&
|
|
367
|
+
!initialPrefillUsedRef.current
|
|
368
|
+
) {
|
|
356
369
|
return;
|
|
357
370
|
}
|
|
358
371
|
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
372
|
+
const attendanceDays = computeFallbackAttendanceDays({
|
|
373
|
+
groupAttendanceDaysMap,
|
|
374
|
+
latestSubscriptionInvoices,
|
|
375
|
+
prepaidMonthCount,
|
|
376
|
+
selectedGroupIds: sortedSelectedGroupIds,
|
|
362
377
|
selectedMonth,
|
|
363
|
-
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
const attendanceDays = getEffectiveDays(
|
|
378
|
+
useAttendanceBased,
|
|
367
379
|
userAttendance,
|
|
368
|
-
|
|
369
|
-
|
|
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
|
}
|