@nightkatana/kronosys-app 1.0.0-beta.2 → 1.0.0-beta.21
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/README.md +28 -1
- package/app/api/action/route.ts +39 -3
- package/app/api/action-logs/route.ts +24 -0
- package/app/api/backup/route.ts +1 -1
- package/app/api/restore/route.ts +145 -0
- package/app/changelog/page.tsx +71 -4
- package/app/globals.css +127 -0
- package/app/guide/page.tsx +61 -15
- package/app/implementation/page.tsx +700 -0
- package/app/layout.tsx +14 -3
- package/app/licenses/page.tsx +99 -37
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +784 -229
- package/app/reporting/page.tsx +1266 -474
- package/app/settings/page.tsx +252 -18
- package/bin/kronosys.mjs +140 -15
- package/components/KronosysPayloadProvider.tsx +2 -0
- package/components/RouteTransition.tsx +18 -0
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
- package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +323 -48
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +211 -20
- package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
- package/components/dashboard/ReportingTour.tsx +87 -21
- package/components/dashboard/SavedProjectPicker.tsx +16 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
- package/components/dashboard/SessionListPanel.tsx +327 -44
- package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +1081 -478
- package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
- package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
- package/components/dashboard/taskFieldStyles.ts +20 -4
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +494 -84
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +177 -6
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +1188 -0
- package/lib/kronosysApi.ts +6 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +517 -75
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +37 -3
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +182 -21
- package/lib/settingsCopy.ts +178 -4
- package/lib/taskParsing.ts +360 -103
- package/lib/taskTemplateDraft.ts +135 -0
- package/lib/taskTimelineGantt.ts +265 -0
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +121 -47
- package/next.config.ts +7 -0
- package/package.json +12 -24
- package/server/actionDispatch.ts +1000 -77
- package/server/actionTaskSession.ts +337 -24
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/defaultCfg.ts +5 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +38 -2
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +66 -3
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/tsconfig.json +2 -1
- package/components/dashboard/IssuePickerModal.tsx +0 -168
- package/components/dashboard/ThemeToggle.test.tsx +0 -26
- package/lib/backupCsvExport.test.ts +0 -149
- package/lib/dashboardQuickSearchQuery.test.ts +0 -63
- package/lib/dataDir.test.ts +0 -87
- package/lib/formatIsoShort.test.ts +0 -46
- package/lib/kronoFocusRhythm.test.ts +0 -130
- package/lib/kronoFocusTimerUrgency.test.ts +0 -74
- package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
- package/lib/reportingAggregate.test.ts +0 -325
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -141
- package/lib/reportingWeekLayout.test.ts +0 -239
- package/lib/sessionAssiduity.test.ts +0 -25
- package/lib/sessionEndWarnings.test.ts +0 -200
- package/lib/sessionListMerge.test.ts +0 -101
- package/lib/sessionTaskSidebarStats.test.ts +0 -24
- package/lib/taskParsing.test.ts +0 -153
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -723
- package/server/actionTaskSession.test.ts +0 -713
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -65
- package/server/payloadStore.test.ts +0 -78
- package/server/sessionWallHydrate.test.ts +0 -46
|
@@ -1,325 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { computeReportingNonFinalFlags } from "./reportingNonFinalIndicators";
|
|
4
|
-
import type { KronosysUpdatePayload } from "./kronosysApi";
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Helpers
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
function makePayload(overrides: Partial<KronosysUpdatePayload> = {}): KronosysUpdatePayload {
|
|
10
|
-
return {
|
|
11
|
-
viewType: "dashboard",
|
|
12
|
-
cfg: {},
|
|
13
|
-
history: [],
|
|
14
|
-
historyArchived: [],
|
|
15
|
-
inspectingSessionId: null,
|
|
16
|
-
knownTags: [],
|
|
17
|
-
knownProjects: [],
|
|
18
|
-
userKnownTags: [],
|
|
19
|
-
excludedSuggestionTags: [],
|
|
20
|
-
tagDescriptions: {},
|
|
21
|
-
projectDescriptions: {},
|
|
22
|
-
gitIdentity: {},
|
|
23
|
-
dashboardDataOrigin: "local_next",
|
|
24
|
-
...overrides,
|
|
25
|
-
} as KronosysUpdatePayload;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ALL_TAGS = new Set<string>();
|
|
29
|
-
const DATE_FROM = "";
|
|
30
|
-
const DATE_TO = "";
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
describe("computeReportingNonFinalFlags", () => {
|
|
34
|
-
it("retourne tout false si payload est null", () => {
|
|
35
|
-
const flags = computeReportingNonFinalFlags(null, ALL_TAGS, DATE_FROM, DATE_TO, undefined, "America/Toronto");
|
|
36
|
-
expect(flags).toEqual({
|
|
37
|
-
sessionsNonFinal: false,
|
|
38
|
-
projectsNonFinal: false,
|
|
39
|
-
tagsNonFinal: false,
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("sessionsNonFinal=true si session live ouverte (sessionId non vide, non archivée)", () => {
|
|
44
|
-
const p = makePayload({ current: { sessionId: "ses-live" } } as Partial<KronosysUpdatePayload>);
|
|
45
|
-
const flags = computeReportingNonFinalFlags(p, ALL_TAGS, DATE_FROM, DATE_TO, undefined, "America/Toronto");
|
|
46
|
-
expect(flags.sessionsNonFinal).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("sessionsNonFinal=false si session live est archivée", () => {
|
|
50
|
-
const p = makePayload({ current: { sessionId: "ses-archived", archived: true } } as Partial<KronosysUpdatePayload>);
|
|
51
|
-
const flags = computeReportingNonFinalFlags(p, ALL_TAGS, DATE_FROM, DATE_TO, undefined, "America/Toronto");
|
|
52
|
-
expect(flags.sessionsNonFinal).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("sessionsNonFinal=true si tasksByDayActive contient des valeurs > 0", () => {
|
|
56
|
-
const p = makePayload();
|
|
57
|
-
const flags = computeReportingNonFinalFlags(
|
|
58
|
-
p, ALL_TAGS, DATE_FROM, DATE_TO, { "2026-04-29": 2 }, "America/Toronto"
|
|
59
|
-
);
|
|
60
|
-
expect(flags.sessionsNonFinal).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("sessionsNonFinal=false si tasksByDayActive est vide", () => {
|
|
64
|
-
const p = makePayload();
|
|
65
|
-
const flags = computeReportingNonFinalFlags(
|
|
66
|
-
p, ALL_TAGS, DATE_FROM, DATE_TO, { "2026-04-29": 0 }, "America/Toronto"
|
|
67
|
-
);
|
|
68
|
-
expect(flags.sessionsNonFinal).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("projectsNonFinal=true si tâche non terminée avec @projet dans l'historique", () => {
|
|
72
|
-
const p = makePayload({
|
|
73
|
-
history: [
|
|
74
|
-
{
|
|
75
|
-
sessionId: "ses-1",
|
|
76
|
-
tasks: [
|
|
77
|
-
{
|
|
78
|
-
id: "t1",
|
|
79
|
-
isDone: false,
|
|
80
|
-
project: "acme",
|
|
81
|
-
tags: [],
|
|
82
|
-
startTime: "2026-04-29T10:00:00Z",
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
} as Partial<KronosysUpdatePayload>);
|
|
88
|
-
const flags = computeReportingNonFinalFlags(p, ALL_TAGS, DATE_FROM, DATE_TO, undefined, "America/Toronto");
|
|
89
|
-
expect(flags.projectsNonFinal).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("tagsNonFinal=true si tâche non terminée avec #tag dans l'historique", () => {
|
|
93
|
-
const p = makePayload({
|
|
94
|
-
history: [
|
|
95
|
-
{
|
|
96
|
-
sessionId: "ses-1",
|
|
97
|
-
tasks: [
|
|
98
|
-
{
|
|
99
|
-
id: "t1",
|
|
100
|
-
isDone: false,
|
|
101
|
-
project: "",
|
|
102
|
-
tags: ["dev"],
|
|
103
|
-
startTime: "2026-04-29T10:00:00Z",
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
} as Partial<KronosysUpdatePayload>);
|
|
109
|
-
const flags = computeReportingNonFinalFlags(p, ALL_TAGS, DATE_FROM, DATE_TO, undefined, "America/Toronto");
|
|
110
|
-
expect(flags.tagsNonFinal).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("projectsNonFinal=false si toutes les tâches sont terminées", () => {
|
|
114
|
-
const p = makePayload({
|
|
115
|
-
history: [
|
|
116
|
-
{
|
|
117
|
-
sessionId: "ses-1",
|
|
118
|
-
tasks: [
|
|
119
|
-
{
|
|
120
|
-
id: "t1",
|
|
121
|
-
isDone: true,
|
|
122
|
-
project: "acme",
|
|
123
|
-
tags: ["dev"],
|
|
124
|
-
startTime: "2026-04-29T10:00:00Z",
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
} as Partial<KronosysUpdatePayload>);
|
|
130
|
-
const flags = computeReportingNonFinalFlags(p, ALL_TAGS, DATE_FROM, DATE_TO, undefined, "America/Toronto");
|
|
131
|
-
expect(flags.projectsNonFinal).toBe(false);
|
|
132
|
-
expect(flags.tagsNonFinal).toBe(false);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("hors de la plage de dates → non compté", () => {
|
|
136
|
-
const p = makePayload({
|
|
137
|
-
history: [
|
|
138
|
-
{
|
|
139
|
-
sessionId: "ses-1",
|
|
140
|
-
tasks: [
|
|
141
|
-
{
|
|
142
|
-
id: "t1",
|
|
143
|
-
isDone: false,
|
|
144
|
-
project: "acme",
|
|
145
|
-
tags: ["dev"],
|
|
146
|
-
startTime: "2025-01-01T10:00:00Z",
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
} as Partial<KronosysUpdatePayload>);
|
|
152
|
-
const flags = computeReportingNonFinalFlags(
|
|
153
|
-
p, ALL_TAGS, "2026-04-01", "2026-04-30", undefined, "America/Toronto"
|
|
154
|
-
);
|
|
155
|
-
expect(flags.projectsNonFinal).toBe(false);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildTagWeekDisplayBlocks,
|
|
5
|
-
groupTagDayRowsForDisplay,
|
|
6
|
-
} from "./reportingTagWeekBreakdown";
|
|
7
|
-
import type { TagWeekCalendarRow } from "./reportingWeekLayout";
|
|
8
|
-
import type { ReportingTagTimeDayRow } from "./reportingAggregate";
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
function makeWeekRow(tagKey: string, slots: number[] = [0, 0, 0, 0, 0, 0, 0]): TagWeekCalendarRow {
|
|
14
|
-
return {
|
|
15
|
-
tagKey,
|
|
16
|
-
displayTag: tagKey,
|
|
17
|
-
weekStart: "2026-04-20",
|
|
18
|
-
slots,
|
|
19
|
-
total: slots.reduce((s, v) => s + v, 0),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function makeDayRow(tagKey: string, day: string, minutes: number): ReportingTagTimeDayRow {
|
|
24
|
-
return { tagKey, displayTag: tagKey, day, minutes };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// buildTagWeekDisplayBlocks
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
describe("buildTagWeekDisplayBlocks", () => {
|
|
31
|
-
it("retourne un tableau vide pour une entrée vide", () => {
|
|
32
|
-
expect(buildTagWeekDisplayBlocks([])).toEqual([]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("étiquette globale → leaf", () => {
|
|
36
|
-
const rows = [makeWeekRow("dev")];
|
|
37
|
-
const blocks = buildTagWeekDisplayBlocks(rows);
|
|
38
|
-
expect(blocks).toHaveLength(1);
|
|
39
|
-
expect(blocks[0].kind).toBe("leaf");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("étiquette fallback (default) placée en dernier", () => {
|
|
43
|
-
const rows = [makeWeekRow("default"), makeWeekRow("design")];
|
|
44
|
-
const blocks = buildTagWeekDisplayBlocks(rows);
|
|
45
|
-
expect(blocks[blocks.length - 1].kind).toBe("leaf");
|
|
46
|
-
if (blocks[blocks.length - 1].kind === "leaf") {
|
|
47
|
-
expect(blocks[blocks.length - 1].row.tagKey).toBe("default");
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("étiquette unique projet#code → leaf (pas de rollup)", () => {
|
|
52
|
-
const rows = [makeWeekRow("acme#frontend")];
|
|
53
|
-
const blocks = buildTagWeekDisplayBlocks(rows);
|
|
54
|
-
expect(blocks).toHaveLength(1);
|
|
55
|
-
expect(blocks[0].kind).toBe("leaf");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("deux étiquettes projet#code du même projet → rollup", () => {
|
|
59
|
-
const rows = [makeWeekRow("acme#frontend", [30, 0, 0, 0, 0, 0, 0]), makeWeekRow("acme#backend", [0, 60, 0, 0, 0, 0, 0])];
|
|
60
|
-
const blocks = buildTagWeekDisplayBlocks(rows);
|
|
61
|
-
expect(blocks).toHaveLength(1);
|
|
62
|
-
expect(blocks[0].kind).toBe("rollup");
|
|
63
|
-
if (blocks[0].kind === "rollup") {
|
|
64
|
-
expect(blocks[0].children).toHaveLength(2);
|
|
65
|
-
expect(blocks[0].parentTotal).toBe(90);
|
|
66
|
-
expect(blocks[0].parentSlots[0]).toBe(30);
|
|
67
|
-
expect(blocks[0].parentSlots[1]).toBe(60);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("deux projets différents → deux rollups distincts", () => {
|
|
72
|
-
const rows = [
|
|
73
|
-
makeWeekRow("acme#a"),
|
|
74
|
-
makeWeekRow("acme#b"),
|
|
75
|
-
makeWeekRow("beta#x"),
|
|
76
|
-
makeWeekRow("beta#y"),
|
|
77
|
-
];
|
|
78
|
-
const blocks = buildTagWeekDisplayBlocks(rows);
|
|
79
|
-
const rollups = blocks.filter((b) => b.kind === "rollup");
|
|
80
|
-
expect(rollups).toHaveLength(2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("les globales sont triées alphabétiquement avant les rollups projet", () => {
|
|
84
|
-
const rows = [makeWeekRow("zzz"), makeWeekRow("aaa"), makeWeekRow("proj#x"), makeWeekRow("proj#y")];
|
|
85
|
-
const blocks = buildTagWeekDisplayBlocks(rows);
|
|
86
|
-
expect(blocks[0].kind).toBe("leaf");
|
|
87
|
-
if (blocks[0].kind === "leaf") expect(blocks[0].row.tagKey).toBe("aaa");
|
|
88
|
-
expect(blocks[1].kind).toBe("leaf");
|
|
89
|
-
if (blocks[1].kind === "leaf") expect(blocks[1].row.tagKey).toBe("zzz");
|
|
90
|
-
expect(blocks[2].kind).toBe("rollup");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// groupTagDayRowsForDisplay
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
describe("groupTagDayRowsForDisplay", () => {
|
|
98
|
-
it("retourne un tableau vide pour entrée vide", () => {
|
|
99
|
-
expect(groupTagDayRowsForDisplay([])).toEqual([]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("regroupe les lignes par jour et trie les jours", () => {
|
|
103
|
-
const rows = [
|
|
104
|
-
makeDayRow("dev", "2026-04-22", 30),
|
|
105
|
-
makeDayRow("design", "2026-04-21", 60),
|
|
106
|
-
makeDayRow("dev", "2026-04-21", 15),
|
|
107
|
-
];
|
|
108
|
-
const groups = groupTagDayRowsForDisplay(rows);
|
|
109
|
-
expect(groups).toHaveLength(2);
|
|
110
|
-
expect(groups[0].day).toBe("2026-04-21");
|
|
111
|
-
expect(groups[1].day).toBe("2026-04-22");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("chaque groupe contient les bons blocs", () => {
|
|
115
|
-
const rows = [
|
|
116
|
-
makeDayRow("dev", "2026-04-21", 30),
|
|
117
|
-
makeDayRow("design", "2026-04-21", 45),
|
|
118
|
-
];
|
|
119
|
-
const groups = groupTagDayRowsForDisplay(rows);
|
|
120
|
-
expect(groups[0].blocks).toHaveLength(2);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("projet#code unique dans un groupe → leaf", () => {
|
|
124
|
-
const rows = [makeDayRow("acme#frontend", "2026-04-21", 60)];
|
|
125
|
-
const groups = groupTagDayRowsForDisplay(rows);
|
|
126
|
-
expect(groups[0].blocks[0].kind).toBe("leaf");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("deux proj#code mêmes projet dans un groupe → rollup", () => {
|
|
130
|
-
const rows = [
|
|
131
|
-
makeDayRow("acme#frontend", "2026-04-21", 30),
|
|
132
|
-
makeDayRow("acme#backend", "2026-04-21", 90),
|
|
133
|
-
];
|
|
134
|
-
const groups = groupTagDayRowsForDisplay(rows);
|
|
135
|
-
const rollup = groups[0].blocks.find((b) => b.kind === "rollup");
|
|
136
|
-
expect(rollup).toBeDefined();
|
|
137
|
-
if (rollup?.kind === "rollup") {
|
|
138
|
-
expect(rollup.parentMinutes).toBe(120);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
});
|