@tuturuuu/ui 0.9.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 +45 -0
- package/package.json +6 -5
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +17 -16
- 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/boards/__tests__/board-share-dialog.test.tsx +16 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +123 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +58 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +16 -4
- package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
- package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
- package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +58 -4
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -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-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- 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/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -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-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- 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 +390 -34
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- 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/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -69,6 +69,8 @@ export const getLinkedFinanceCategorySelection = (
|
|
|
69
69
|
|
|
70
70
|
const MONTH_VALUE_PATTERN = /^(\d{4})-(\d{2})$/;
|
|
71
71
|
const DATE_VALUE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
72
|
+
export const MAX_PREPAID_MONTH_COUNT = 12;
|
|
73
|
+
export const PREPAID_MONTH_OPTION_HORIZON = 12;
|
|
72
74
|
|
|
73
75
|
export const parseLocalCalendarDate = (
|
|
74
76
|
value: string | Date | null | undefined
|
|
@@ -122,6 +124,79 @@ export const formatMonthLabel = (month: string, locale: string): string => {
|
|
|
122
124
|
});
|
|
123
125
|
};
|
|
124
126
|
|
|
127
|
+
export const addMonthsToMonthValue = (
|
|
128
|
+
month: string,
|
|
129
|
+
monthOffset: number
|
|
130
|
+
): string => {
|
|
131
|
+
const date = getMonthStartDate(month);
|
|
132
|
+
if (Number.isNaN(date.getTime())) return month;
|
|
133
|
+
|
|
134
|
+
date.setMonth(date.getMonth() + monthOffset);
|
|
135
|
+
return formatMonthValue(date);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const normalizePrepaidMonthCount = (
|
|
139
|
+
value: number | null | undefined
|
|
140
|
+
): number => {
|
|
141
|
+
if (!Number.isFinite(value) || !value) return 1;
|
|
142
|
+
|
|
143
|
+
return Math.min(MAX_PREPAID_MONTH_COUNT, Math.max(1, Math.trunc(value)));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const getCoverageMonths = (
|
|
147
|
+
selectedMonth: string,
|
|
148
|
+
prepaidMonthCount = 1
|
|
149
|
+
): string[] => {
|
|
150
|
+
const monthCount = normalizePrepaidMonthCount(prepaidMonthCount);
|
|
151
|
+
const startMonth = getMonthStartDate(selectedMonth);
|
|
152
|
+
if (Number.isNaN(startMonth.getTime())) return [];
|
|
153
|
+
|
|
154
|
+
return Array.from({ length: monthCount }, (_, index) => {
|
|
155
|
+
const date = new Date(startMonth);
|
|
156
|
+
date.setMonth(date.getMonth() + index);
|
|
157
|
+
return formatMonthValue(date);
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const getCoverageEndMonthValue = (
|
|
162
|
+
selectedMonth: string,
|
|
163
|
+
prepaidMonthCount = 1
|
|
164
|
+
): string => {
|
|
165
|
+
const coverageMonths = getCoverageMonths(selectedMonth, prepaidMonthCount);
|
|
166
|
+
return coverageMonths[coverageMonths.length - 1] ?? selectedMonth;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const getCoverageValidUntilMonthValue = (
|
|
170
|
+
selectedMonth: string,
|
|
171
|
+
prepaidMonthCount = 1
|
|
172
|
+
): string => addMonthsToMonthValue(selectedMonth, prepaidMonthCount);
|
|
173
|
+
|
|
174
|
+
export const formatCoverageRangeLabel = ({
|
|
175
|
+
locale,
|
|
176
|
+
prepaidMonthCount,
|
|
177
|
+
selectedMonth,
|
|
178
|
+
}: {
|
|
179
|
+
locale: string;
|
|
180
|
+
prepaidMonthCount?: number | null;
|
|
181
|
+
selectedMonth: string;
|
|
182
|
+
}): string => {
|
|
183
|
+
const coverageMonths = getCoverageMonths(
|
|
184
|
+
selectedMonth,
|
|
185
|
+
prepaidMonthCount ?? 1
|
|
186
|
+
);
|
|
187
|
+
const firstMonth = coverageMonths[0];
|
|
188
|
+
const lastMonth = coverageMonths[coverageMonths.length - 1];
|
|
189
|
+
|
|
190
|
+
if (!firstMonth || !lastMonth || firstMonth === lastMonth) {
|
|
191
|
+
return formatMonthLabel(selectedMonth, locale);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return `${formatMonthLabel(firstMonth, locale)} - ${formatMonthLabel(
|
|
195
|
+
lastMonth,
|
|
196
|
+
locale
|
|
197
|
+
)}`;
|
|
198
|
+
};
|
|
199
|
+
|
|
125
200
|
const getComparableTimestamp = (value: string | null | undefined): number => {
|
|
126
201
|
if (!value) return 0;
|
|
127
202
|
const timestamp = new Date(value).getTime();
|
|
@@ -183,6 +258,38 @@ export const isSubscriptionMonthPaidForGroup = (
|
|
|
183
258
|
getSubscriptionCoverageInvoiceForGroup(latestInvoices, groupId)
|
|
184
259
|
);
|
|
185
260
|
|
|
261
|
+
export const isSubscriptionRangePaidForGroup = (
|
|
262
|
+
groupId: string,
|
|
263
|
+
selectedMonth: string,
|
|
264
|
+
prepaidMonthCount: number,
|
|
265
|
+
latestInvoices: SubscriptionCoverageInvoice[]
|
|
266
|
+
): boolean => {
|
|
267
|
+
const coverageMonths = getCoverageMonths(selectedMonth, prepaidMonthCount);
|
|
268
|
+
if (coverageMonths.length === 0) return false;
|
|
269
|
+
|
|
270
|
+
return coverageMonths.every((month) =>
|
|
271
|
+
isSubscriptionMonthPaidForGroup(groupId, month, latestInvoices)
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const isSubscriptionRangeFullyPaidForGroups = (
|
|
276
|
+
groupIds: string[],
|
|
277
|
+
selectedMonth: string,
|
|
278
|
+
prepaidMonthCount: number,
|
|
279
|
+
latestInvoices: SubscriptionCoverageInvoice[]
|
|
280
|
+
): boolean => {
|
|
281
|
+
if (groupIds.length === 0) return false;
|
|
282
|
+
|
|
283
|
+
return groupIds.every((groupId) =>
|
|
284
|
+
isSubscriptionRangePaidForGroup(
|
|
285
|
+
groupId,
|
|
286
|
+
selectedMonth,
|
|
287
|
+
prepaidMonthCount,
|
|
288
|
+
latestInvoices
|
|
289
|
+
)
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
186
293
|
export const getAttendanceStats = (
|
|
187
294
|
attendance: AttendanceRecord[]
|
|
188
295
|
): AttendanceStats => {
|
|
@@ -283,6 +390,17 @@ export const getBillableSessionsForGroups = (
|
|
|
283
390
|
});
|
|
284
391
|
};
|
|
285
392
|
|
|
393
|
+
export const getBillableSessionsForGroupsInRange = (
|
|
394
|
+
userGroups: UserGroup[],
|
|
395
|
+
groupIds: string[],
|
|
396
|
+
selectedMonth: string,
|
|
397
|
+
prepaidMonthCount = 1,
|
|
398
|
+
latestInvoices: SubscriptionCoverageInvoice[] = []
|
|
399
|
+
): BillableSession[] =>
|
|
400
|
+
getCoverageMonths(selectedMonth, prepaidMonthCount).flatMap((month) =>
|
|
401
|
+
getBillableSessionsForGroups(userGroups, groupIds, month, latestInvoices)
|
|
402
|
+
);
|
|
403
|
+
|
|
286
404
|
export const getBillableAttendanceRecords = (
|
|
287
405
|
attendance: AttendanceRecord[],
|
|
288
406
|
groupIds: string[],
|
|
@@ -305,6 +423,17 @@ export const getBillableAttendanceRecords = (
|
|
|
305
423
|
});
|
|
306
424
|
};
|
|
307
425
|
|
|
426
|
+
export const getBillableAttendanceRecordsInRange = (
|
|
427
|
+
attendance: AttendanceRecord[],
|
|
428
|
+
groupIds: string[],
|
|
429
|
+
selectedMonth: string,
|
|
430
|
+
prepaidMonthCount = 1,
|
|
431
|
+
latestInvoices: SubscriptionCoverageInvoice[] = []
|
|
432
|
+
): AttendanceRecord[] =>
|
|
433
|
+
getCoverageMonths(selectedMonth, prepaidMonthCount).flatMap((month) =>
|
|
434
|
+
getBillableAttendanceRecords(attendance, groupIds, month, latestInvoices)
|
|
435
|
+
);
|
|
436
|
+
|
|
308
437
|
export const getSessionsForMonth = (
|
|
309
438
|
sessionsArray: string[] | null,
|
|
310
439
|
month: string
|
|
@@ -453,26 +582,35 @@ export const getAvailableMonths = (
|
|
|
453
582
|
groupIds: string[],
|
|
454
583
|
latestInvoices: SubscriptionCoverageInvoice[],
|
|
455
584
|
locale: string,
|
|
456
|
-
selectedMonthFallback: string | null = null
|
|
585
|
+
selectedMonthFallback: string | null = null,
|
|
586
|
+
futureMonthHorizon = 0
|
|
457
587
|
): AvailableMonthOption[] => {
|
|
458
588
|
if (groupIds.length === 0) return [];
|
|
459
589
|
const { earliestStart, latestEnd } = getGroupsDateRange(userGroups, groupIds);
|
|
460
590
|
if (!earliestStart) return [];
|
|
461
591
|
|
|
592
|
+
const currentMonthStart = (() => {
|
|
593
|
+
const date = new Date();
|
|
594
|
+
date.setDate(1);
|
|
595
|
+
return date;
|
|
596
|
+
})();
|
|
597
|
+
const futureHorizonEnd = new Date(currentMonthStart);
|
|
598
|
+
futureHorizonEnd.setMonth(
|
|
599
|
+
futureHorizonEnd.getMonth() + Math.max(0, futureMonthHorizon)
|
|
600
|
+
);
|
|
601
|
+
|
|
462
602
|
const resolvedLatestEnd = latestEnd
|
|
463
603
|
? latestEnd
|
|
464
604
|
: selectedMonthFallback
|
|
465
605
|
? getMonthStartDate(selectedMonthFallback)
|
|
466
|
-
:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return d;
|
|
470
|
-
})();
|
|
606
|
+
: currentMonthStart;
|
|
607
|
+
const resolvedRangeEnd =
|
|
608
|
+
futureMonthHorizon > 0 && !latestEnd ? futureHorizonEnd : resolvedLatestEnd;
|
|
471
609
|
|
|
472
610
|
const months: AvailableMonthOption[] = [];
|
|
473
611
|
const currentDate = new Date(earliestStart);
|
|
474
612
|
currentDate.setDate(1);
|
|
475
|
-
const normalizedLatestEnd = new Date(
|
|
613
|
+
const normalizedLatestEnd = new Date(resolvedRangeEnd);
|
|
476
614
|
normalizedLatestEnd.setDate(1);
|
|
477
615
|
|
|
478
616
|
while (currentDate <= normalizedLatestEnd) {
|
|
@@ -504,6 +642,101 @@ export const getTotalSessionsForGroups = (
|
|
|
504
642
|
).length;
|
|
505
643
|
};
|
|
506
644
|
|
|
645
|
+
export const getBillableQuantityForGroupRange = ({
|
|
646
|
+
groupId,
|
|
647
|
+
latestInvoices,
|
|
648
|
+
now = new Date(),
|
|
649
|
+
prepaidMonthCount = 1,
|
|
650
|
+
selectedMonth,
|
|
651
|
+
useAttendanceBased,
|
|
652
|
+
userAttendance,
|
|
653
|
+
userGroups,
|
|
654
|
+
}: {
|
|
655
|
+
groupId: string;
|
|
656
|
+
latestInvoices?: SubscriptionCoverageInvoice[];
|
|
657
|
+
now?: Date;
|
|
658
|
+
prepaidMonthCount?: number;
|
|
659
|
+
selectedMonth: string;
|
|
660
|
+
useAttendanceBased: boolean;
|
|
661
|
+
userAttendance: AttendanceRecord[];
|
|
662
|
+
userGroups: UserGroup[];
|
|
663
|
+
}): number => {
|
|
664
|
+
const currentMonthStart = getMonthStartDate(now);
|
|
665
|
+
const coverageMonths = getCoverageMonths(selectedMonth, prepaidMonthCount);
|
|
666
|
+
|
|
667
|
+
return coverageMonths.reduce((total, month) => {
|
|
668
|
+
if (isSubscriptionMonthPaidForGroup(groupId, month, latestInvoices ?? [])) {
|
|
669
|
+
return total;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const monthStart = getMonthStartDate(month);
|
|
673
|
+
const useScheduledSessions =
|
|
674
|
+
!useAttendanceBased ||
|
|
675
|
+
(!Number.isNaN(monthStart.getTime()) &&
|
|
676
|
+
!Number.isNaN(currentMonthStart.getTime()) &&
|
|
677
|
+
monthStart > currentMonthStart);
|
|
678
|
+
|
|
679
|
+
if (useScheduledSessions) {
|
|
680
|
+
return (
|
|
681
|
+
total +
|
|
682
|
+
getBillableSessionsForGroups(
|
|
683
|
+
userGroups,
|
|
684
|
+
[groupId],
|
|
685
|
+
month,
|
|
686
|
+
latestInvoices
|
|
687
|
+
).length
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return (
|
|
692
|
+
total +
|
|
693
|
+
getEffectiveAttendanceDays(
|
|
694
|
+
getBillableAttendanceRecords(
|
|
695
|
+
userAttendance,
|
|
696
|
+
[groupId],
|
|
697
|
+
month,
|
|
698
|
+
latestInvoices
|
|
699
|
+
)
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
}, 0);
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
export const getBillableQuantityMapForGroupsRange = ({
|
|
706
|
+
groupIds,
|
|
707
|
+
latestInvoices,
|
|
708
|
+
now,
|
|
709
|
+
prepaidMonthCount = 1,
|
|
710
|
+
selectedMonth,
|
|
711
|
+
useAttendanceBased,
|
|
712
|
+
userAttendance,
|
|
713
|
+
userGroups,
|
|
714
|
+
}: {
|
|
715
|
+
groupIds: string[];
|
|
716
|
+
latestInvoices?: SubscriptionCoverageInvoice[];
|
|
717
|
+
now?: Date;
|
|
718
|
+
prepaidMonthCount?: number;
|
|
719
|
+
selectedMonth: string;
|
|
720
|
+
useAttendanceBased: boolean;
|
|
721
|
+
userAttendance: AttendanceRecord[];
|
|
722
|
+
userGroups: UserGroup[];
|
|
723
|
+
}): Record<string, number> =>
|
|
724
|
+
Object.fromEntries(
|
|
725
|
+
groupIds.map((groupId) => [
|
|
726
|
+
groupId,
|
|
727
|
+
getBillableQuantityForGroupRange({
|
|
728
|
+
groupId,
|
|
729
|
+
latestInvoices,
|
|
730
|
+
now,
|
|
731
|
+
prepaidMonthCount,
|
|
732
|
+
selectedMonth,
|
|
733
|
+
useAttendanceBased,
|
|
734
|
+
userAttendance,
|
|
735
|
+
userGroups,
|
|
736
|
+
}),
|
|
737
|
+
])
|
|
738
|
+
);
|
|
739
|
+
|
|
507
740
|
/** Days before valid_until to consider "expiring soon" */
|
|
508
741
|
const EXPIRING_SOON_DAYS = 14;
|
|
509
742
|
|
|
@@ -195,6 +195,22 @@ describe('BoardShareDialog', () => {
|
|
|
195
195
|
expect(screen.getByText('pm@example.com')).toBeInTheDocument();
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
it('does not crash when viewable members payload is missing members', async () => {
|
|
199
|
+
listWorkspaceTaskBoardViewableMembersMock.mockResolvedValue({});
|
|
200
|
+
|
|
201
|
+
renderBoardShareDialog();
|
|
202
|
+
|
|
203
|
+
fireEvent.click(
|
|
204
|
+
screen.getByRole('button', {
|
|
205
|
+
name: /ws-task-boards.share.workspace_members.title/,
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(
|
|
210
|
+
await screen.findByText('ws-task-boards.share.workspace_members.empty')
|
|
211
|
+
).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
198
214
|
it('keeps direct board guests first-class for invite, update, and remove', async () => {
|
|
199
215
|
listWorkspaceTaskBoardSharesMock.mockResolvedValue({
|
|
200
216
|
shares: [
|
|
@@ -104,6 +104,18 @@ describe('TaskBoardForm', () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it('uses the surface background for the board name input', () => {
|
|
108
|
+
render(
|
|
109
|
+
<QueryClientProvider client={queryClient}>
|
|
110
|
+
<TaskBoardForm wsId="ws-1" onFinish={vi.fn()} />
|
|
111
|
+
</QueryClientProvider>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(screen.getByLabelText('ws-task-boards.name')).toHaveClass(
|
|
115
|
+
'bg-background'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
107
119
|
it('shows the localized duplicate-name message when the API rejects a duplicate board name', async () => {
|
|
108
120
|
mutateAsync.mockRejectedValueOnce({
|
|
109
121
|
code: 'TASK_BOARD_NAME_EXISTS',
|