@tuturuuu/ui 0.10.0 → 0.11.0
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 +16 -0
- package/package.json +4 -4
- 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/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/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/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.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,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/tutur3u/platform/compare/ui-v0.10.0...ui-v0.11.0) (2026-06-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **finance:** support prepaid subscription invoice ranges ([fa0f338](https://github.com/tutur3u/platform/commit/fa0f3388dfb3d3ea539a8d8bdc3f100f268542c6))
|
|
9
|
+
* **tasks:** add board header loading skeleton ([73f1242](https://github.com/tutur3u/platform/commit/73f12421a9cd50c035a1c9940e152354b1974f26))
|
|
10
|
+
* **tasks:** add task templates ([8d0700a](https://github.com/tutur3u/platform/commit/8d0700ad255c7b5874bfa065575df6b1cde34063))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **tasks:** pad board header skeleton ([b0c5c0f](https://github.com/tutur3u/platform/commit/b0c5c0fea8871cac2603da6239b980c752b1c9f6))
|
|
16
|
+
* **tasks:** prevent description wipes during yjs hydration ([947f38c](https://github.com/tutur3u/platform/commit/947f38c6474d07b1fac1c44e166638bfc87260c0))
|
|
17
|
+
* **tasks:** restore tracked task descriptions ([f892ae2](https://github.com/tutur3u/platform/commit/f892ae23dfec41c2d25649b97a628d8cdcd1fa5d))
|
|
18
|
+
|
|
3
19
|
## [0.10.0](https://github.com/tutur3u/platform/compare/ui-v0.9.0...ui-v0.10.0) (2026-06-26)
|
|
4
20
|
|
|
5
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -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.12.0",
|
|
92
92
|
"@tuturuuu/supabase": "0.4.0",
|
|
93
|
-
"@tuturuuu/utils": "0.
|
|
93
|
+
"@tuturuuu/utils": "0.11.0",
|
|
94
94
|
"@types/debug": "^4.1.13",
|
|
95
95
|
"browser-image-compression": "^2.0.2",
|
|
96
96
|
"class-variance-authority": "^0.7.1",
|
|
@@ -149,7 +149,7 @@
|
|
|
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.0",
|
|
153
153
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
154
154
|
"@types/html2canvas": "^1.0.0",
|
|
155
155
|
"@types/lodash": "^4.17.24",
|
|
@@ -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
|
}
|
|
@@ -3,11 +3,15 @@ import { useEffect, useRef } from 'react';
|
|
|
3
3
|
import type { SelectedProductItem, UserGroupProducts } from '../types';
|
|
4
4
|
import type { UserGroup } from '../utils';
|
|
5
5
|
import {
|
|
6
|
+
formatCoverageRangeLabel,
|
|
6
7
|
formatMonthLabel,
|
|
7
8
|
getAttendanceStats,
|
|
9
|
+
getBillableAttendanceRecordsInRange,
|
|
10
|
+
getBillableSessionsForGroupsInRange,
|
|
11
|
+
getCoverageEndMonthValue,
|
|
12
|
+
getCoverageMonths,
|
|
8
13
|
getSubscriptionCoverageInvoiceForGroup,
|
|
9
|
-
|
|
10
|
-
parseLocalCalendarDate,
|
|
14
|
+
isSubscriptionMonthPaidForGroup,
|
|
11
15
|
} from '../utils';
|
|
12
16
|
|
|
13
17
|
interface UseSubscriptionInvoiceContentProps {
|
|
@@ -26,6 +30,7 @@ interface UseSubscriptionInvoiceContentProps {
|
|
|
26
30
|
locale: string;
|
|
27
31
|
onContentChange: (content: string) => void;
|
|
28
32
|
onNotesChange: (notes: string) => void;
|
|
33
|
+
prepaidMonthCount?: number;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function useSubscriptionInvoiceContent({
|
|
@@ -41,6 +46,7 @@ export function useSubscriptionInvoiceContent({
|
|
|
41
46
|
locale,
|
|
42
47
|
onContentChange,
|
|
43
48
|
onNotesChange,
|
|
49
|
+
prepaidMonthCount = 1,
|
|
44
50
|
}: UseSubscriptionInvoiceContentProps): void {
|
|
45
51
|
const t = useTranslations();
|
|
46
52
|
const contentCallbackRef = useRef(onContentChange);
|
|
@@ -63,7 +69,18 @@ export function useSubscriptionInvoiceContent({
|
|
|
63
69
|
const formatMonth = (monthStr: string) =>
|
|
64
70
|
formatMonthLabel(monthStr, locale);
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
const coverageMonths = getCoverageMonths(selectedMonth, prepaidMonthCount);
|
|
73
|
+
const coverageEndMonth = getCoverageEndMonthValue(
|
|
74
|
+
selectedMonth,
|
|
75
|
+
prepaidMonthCount
|
|
76
|
+
);
|
|
77
|
+
const fallbackRangeLabel = formatCoverageRangeLabel({
|
|
78
|
+
locale,
|
|
79
|
+
prepaidMonthCount,
|
|
80
|
+
selectedMonth,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Group by unpaid month ranges.
|
|
67
84
|
const rangeToGroups = new Map<string, string[]>();
|
|
68
85
|
|
|
69
86
|
selectedGroupIds.forEach((groupId) => {
|
|
@@ -77,12 +94,21 @@ export function useSubscriptionInvoiceContent({
|
|
|
77
94
|
latestSubscriptionInvoices,
|
|
78
95
|
groupId
|
|
79
96
|
);
|
|
97
|
+
const unpaidMonths = coverageMonths.filter(
|
|
98
|
+
(month) =>
|
|
99
|
+
!isSubscriptionMonthPaidForGroup(
|
|
100
|
+
groupId,
|
|
101
|
+
month,
|
|
102
|
+
latestSubscriptionInvoices
|
|
103
|
+
)
|
|
104
|
+
);
|
|
80
105
|
|
|
81
|
-
const startMonth =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const endMonth =
|
|
106
|
+
const startMonth =
|
|
107
|
+
unpaidMonths[0] ??
|
|
108
|
+
latestInvoice?.valid_until?.slice(0, 7) ??
|
|
109
|
+
selectedMonth;
|
|
110
|
+
const endMonth =
|
|
111
|
+
unpaidMonths[unpaidMonths.length - 1] ?? coverageEndMonth;
|
|
86
112
|
|
|
87
113
|
let rangeLabel = '';
|
|
88
114
|
if (startMonth && startMonth < endMonth) {
|
|
@@ -90,7 +116,10 @@ export function useSubscriptionInvoiceContent({
|
|
|
90
116
|
rangeLabel = `${formatMonth(startMonth)} - ${formatMonth(endMonth)}`;
|
|
91
117
|
} else {
|
|
92
118
|
// Single month
|
|
93
|
-
rangeLabel =
|
|
119
|
+
rangeLabel =
|
|
120
|
+
unpaidMonths.length === 0
|
|
121
|
+
? fallbackRangeLabel
|
|
122
|
+
: formatMonth(endMonth);
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
if (!rangeToGroups.has(rangeLabel)) {
|
|
@@ -126,28 +155,23 @@ export function useSubscriptionInvoiceContent({
|
|
|
126
155
|
}
|
|
127
156
|
|
|
128
157
|
let autoNotes: string | null = null;
|
|
129
|
-
if (selectedGroupIds.length > 0
|
|
130
|
-
const filteredAttendance =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!latestInvoice?.valid_until) return true;
|
|
138
|
-
const validUntil = parseLocalCalendarDate(latestInvoice.valid_until);
|
|
139
|
-
const attendanceDate = parseLocalCalendarDate(a.date);
|
|
140
|
-
return attendanceDate >= validUntil;
|
|
141
|
-
});
|
|
142
|
-
|
|
158
|
+
if (selectedGroupIds.length > 0) {
|
|
159
|
+
const filteredAttendance = getBillableAttendanceRecordsInRange(
|
|
160
|
+
userAttendance,
|
|
161
|
+
selectedGroupIds,
|
|
162
|
+
selectedMonth,
|
|
163
|
+
prepaidMonthCount,
|
|
164
|
+
latestSubscriptionInvoices
|
|
165
|
+
);
|
|
143
166
|
const attendanceStats = getAttendanceStats(filteredAttendance);
|
|
144
167
|
const attendanceDays = attendanceStats.present + attendanceStats.late;
|
|
145
|
-
const totalSessions =
|
|
168
|
+
const totalSessions = getBillableSessionsForGroupsInRange(
|
|
146
169
|
userGroups,
|
|
147
170
|
selectedGroupIds,
|
|
148
171
|
selectedMonth,
|
|
172
|
+
prepaidMonthCount,
|
|
149
173
|
latestSubscriptionInvoices
|
|
150
|
-
);
|
|
174
|
+
).length;
|
|
151
175
|
autoNotes = t('ws-invoices.attendance_summary_note', {
|
|
152
176
|
attended: attendanceDays,
|
|
153
177
|
total: totalSessions,
|
|
@@ -192,5 +216,6 @@ export function useSubscriptionInvoiceContent({
|
|
|
192
216
|
locale,
|
|
193
217
|
t,
|
|
194
218
|
latestSubscriptionInvoices,
|
|
219
|
+
prepaidMonthCount,
|
|
195
220
|
]);
|
|
196
221
|
}
|
|
@@ -501,10 +501,18 @@ export const useSubscriptionInvoiceContext = (
|
|
|
501
501
|
wsId: string,
|
|
502
502
|
userId: string,
|
|
503
503
|
groupIds: string[],
|
|
504
|
-
month: string
|
|
504
|
+
month: string,
|
|
505
|
+
monthCount = 1
|
|
505
506
|
) => {
|
|
506
507
|
return useQuery({
|
|
507
|
-
queryKey: [
|
|
508
|
+
queryKey: [
|
|
509
|
+
'subscription-invoice-context',
|
|
510
|
+
wsId,
|
|
511
|
+
userId,
|
|
512
|
+
groupIds,
|
|
513
|
+
month,
|
|
514
|
+
monthCount,
|
|
515
|
+
],
|
|
508
516
|
queryFn: async () => {
|
|
509
517
|
if (!wsId || !userId || groupIds.length === 0 || !month) {
|
|
510
518
|
return {
|
|
@@ -524,6 +532,7 @@ export const useSubscriptionInvoiceContext = (
|
|
|
524
532
|
return getSubscriptionInvoiceContext(wsId, {
|
|
525
533
|
groupIds,
|
|
526
534
|
month,
|
|
535
|
+
monthCount,
|
|
527
536
|
userId,
|
|
528
537
|
});
|
|
529
538
|
},
|
|
@@ -61,6 +61,7 @@ export interface CreateInvoicePayload {
|
|
|
61
61
|
export interface CreateSubscriptionInvoicePayload extends CreateInvoicePayload {
|
|
62
62
|
customer_id: string;
|
|
63
63
|
group_ids: string[];
|
|
64
|
+
prepaid_month_count?: number;
|
|
64
65
|
selected_month: string;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -72,11 +73,15 @@ export interface InvoiceMutationResponse {
|
|
|
72
73
|
subtotal: number;
|
|
73
74
|
total: number;
|
|
74
75
|
};
|
|
76
|
+
coverage_end_month?: string;
|
|
77
|
+
coverage_start_month?: string;
|
|
75
78
|
frontend_values?: {
|
|
76
79
|
discount_amount?: number;
|
|
77
80
|
subtotal?: number;
|
|
78
81
|
total?: number;
|
|
79
82
|
};
|
|
83
|
+
prepaid_month_count?: number;
|
|
84
|
+
valid_until?: string;
|
|
80
85
|
values_recalculated?: boolean;
|
|
81
86
|
};
|
|
82
87
|
invoice_id: string;
|