@nightkatana/kronosys-app 1.0.0-beta.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.
Files changed (179) hide show
  1. package/README.md +81 -0
  2. package/app/api/action/route.ts +16 -0
  3. package/app/api/backup/route.ts +84 -0
  4. package/app/api/health/route.ts +22 -0
  5. package/app/api/state/route.ts +27 -0
  6. package/app/apple-icon.png +0 -0
  7. package/app/changelog/page.tsx +122 -0
  8. package/app/globals.css +210 -0
  9. package/app/guide/layout.tsx +11 -0
  10. package/app/guide/page.tsx +278 -0
  11. package/app/icon.png +0 -0
  12. package/app/layout.tsx +77 -0
  13. package/app/licenses/layout.tsx +11 -0
  14. package/app/licenses/page.tsx +246 -0
  15. package/app/manifest.ts +32 -0
  16. package/app/page.tsx +1610 -0
  17. package/app/reporting/page.tsx +2943 -0
  18. package/app/settings/layout.tsx +10 -0
  19. package/app/settings/page.tsx +3518 -0
  20. package/bin/kronosys.mjs +46 -0
  21. package/components/KronosysPackageVersionProvider.tsx +19 -0
  22. package/components/KronosysPayloadProvider.tsx +109 -0
  23. package/components/PwaRegister.tsx +25 -0
  24. package/components/SiteLegalFooter.tsx +21 -0
  25. package/components/ThemeProvider.tsx +78 -0
  26. package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
  27. package/components/dashboard/AppShellRouteNav.tsx +131 -0
  28. package/components/dashboard/AppVersionStamp.tsx +16 -0
  29. package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
  30. package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
  31. package/components/dashboard/DashboardCommandCenter.tsx +470 -0
  32. package/components/dashboard/DashboardLangGateModal.tsx +118 -0
  33. package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
  34. package/components/dashboard/DashboardSimpleModal.tsx +337 -0
  35. package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
  36. package/components/dashboard/DashboardToastProvider.tsx +64 -0
  37. package/components/dashboard/DashboardTour.tsx +435 -0
  38. package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
  39. package/components/dashboard/DeleteSessionModal.tsx +130 -0
  40. package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
  41. package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
  42. package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
  43. package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
  44. package/components/dashboard/IssuePickerModal.tsx +168 -0
  45. package/components/dashboard/KronoFocusPanel.tsx +834 -0
  46. package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
  47. package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
  48. package/components/dashboard/LanguageMenu.tsx +123 -0
  49. package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
  50. package/components/dashboard/NewSessionScopeModal.tsx +410 -0
  51. package/components/dashboard/PageRefreshButton.tsx +130 -0
  52. package/components/dashboard/PlainHelpPopover.tsx +97 -0
  53. package/components/dashboard/ReportingPageToc.tsx +68 -0
  54. package/components/dashboard/ReportingTour.tsx +342 -0
  55. package/components/dashboard/SavedProjectPicker.tsx +92 -0
  56. package/components/dashboard/SavedTagPicker.tsx +115 -0
  57. package/components/dashboard/ScrollToTopFab.tsx +41 -0
  58. package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
  59. package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
  60. package/components/dashboard/SessionListPanel.tsx +320 -0
  61. package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
  62. package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
  63. package/components/dashboard/SettingsTour.tsx +332 -0
  64. package/components/dashboard/TagPills.tsx +149 -0
  65. package/components/dashboard/TagsHelpTrigger.tsx +84 -0
  66. package/components/dashboard/TaskFocusPanel.tsx +1261 -0
  67. package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
  68. package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
  69. package/components/dashboard/ThemeToggle.test.tsx +26 -0
  70. package/components/dashboard/ThemeToggle.tsx +36 -0
  71. package/components/dashboard/UserGuideBodyText.tsx +62 -0
  72. package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
  73. package/components/dashboard/taskFieldStyles.ts +139 -0
  74. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
  75. package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
  76. package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
  77. package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
  78. package/lib/appShellHeaderClasses.ts +12 -0
  79. package/lib/backupCsvExport.test.ts +149 -0
  80. package/lib/backupCsvExport.ts +392 -0
  81. package/lib/changelogCopy.ts +34 -0
  82. package/lib/concurrentTaskStartPreference.ts +29 -0
  83. package/lib/dashboardClockFormat.ts +13 -0
  84. package/lib/dashboardColumnChrome.ts +3 -0
  85. package/lib/dashboardColumnHintsStorage.ts +57 -0
  86. package/lib/dashboardCopy.ts +1831 -0
  87. package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
  88. package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
  89. package/lib/dashboardLangStorage.ts +72 -0
  90. package/lib/dashboardQuickSearch.ts +476 -0
  91. package/lib/dashboardQuickSearchQuery.test.ts +63 -0
  92. package/lib/dashboardQuickSearchQuery.ts +179 -0
  93. package/lib/dashboardSessionNav.ts +33 -0
  94. package/lib/dashboardShortcuts.ts +268 -0
  95. package/lib/dashboardTimeZone.ts +91 -0
  96. package/lib/dashboardTourStorage.ts +68 -0
  97. package/lib/dataDir.test.ts +87 -0
  98. package/lib/dataDir.ts +83 -0
  99. package/lib/devDataPreferenceFile.ts +55 -0
  100. package/lib/devDataRuntimeInfo.ts +34 -0
  101. package/lib/formatIsoShort.test.ts +46 -0
  102. package/lib/formatIsoShort.ts +29 -0
  103. package/lib/generatedUserChangelog.ts +34 -0
  104. package/lib/gitlabIssueSearch.ts +8 -0
  105. package/lib/kronoFocusDurationHistory.ts +71 -0
  106. package/lib/kronoFocusRhythm.test.ts +130 -0
  107. package/lib/kronoFocusRhythm.ts +46 -0
  108. package/lib/kronoFocusTimerUrgency.test.ts +74 -0
  109. package/lib/kronoFocusTimerUrgency.ts +24 -0
  110. package/lib/kronosysApi.ts +143 -0
  111. package/lib/legacyEditorPayloadKeys.ts +52 -0
  112. package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
  113. package/lib/legacyKronoFocusStorageKeys.ts +32 -0
  114. package/lib/licensesCopy.ts +128 -0
  115. package/lib/openPlainTextInNewTab.ts +49 -0
  116. package/lib/readKronosysPackageVersion.ts +10 -0
  117. package/lib/reportingAggregate.test.ts +325 -0
  118. package/lib/reportingAggregate.ts +819 -0
  119. package/lib/reportingDatePresets.ts +41 -0
  120. package/lib/reportingMetricHelp.ts +430 -0
  121. package/lib/reportingNonFinalIndicators.test.ts +157 -0
  122. package/lib/reportingNonFinalIndicators.ts +102 -0
  123. package/lib/reportingStrings.ts +491 -0
  124. package/lib/reportingTagWeekBreakdown.test.ts +141 -0
  125. package/lib/reportingTagWeekBreakdown.ts +181 -0
  126. package/lib/reportingWeekLayout.test.ts +239 -0
  127. package/lib/reportingWeekLayout.ts +313 -0
  128. package/lib/sessionAssiduity.test.ts +25 -0
  129. package/lib/sessionAssiduity.ts +33 -0
  130. package/lib/sessionEndReason.ts +55 -0
  131. package/lib/sessionEndWarnings.test.ts +200 -0
  132. package/lib/sessionEndWarnings.ts +125 -0
  133. package/lib/sessionListMerge.test.ts +101 -0
  134. package/lib/sessionListMerge.ts +70 -0
  135. package/lib/sessionTaskSidebarStats.test.ts +24 -0
  136. package/lib/sessionTaskSidebarStats.ts +54 -0
  137. package/lib/settingsCopy.ts +1276 -0
  138. package/lib/taskParsing.test.ts +153 -0
  139. package/lib/taskParsing.ts +737 -0
  140. package/lib/theme.ts +15 -0
  141. package/lib/translucentButtonClasses.ts +34 -0
  142. package/lib/usageProfile.test.ts +84 -0
  143. package/lib/usageProfile.ts +52 -0
  144. package/lib/userGuideCopy.ts +464 -0
  145. package/lib/workspaceLocDefaults.ts +21 -0
  146. package/next-env.d.ts +6 -0
  147. package/next.config.ts +15 -0
  148. package/package.json +87 -0
  149. package/postcss.config.mjs +12 -0
  150. package/public/apple-icon.png +0 -0
  151. package/public/favicon.ico +0 -0
  152. package/public/file.svg +1 -0
  153. package/public/globe.svg +1 -0
  154. package/public/icon-192.png +0 -0
  155. package/public/icon-512.png +0 -0
  156. package/public/icon.png +0 -0
  157. package/public/next.svg +1 -0
  158. package/public/sw.js +13 -0
  159. package/public/traceback.png +0 -0
  160. package/public/vercel.svg +1 -0
  161. package/public/window.svg +1 -0
  162. package/server/actionDispatch.test.ts +723 -0
  163. package/server/actionDispatch.ts +1476 -0
  164. package/server/actionTaskSession.test.ts +713 -0
  165. package/server/actionTaskSession.ts +717 -0
  166. package/server/db.ts +42 -0
  167. package/server/defaultCfg.ts +87 -0
  168. package/server/gitlabTokenStore.ts +34 -0
  169. package/server/kronoFocusHydrate.test.ts +142 -0
  170. package/server/kronoFocusHydrate.ts +69 -0
  171. package/server/kronoFocusMigrate.test.ts +53 -0
  172. package/server/kronoFocusMigrate.ts +78 -0
  173. package/server/mainTimerHydrate.test.ts +65 -0
  174. package/server/mainTimerHydrate.ts +53 -0
  175. package/server/payloadStore.test.ts +78 -0
  176. package/server/payloadStore.ts +83 -0
  177. package/server/sessionWallHydrate.test.ts +46 -0
  178. package/server/sessionWallHydrate.ts +88 -0
  179. package/tsconfig.json +41 -0
@@ -0,0 +1,325 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ UNDATED_KEY,
5
+ aggregateArchivedExcludedTaskMinutes,
6
+ aggregateProjectTaskMinutesByDay,
7
+ aggregateReporting,
8
+ aggregateReportingByProject,
9
+ aggregateTagTaskMinutesByDayAndWeek,
10
+ buildTagFilterSet,
11
+ collectTasksDeduped,
12
+ dayInRange,
13
+ localWeekMondayFromDayKey,
14
+ maxBucket,
15
+ mergeSessionsFromPayload,
16
+ sessionWallClockMinutes,
17
+ sortedDayKeys,
18
+ taskMatchesTags,
19
+ } from "./reportingAggregate";
20
+ import { calendarDateKeyInTimeZone } from "./dashboardTimeZone";
21
+ import type { KronosysUpdatePayload } from "./kronosysApi";
22
+
23
+ const TZ = "America/Toronto";
24
+
25
+ describe("reportingAggregate basic utils", () => {
26
+ it("calendarDateKeyInTimeZone produit le jour attendu pour un instant UTC", () => {
27
+ expect(calendarDateKeyInTimeZone("2026-06-15T04:00:00.000Z", TZ)).toBe("2026-06-15");
28
+ expect(calendarDateKeyInTimeZone("2026-06-15T03:59:59.999Z", TZ)).toBe("2026-06-14");
29
+ });
30
+
31
+ it("calendarDateKeyInTimeZone refuse les entrées invalides", () => {
32
+ expect(calendarDateKeyInTimeZone(null, TZ)).toBeNull();
33
+ expect(calendarDateKeyInTimeZone("", TZ)).toBeNull();
34
+ expect(calendarDateKeyInTimeZone("pas-une-date", TZ)).toBeNull();
35
+ expect(calendarDateKeyInTimeZone("2026-01-01T00:00:00.000Z", "Not/A_Timezone")).toBeNull();
36
+ });
37
+ });
38
+
39
+ describe("reportingAggregate", () => {
40
+ it("mergeSessionsFromPayload fusionne courant, historique et archives", () => {
41
+ const payload = {
42
+ history: [{ sessionId: "a", archived: false }],
43
+ historyArchived: [{ sessionId: "b", archived: true }],
44
+ current: { sessionId: "c", archived: false },
45
+ } as unknown as KronosysUpdatePayload;
46
+ const merged = mergeSessionsFromPayload(payload);
47
+ const ids = merged.map((s) => s.sessionId).sort();
48
+ expect(ids).toEqual(["a", "b", "c"]);
49
+ });
50
+
51
+ it("aggregateArchivedExcludedTaskMinutes compte le temps des tâches d’archives non finalisées", () => {
52
+ const sessions = [
53
+ {
54
+ sessionId: "arch-1",
55
+ archived: true,
56
+ tasks: [
57
+ {
58
+ isDone: false,
59
+ project: "acme",
60
+ durationMs: 6 * 60_000,
61
+ startTime: "2026-04-23T10:00:00.000Z",
62
+ endTime: "2026-04-23T10:06:00.000Z",
63
+ tags: ["default"],
64
+ subtasks: [],
65
+ },
66
+ ],
67
+ },
68
+ ];
69
+ const m = aggregateArchivedExcludedTaskMinutes(
70
+ sessions,
71
+ buildTagFilterSet([]),
72
+ "2026-04-23",
73
+ "2026-04-23",
74
+ TZ,
75
+ true
76
+ );
77
+ expect(m).toBe(6);
78
+ });
79
+
80
+ it("aggregateReporting agrège l’assiduité (référence, retards, somme, moyenne si en retard)", () => {
81
+ const sessions = [
82
+ {
83
+ sessionId: "on-time",
84
+ savedAt: "2026-04-24T12:00:00.000Z",
85
+ startAt: "2026-04-24T12:00:00.000Z",
86
+ sessionStartOffsetMinutes: 0,
87
+ tasks: [{ id: "t", isDone: true, durationMs: 1, tags: ["a"] }],
88
+ },
89
+ {
90
+ sessionId: "late",
91
+ savedAt: "2026-04-24T12:00:00.000Z",
92
+ startAt: "2026-04-24T12:20:00.000Z",
93
+ sessionStartOffsetMinutes: 20,
94
+ tasks: [{ id: "t2", isDone: true, durationMs: 1, tags: ["a"] }],
95
+ },
96
+ {
97
+ sessionId: "no-ref",
98
+ savedAt: "2026-04-24T12:00:00.000Z",
99
+ tasks: [{ id: "t3", isDone: true, durationMs: 1, tags: ["a"] }],
100
+ },
101
+ ];
102
+ const r = aggregateReporting(sessions, buildTagFilterSet([]), "2026-04-24", "2026-04-24", TZ, true);
103
+ expect(r.assiduityReferenceSessionCount).toBe(2);
104
+ expect(r.assiduityLateSessionCount).toBe(1);
105
+ expect(r.assiduityLateMinutesTotal).toBe(20);
106
+ expect(r.assiduityAverageLateMinutesWhenLate).toBe(20);
107
+ });
108
+
109
+ it("aggregateReportingByProject utilise la durée enregistrée sur la tâche parente", () => {
110
+ const sessions = [
111
+ {
112
+ sessionId: "s1",
113
+ archived: false,
114
+ tasks: [
115
+ {
116
+ isDone: true,
117
+ project: "acme",
118
+ durationMs: 6 * 60_000,
119
+ startTime: "2026-04-23T10:00:00.000Z",
120
+ endTime: "2026-04-23T10:06:00.000Z",
121
+ tags: ["default"],
122
+ subtasks: [{ done: true, durationMs: 6 * 60_000 }],
123
+ },
124
+ ],
125
+ },
126
+ ];
127
+ const rows = aggregateReportingByProject(sessions, buildTagFilterSet([]), "2026-04-23", "2026-04-23", TZ, true);
128
+ const acme = rows.find((r) => r.projectKey === "acme");
129
+ expect(acme?.minutes).toBe(6);
130
+ });
131
+
132
+ it("aggregateTagTaskMinutesByDayAndWeek compte la durée complète pour chaque étiquette d’une tâche", () => {
133
+ const sessions = [
134
+ {
135
+ sessionId: "s1",
136
+ archived: false,
137
+ tasks: [
138
+ {
139
+ isDone: true,
140
+ durationMs: 60 * 60_000,
141
+ startTime: "2026-04-23T10:00:00.000Z",
142
+ endTime: "2026-04-23T11:00:00.000Z",
143
+ tags: ["alpha", "beta"],
144
+ subtasks: [],
145
+ },
146
+ ],
147
+ },
148
+ ];
149
+ const { byDay } = aggregateTagTaskMinutesByDayAndWeek(
150
+ sessions,
151
+ buildTagFilterSet([]),
152
+ "2026-04-23",
153
+ "2026-04-23",
154
+ TZ,
155
+ "monday",
156
+ "défaut",
157
+ true
158
+ );
159
+ const alpha = byDay.find((r) => r.tagKey === "alpha");
160
+ const beta = byDay.find((r) => r.tagKey === "beta");
161
+ expect(alpha?.minutes).toBe(60);
162
+ expect(beta?.minutes).toBe(60);
163
+ });
164
+
165
+ it("mergeSessionsFromPayload fusionne courant et historique", () => {
166
+ const payload = {
167
+ history: [{ sessionId: "a", archived: false }],
168
+ historyArchived: [{ sessionId: "b", archived: true }],
169
+ current: { sessionId: "c", archived: false },
170
+ } as unknown as KronosysUpdatePayload;
171
+ const merged = mergeSessionsFromPayload(payload);
172
+ const ids = merged.map((s) => s.sessionId).sort();
173
+ expect(ids).toEqual(["a", "b", "c"]);
174
+ });
175
+
176
+ it("collectTasksDeduped fusionne tasks et activeTasks sans doublons", () => {
177
+ const s = {
178
+ sessionId: "s1",
179
+ tasks: [{ id: "t1", name: "T1" }],
180
+ activeTasks: [{ id: "t1", name: "T1-active" }, { id: "t2", name: "T2" }],
181
+ };
182
+ const tasks = collectTasksDeduped(s);
183
+ expect(tasks).toHaveLength(2);
184
+ expect(tasks.find(t => t.id === "t1")?.name).toBe("T1"); // Priorité à la liste tasks existante
185
+ expect(tasks.find(t => t.id === "t2")).toBeDefined();
186
+ });
187
+
188
+ it("buildTagFilterSet normalise les tags", () => {
189
+ const set = buildTagFilterSet(["#Bug", "Feature"]);
190
+ expect(set.has("bug")).toBe(true);
191
+ expect(set.has("feature")).toBe(true);
192
+ expect(set.size).toBe(2);
193
+ });
194
+
195
+ it("dayInRange respecte les bornes", () => {
196
+ expect(dayInRange("2026-04-20", "2026-04-01", "2026-04-30")).toBe(true);
197
+ expect(dayInRange("2026-04-20", "2026-04-21", "2026-04-30")).toBe(false);
198
+ expect(dayInRange("2026-04-20", null, null)).toBe(true);
199
+ expect(dayInRange(null, "2026-04-01", null)).toBe(false);
200
+ });
201
+
202
+ it("localWeekMondayFromDayKey renvoie le lundi", () => {
203
+ // 2026-04-29 est un mercredi
204
+ expect(localWeekMondayFromDayKey("2026-04-29")).toBe("2026-04-27");
205
+ });
206
+
207
+ it("sessionWallClockMinutes utilise la durée persistée si valide", () => {
208
+ expect(sessionWallClockMinutes({ sessionId: "x", sessionDurationMinutes: 15 })).toBe(15);
209
+ });
210
+
211
+ it("sessionWallClockMinutes calcule à partir de start/end si non persisté", () => {
212
+ const s = {
213
+ sessionId: "x",
214
+ startAt: "2026-04-29T10:00:00Z",
215
+ endAt: "2026-04-29T10:10:00Z",
216
+ };
217
+ expect(sessionWallClockMinutes(s)).toBe(10);
218
+ });
219
+ });
220
+
221
+ describe("taskMatchesTags", () => {
222
+ it("true si filtre vide", () => {
223
+ expect(taskMatchesTags({ tags: ["dev"] }, new Set())).toBe(true);
224
+ });
225
+
226
+ it("filtre les tâches par tags", () => {
227
+ const filter = new Set(["bug"]);
228
+ expect(taskMatchesTags({ tags: ["bug"] }, filter)).toBe(true);
229
+ expect(taskMatchesTags({ tags: ["feature"] }, filter)).toBe(false);
230
+ });
231
+
232
+ it("gère le default bucket pour les tâches sans tags", () => {
233
+ const filter = new Set(["default"]);
234
+ expect(taskMatchesTags({ tags: [] }, filter, true)).toBe(true);
235
+ expect(taskMatchesTags({ tags: [] }, filter, false)).toBe(false);
236
+ });
237
+ });
238
+
239
+ describe("aggregations", () => {
240
+ const sessions = [
241
+ {
242
+ sessionId: "s1",
243
+ savedAt: "2026-04-29T12:00:00Z",
244
+ tasks: [
245
+ {
246
+ id: "t1",
247
+ durationMs: 600000, // 10 min
248
+ isDone: true,
249
+ project: "acme",
250
+ tags: ["dev"],
251
+ startTime: "2026-04-29T10:00:00Z",
252
+ },
253
+ ],
254
+ },
255
+ {
256
+ sessionId: "arch-1",
257
+ archived: true,
258
+ tasks: [
259
+ {
260
+ id: "t2",
261
+ durationMs: 300000, // 5 min
262
+ isDone: false,
263
+ tags: ["bug"],
264
+ startTime: "2026-04-29T11:00:00Z",
265
+ }
266
+ ]
267
+ }
268
+ ];
269
+
270
+ it("aggregateReportingByProject regroupe correctement", () => {
271
+ const result = aggregateReportingByProject(sessions, new Set(), null, null, TZ, true);
272
+ const acme = result.find(r => r.projectKey === "acme");
273
+ expect(acme?.minutes).toBe(10);
274
+ expect(acme?.taskCount).toBe(1);
275
+ });
276
+
277
+ it("aggregateProjectTaskMinutesByDay regroupe par projet et jour", () => {
278
+ const result = aggregateProjectTaskMinutesByDay(sessions, new Set(), null, null, TZ, true);
279
+ expect(result).toHaveLength(1); // Seul t1 (acme) est inclus ; t2 est exclu car archivé+non-terminé
280
+ const acmeDay = result.find(r => r.projectKey === "acme");
281
+ expect(acmeDay?.minutes).toBe(10);
282
+ expect(acmeDay?.day).toBe("2026-04-29");
283
+ });
284
+
285
+ it("aggregateTagTaskMinutesByDayAndWeek répartit le temps", () => {
286
+ const sMulti = [{
287
+ sessionId: "sm",
288
+ tasks: [{
289
+ durationMs: 60000,
290
+ tags: ["a", "b"],
291
+ startTime: "2026-04-27T10:00:00Z"
292
+ }]
293
+ }];
294
+ const { byDay } = aggregateTagTaskMinutesByDayAndWeek(sMulti, new Set(), null, null, TZ, "monday", "default", true);
295
+ expect(byDay).toHaveLength(2);
296
+ expect(byDay[0].minutes).toBe(1);
297
+ expect(byDay[1].minutes).toBe(1);
298
+ });
299
+
300
+ it("aggregateReporting calcule les totaux globaux", () => {
301
+ const res = aggregateReporting(sessions, new Set(), null, null, TZ, true);
302
+ expect(res.taskMinutesTotal).toBe(10); // t2 est exclue car archivée et non terminée
303
+ expect(res.taskCountContributing).toBe(1);
304
+ expect(res.sessionCountContributing).toBe(2);
305
+ });
306
+
307
+ it("aggregateArchivedExcludedTaskMinutes compte t2", () => {
308
+ const m = aggregateArchivedExcludedTaskMinutes(sessions, new Set(), null, null, TZ, true);
309
+ expect(m).toBe(5);
310
+ });
311
+ });
312
+
313
+ describe("helpers", () => {
314
+ it("sortedDayKeys combine et trie", () => {
315
+ const m1 = { "2026-04-30": 1, "2026-04-28": 1 };
316
+ const m2 = { "2026-04-29": 1 };
317
+ expect(sortedDayKeys(m1, m2)).toEqual(["2026-04-28", "2026-04-29", "2026-04-30"]);
318
+ });
319
+
320
+ it("maxBucket trouve le maximum", () => {
321
+ const m1 = { a: 10, b: 5 };
322
+ const m2 = { c: 20 };
323
+ expect(maxBucket([m1, m2])).toBe(20);
324
+ });
325
+ });