@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
|
@@ -114,6 +114,8 @@ export type ReportingMetricHelpBlock = {
|
|
|
114
114
|
metricHelpAssiduityLateTotalBody: string;
|
|
115
115
|
metricHelpAssiduityAvgLateAria: string;
|
|
116
116
|
metricHelpAssiduityAvgLateBody: string;
|
|
117
|
+
metricHelpClosureBreakdownAria: string;
|
|
118
|
+
metricHelpClosureBreakdownBody: string;
|
|
117
119
|
};
|
|
118
120
|
|
|
119
121
|
export const reportingMetricHelpEn: ReportingMetricHelpBlock = {
|
|
@@ -269,6 +271,9 @@ export const reportingMetricHelpEn: ReportingMetricHelpBlock = {
|
|
|
269
271
|
metricHelpAssiduityAvgLateAria: "Help: average late when late",
|
|
270
272
|
metricHelpAssiduityAvgLateBody:
|
|
271
273
|
"Cumulative late minutes divided by the number of late sessions (unchanged if there are zero late sessions, shown as —). A simple punctuality read when a reference time was stored.",
|
|
274
|
+
metricHelpClosureBreakdownAria: "Help: sessions by closure type",
|
|
275
|
+
metricHelpClosureBreakdownBody:
|
|
276
|
+
"How many included sessions fall into each closure category saved when the session ended (planned, early, overrun, other). “Unspecified” covers open sessions and ended sessions where no category was saved. Same session-day and tag rules as “Sessions (in range)”; the five counts sum to that total.",
|
|
272
277
|
};
|
|
273
278
|
|
|
274
279
|
export const reportingMetricHelpFr: ReportingMetricHelpBlock = {
|
|
@@ -427,4 +432,7 @@ export const reportingMetricHelpFr: ReportingMetricHelpBlock = {
|
|
|
427
432
|
metricHelpAssiduityAvgLateAria: "Aide : retard moyen (lorsqu’en retard)",
|
|
428
433
|
metricHelpAssiduityAvgLateBody:
|
|
429
434
|
"Retard cumulé divisé par le nombre de sessions en retard ; indiqué en « — » s’il n’y en a pas. Pertinent lorsqu’une heure de début a été enregistrée côté hôte.",
|
|
435
|
+
metricHelpClosureBreakdownAria: "Aide : sessions par type de clôture",
|
|
436
|
+
metricHelpClosureBreakdownBody:
|
|
437
|
+
"Nombre de sessions incluses par catégorie enregistrée à la fin de session (conforme au prévu, anticipée, prolongation, autre). « Sans catégorie » regroupe les sessions encore ouvertes et celles terminées sans choix enregistré. Mêmes règles de jour de session et d’étiquettes que « Sessions (plage) » ; la somme des cinq lignes égale ce total.",
|
|
430
438
|
};
|
package/lib/reportingStrings.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Lang } from "./dashboardCopy";
|
|
2
|
+
import { dashboardStrings } from "./dashboardCopy";
|
|
2
3
|
import {
|
|
3
4
|
reportingMetricHelpEn,
|
|
4
5
|
reportingMetricHelpFr,
|
|
@@ -9,8 +10,13 @@ export type ReportingNavStrings = {
|
|
|
9
10
|
dashboard: string;
|
|
10
11
|
reporting: string;
|
|
11
12
|
settings: string;
|
|
13
|
+
logs: string;
|
|
12
14
|
/** Guide d’utilisation in-app (`/guide`). */
|
|
13
15
|
guide: string;
|
|
16
|
+
/** Page inventaire des user stories (`/implementation`). */
|
|
17
|
+
implementation: string;
|
|
18
|
+
/** Infobulle lorsque l’icône tableau de bord pulse (rappel conflit minuteurs). */
|
|
19
|
+
dashboardAttentionHint?: string;
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
export type ReportingStrings = ReportingNavStrings &
|
|
@@ -87,6 +93,8 @@ export type ReportingStrings = ReportingNavStrings &
|
|
|
87
93
|
tableActive: string;
|
|
88
94
|
/** Temps enregistré sur les tâches (durationMs), par jour de tâche. */
|
|
89
95
|
tableTaskTime: string;
|
|
96
|
+
/** Temps non concurrent (union des intervalles horodatés), par jour de tâche. */
|
|
97
|
+
tableTaskTimeNonConcurrent: string;
|
|
90
98
|
/** Temps de codage session (codingMinutesSession), par jour de session. */
|
|
91
99
|
tableSessionCoding: string;
|
|
92
100
|
tableSessionWall: string;
|
|
@@ -149,6 +157,11 @@ export type ReportingStrings = ReportingNavStrings &
|
|
|
149
157
|
tocNavAria: string;
|
|
150
158
|
/** Libellé court pour la grille d’indicateurs (pas de titre visible dans la section). */
|
|
151
159
|
tocSummaryKpis: string;
|
|
160
|
+
/** Sessions regroupées par raison de clôture (sous-bloc du résumé). */
|
|
161
|
+
tocClosureBreakdown: string;
|
|
162
|
+
closureBreakdownTitle: string;
|
|
163
|
+
/** Sessions sans catégorie de clôture enregistrée (ou session encore ouverte). */
|
|
164
|
+
closureBreakdownUnspecified: string;
|
|
152
165
|
/** Libellé pour le tableau jour / sessions / tâches. */
|
|
153
166
|
tocDailyTable: string;
|
|
154
167
|
tocTagTimeSection: string;
|
|
@@ -170,7 +183,9 @@ const en: ReportingStrings = {
|
|
|
170
183
|
dashboard: "Dashboard",
|
|
171
184
|
reporting: "Reporting",
|
|
172
185
|
settings: "Settings",
|
|
186
|
+
logs: "Action logs",
|
|
173
187
|
guide: "User guide",
|
|
188
|
+
implementation: "User stories — implementation",
|
|
174
189
|
title: "Reporting",
|
|
175
190
|
subtitle:
|
|
176
191
|
"Aggregates from Kronosys history (local API). Filter by date range and tags. Task time by @project is included.",
|
|
@@ -197,7 +212,7 @@ const en: ReportingStrings = {
|
|
|
197
212
|
|
|
198
213
|
• Workspace folder snapshot (lines in the open project): ignores filters — not Kronosys history.
|
|
199
214
|
|
|
200
|
-
• Summary KPIs, charts, day-by-day table: filters apply. Session-day side: session count, session coding/active/wall time, KronoFocus sessions completed, lines written, and (when the host stored a planned start) punctuality aggregates: reference count, late session count, cumulative and average late minutes. Task-day side: task rows, recorded task time, done/in-progress counts, task KronoFocus used/cycles.
|
|
215
|
+
• Summary KPIs, charts, day-by-day table: filters apply. Session-day side: session count, session coding/active/wall time, KronoFocus sessions completed, lines written, breakdown of sessions by closure type when the host saved one at session end, and (when the host stored a planned start) punctuality aggregates: reference count, late session count, cumulative and average late minutes. Task-day side: task rows, recorded task time, done/in-progress counts, task KronoFocus used/cycles.
|
|
201
216
|
|
|
202
217
|
• “Lines by language” and “Coding signals” tables: session day within the range; with tags selected, a whole session is dropped if no task matches (same rule as elsewhere).
|
|
203
218
|
|
|
@@ -249,6 +264,7 @@ Archived sessions: task-based metrics only count fully completed tasks (subtasks
|
|
|
249
264
|
tableDone: "Done",
|
|
250
265
|
tableActive: "Active",
|
|
251
266
|
tableTaskTime: "Task time",
|
|
267
|
+
tableTaskTimeNonConcurrent: "Task time (non-concurrent)",
|
|
252
268
|
tableSessionCoding: "Session coding",
|
|
253
269
|
tableSessionWall: "Session span",
|
|
254
270
|
undatedLabel: "Undated",
|
|
@@ -307,6 +323,9 @@ Archived sessions: task-based metrics only count fully completed tasks (subtasks
|
|
|
307
323
|
tocTitle: "On this page",
|
|
308
324
|
tocNavAria: "Reporting — sections on this page",
|
|
309
325
|
tocSummaryKpis: "Summary indicators",
|
|
326
|
+
tocClosureBreakdown: "Sessions by closure type",
|
|
327
|
+
closureBreakdownTitle: "Sessions by closure type",
|
|
328
|
+
closureBreakdownUnspecified: "Unspecified (open or no category)",
|
|
310
329
|
tocDailyTable: "Day-by-day table",
|
|
311
330
|
tocTagTimeSection: "Time by tag",
|
|
312
331
|
weekNavPrev: "Previous week",
|
|
@@ -326,7 +345,9 @@ const fr: ReportingStrings = {
|
|
|
326
345
|
dashboard: "Tableau de bord",
|
|
327
346
|
reporting: "Rapports",
|
|
328
347
|
settings: "Paramètres",
|
|
348
|
+
logs: "Journal des actions",
|
|
329
349
|
guide: "Guide d’utilisation",
|
|
350
|
+
implementation: "User stories — implémentation",
|
|
330
351
|
title: "Rapports",
|
|
331
352
|
subtitle:
|
|
332
353
|
"Agrégats à partir de l’historique Kronosys (API locale). Filtrez par plage de dates et par étiquettes. Le temps par @projet est inclus.",
|
|
@@ -354,7 +375,7 @@ const fr: ReportingStrings = {
|
|
|
354
375
|
|
|
355
376
|
• Instantané « Dossier ouvert » (lignes par langage du workspace) : hors filtres — ce n’est pas l’historique Kronosys.
|
|
356
377
|
|
|
357
|
-
• Indicateurs de synthèse, graphiques, tableau par jour : la plage et les étiquettes s’appliquent. Côté jour de session : nombre de sessions, temps de codage et d’activité, durée murale, KronoFocus « sessions terminées », lignes écrites, et (si l’hôte a enregistré un début prévu) indicateurs d’assiduité : sessions avec référence, sessions en retard, retard cumulé, retard moyen lorsqu’en retard. Côté jour de tâche : lignes de tâches, temps enregistré, tâches terminées / en cours, KronoFocus côté tâches.
|
|
378
|
+
• Indicateurs de synthèse, graphiques, tableau par jour : la plage et les étiquettes s’appliquent. Côté jour de session : nombre de sessions, temps de codage et d’activité, durée murale, KronoFocus « sessions terminées », lignes écrites, ventilation des sessions par type de clôture lorsque l’hôte en a enregistré un à la fin de session, et (si l’hôte a enregistré un début prévu) indicateurs d’assiduité : sessions avec référence, sessions en retard, retard cumulé, retard moyen lorsqu’en retard. Côté jour de tâche : lignes de tâches, temps enregistré, tâches terminées / en cours, KronoFocus côté tâches.
|
|
358
379
|
|
|
359
380
|
• Tableaux « Lignes par langage » et « Signaux de codage » : jour de session dans la plage ; avec étiquettes sélectionnées, toute la session est exclue si aucune tâche ne correspond (même règle qu’ailleurs).
|
|
360
381
|
|
|
@@ -407,6 +428,7 @@ Sessions archivées : pour les métriques basées sur les tâches, seules les t
|
|
|
407
428
|
tableDone: "Terminées",
|
|
408
429
|
tableActive: "En cours",
|
|
409
430
|
tableTaskTime: "Temps (tâches)",
|
|
431
|
+
tableTaskTimeNonConcurrent: "Temps (tâches, non concurrent)",
|
|
410
432
|
tableSessionCoding: "Codage (sessions)",
|
|
411
433
|
tableSessionWall: "Durée session",
|
|
412
434
|
undatedLabel: "Sans date",
|
|
@@ -466,6 +488,9 @@ Sessions archivées : pour les métriques basées sur les tâches, seules les t
|
|
|
466
488
|
tocTitle: "Sur cette page",
|
|
467
489
|
tocNavAria: "Rapports — sections de la page",
|
|
468
490
|
tocSummaryKpis: "Indicateurs de synthèse",
|
|
491
|
+
tocClosureBreakdown: "Sessions par type de clôture",
|
|
492
|
+
closureBreakdownTitle: "Sessions par type de clôture",
|
|
493
|
+
closureBreakdownUnspecified: "Sans catégorie (ouverte ou non enregistrée)",
|
|
469
494
|
tocDailyTable: "Tableau par jour",
|
|
470
495
|
tocTagTimeSection: "Temps par étiquette",
|
|
471
496
|
weekNavPrev: "Semaine précédente",
|
|
@@ -487,5 +512,14 @@ export function reportingStrings(lang: Lang): ReportingStrings {
|
|
|
487
512
|
|
|
488
513
|
export function reportingNav(lang: Lang): ReportingNavStrings {
|
|
489
514
|
const s = reportingStrings(lang);
|
|
490
|
-
|
|
515
|
+
const d = dashboardStrings(lang);
|
|
516
|
+
return {
|
|
517
|
+
dashboard: s.dashboard,
|
|
518
|
+
reporting: s.reporting,
|
|
519
|
+
settings: s.settings,
|
|
520
|
+
logs: s.logs,
|
|
521
|
+
guide: s.guide,
|
|
522
|
+
implementation: s.implementation,
|
|
523
|
+
dashboardAttentionHint: d.navDashboardPlannedConflictPulseHint,
|
|
524
|
+
};
|
|
491
525
|
}
|
package/lib/sessionListMerge.ts
CHANGED
|
@@ -7,6 +7,7 @@ type LiveLike = {
|
|
|
7
7
|
createdAt?: string | null;
|
|
8
8
|
startAt?: string | null;
|
|
9
9
|
endAt?: string | null;
|
|
10
|
+
sessionNote?: string;
|
|
10
11
|
sessionEndReasonKind?: unknown;
|
|
11
12
|
sessionEndReasonNote?: unknown;
|
|
12
13
|
sessionDurationMinutes?: number;
|
|
@@ -66,5 +67,8 @@ export function mergeLiveSessionIntoHistory(
|
|
|
66
67
|
if (typeof live?.sessionEndReasonNote === "string" && live.sessionEndReasonNote.trim() !== "") {
|
|
67
68
|
entry.sessionEndReasonNote = live.sessionEndReasonNote.trim();
|
|
68
69
|
}
|
|
70
|
+
if (typeof live?.sessionNote === "string") {
|
|
71
|
+
entry.sessionNote = live.sessionNote;
|
|
72
|
+
}
|
|
69
73
|
return [entry, ...without];
|
|
70
74
|
}
|
|
@@ -2,53 +2,214 @@
|
|
|
2
2
|
* Compteurs de tâches pour la carte « session sélectionnée », alignés sur {@link TaskFocusPanel}.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { isTaskDisplayPlanned } from "./temporalDisplayPlanned";
|
|
6
|
+
|
|
7
|
+
type TaskLike = {
|
|
8
|
+
id?: string;
|
|
9
|
+
isDone?: boolean;
|
|
10
|
+
manualTaskTimerPaused?: boolean;
|
|
11
|
+
startTime?: string;
|
|
12
|
+
endTime?: string;
|
|
13
|
+
};
|
|
14
|
+
type SubtaskLike = { id?: string; durationMs?: number };
|
|
15
|
+
type SessionTaskLike = TaskLike & {
|
|
16
|
+
durationMs?: number;
|
|
17
|
+
subtasks?: SubtaskLike[];
|
|
18
|
+
activeSubtaskTimerId?: string | null;
|
|
19
|
+
subtaskTimerStartedAt?: string | number;
|
|
20
|
+
mainTimerSegmentStartedAt?: string | null;
|
|
21
|
+
};
|
|
6
22
|
|
|
7
23
|
type SessionLike = {
|
|
8
|
-
tasks?:
|
|
9
|
-
activeTasks?:
|
|
10
|
-
activeTask?:
|
|
24
|
+
tasks?: SessionTaskLike[];
|
|
25
|
+
activeTasks?: SessionTaskLike[];
|
|
26
|
+
activeTask?: SessionTaskLike | null;
|
|
11
27
|
/** Session clôturée : pas de compteurs « en cours » ou « en liste » pour l’affichage carte session. */
|
|
12
28
|
endAt?: string | null;
|
|
13
29
|
};
|
|
30
|
+
const SUBTASK_TIMER_STARTED_AT = "subtaskTimerStartedAt";
|
|
31
|
+
const MAIN_TIMER_SEGMENT_STARTED_AT = "mainTimerSegmentStartedAt";
|
|
14
32
|
|
|
15
33
|
function sessionClosedForDisplay(session: SessionLike | null | undefined): boolean {
|
|
16
34
|
return typeof session?.endAt === "string" && session.endAt.trim() !== "";
|
|
17
35
|
}
|
|
18
36
|
|
|
19
|
-
function activeStackFromSession(session: SessionLike | null | undefined):
|
|
37
|
+
function activeStackFromSession(session: SessionLike | null | undefined): SessionTaskLike[] {
|
|
20
38
|
if (!session) {
|
|
21
39
|
return [];
|
|
22
40
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
const hasActiveTasks = Array.isArray(session.activeTasks) && session.activeTasks.length > 0;
|
|
42
|
+
let raw: Array<SessionTaskLike | null | undefined> = [];
|
|
43
|
+
if (hasActiveTasks) {
|
|
44
|
+
raw = session.activeTasks ?? [];
|
|
45
|
+
} else if (session.activeTask) {
|
|
46
|
+
raw = [session.activeTask];
|
|
47
|
+
}
|
|
48
|
+
return raw.filter((t): t is SessionTaskLike => Boolean(t));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function asNonNegativeIntMs(raw: unknown): number {
|
|
52
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
return Math.max(0, Math.floor(raw));
|
|
30
56
|
}
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
function readTimerStartMs(raw: unknown): number | null {
|
|
59
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
60
|
+
const parsed = Date.parse(raw);
|
|
61
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
62
|
+
}
|
|
63
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
64
|
+
return raw;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function collectUniqueSessionTasks(session: SessionLike | null | undefined): SessionTaskLike[] {
|
|
70
|
+
const taskRows = new Map<string, SessionTaskLike>();
|
|
71
|
+
const pushTask = (task: SessionTaskLike | null | undefined) => {
|
|
72
|
+
if (!task) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const id = typeof task.id === "string" ? task.id.trim() : "";
|
|
76
|
+
if (!id || taskRows.has(id)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
taskRows.set(id, task);
|
|
80
|
+
};
|
|
81
|
+
for (const row of session?.activeTasks ?? []) {
|
|
82
|
+
pushTask(row);
|
|
83
|
+
}
|
|
84
|
+
pushTask(session?.activeTask ?? null);
|
|
85
|
+
for (const row of session?.tasks ?? []) {
|
|
86
|
+
pushTask(row);
|
|
87
|
+
}
|
|
88
|
+
return [...taskRows.values()];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getTaskSubtasksSnapshot(task: SessionTaskLike): {
|
|
92
|
+
subtaskCount: number;
|
|
93
|
+
subtaskStoredMs: number;
|
|
94
|
+
} {
|
|
95
|
+
const subtasks = Array.isArray(task.subtasks) ? task.subtasks : [];
|
|
96
|
+
let subtaskStoredMs = 0;
|
|
97
|
+
for (const subtask of subtasks) {
|
|
98
|
+
subtaskStoredMs += asNonNegativeIntMs(subtask?.durationMs);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
subtaskCount: subtasks.length,
|
|
102
|
+
subtaskStoredMs,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getTaskInflightTimerMs(task: SessionTaskLike, nowMs: number): {
|
|
107
|
+
taskInflightMs: number;
|
|
108
|
+
subtaskInflightMs: number;
|
|
109
|
+
} {
|
|
110
|
+
if (isTaskDisplayPlanned(task, nowMs)) {
|
|
111
|
+
return { taskInflightMs: 0, subtaskInflightMs: 0 };
|
|
112
|
+
}
|
|
113
|
+
const activeSubtaskId =
|
|
114
|
+
typeof task.activeSubtaskTimerId === "string" ? task.activeSubtaskTimerId.trim() : "";
|
|
115
|
+
if (activeSubtaskId) {
|
|
116
|
+
const startedMs = readTimerStartMs(task[SUBTASK_TIMER_STARTED_AT]);
|
|
117
|
+
const subtaskInflightMs =
|
|
118
|
+
startedMs === null ? 0 : Math.max(0, Math.floor(nowMs - startedMs));
|
|
119
|
+
return { taskInflightMs: subtaskInflightMs, subtaskInflightMs };
|
|
120
|
+
}
|
|
121
|
+
const startedMs = readTimerStartMs(task[MAIN_TIMER_SEGMENT_STARTED_AT]);
|
|
122
|
+
const taskInflightMs = startedMs === null ? 0 : Math.max(0, Math.floor(nowMs - startedMs));
|
|
123
|
+
return { taskInflightMs, subtaskInflightMs: 0 };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Tâches avec minuteur actif (!terminé, pas en pause manuelle, pas « planifiée » dans le futur). */
|
|
127
|
+
export function countRunningTasksOnTimer(
|
|
128
|
+
session: SessionLike | null | undefined,
|
|
129
|
+
nowMs: number = Date.now(),
|
|
130
|
+
): number {
|
|
131
|
+
return activeStackFromSession(session).filter(
|
|
132
|
+
(t) =>
|
|
133
|
+
!t.isDone &&
|
|
134
|
+
!t.manualTaskTimerPaused &&
|
|
135
|
+
!isTaskDisplayPlanned(t, nowMs),
|
|
136
|
+
).length;
|
|
35
137
|
}
|
|
36
138
|
|
|
37
139
|
/**
|
|
38
|
-
* Même regroupement que le panneau tâches : minuteur actif, liste des non terminées, terminées.
|
|
140
|
+
* Même regroupement que le panneau tâches : minuteur actif, planifiées, liste des autres non terminées, terminées.
|
|
39
141
|
*/
|
|
40
|
-
export function countSessionTasksForSidebar(
|
|
142
|
+
export function countSessionTasksForSidebar(
|
|
143
|
+
session: SessionLike | null | undefined,
|
|
144
|
+
nowMs: number = Date.now(),
|
|
145
|
+
): {
|
|
41
146
|
running: number;
|
|
147
|
+
planned: number;
|
|
42
148
|
pausedList: number;
|
|
43
149
|
completed: number;
|
|
44
150
|
} {
|
|
45
|
-
const taskList = session?.tasks ?? [];
|
|
46
|
-
const completed = taskList.filter((t) => t.isDone === true).length;
|
|
47
151
|
const closed = sessionClosedForDisplay(session);
|
|
48
|
-
const
|
|
152
|
+
const unique = collectUniqueSessionTasks(session);
|
|
153
|
+
let planned = 0;
|
|
154
|
+
let completed = 0;
|
|
155
|
+
let pausedList = 0;
|
|
156
|
+
for (const t of unique) {
|
|
157
|
+
if (isTaskDisplayPlanned(t, nowMs)) {
|
|
158
|
+
planned += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (t.isDone === true) {
|
|
162
|
+
completed += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (closed) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const onStack = activeStackFromSession(session).some(
|
|
169
|
+
(a) => String(a.id) === String(t.id),
|
|
170
|
+
);
|
|
171
|
+
const stackRunning =
|
|
172
|
+
onStack && !t.isDone && !t.manualTaskTimerPaused && !isTaskDisplayPlanned(t, nowMs);
|
|
173
|
+
if (!stackRunning) {
|
|
174
|
+
pausedList += 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
49
177
|
return {
|
|
50
|
-
running: closed ? 0 : countRunningTasksOnTimer(session),
|
|
178
|
+
running: closed ? 0 : countRunningTasksOnTimer(session, nowMs),
|
|
179
|
+
planned,
|
|
51
180
|
pausedList,
|
|
52
181
|
completed,
|
|
53
182
|
};
|
|
54
183
|
}
|
|
184
|
+
|
|
185
|
+
export function getSessionTaskTimerBreakdown(
|
|
186
|
+
session: SessionLike | null | undefined,
|
|
187
|
+
nowMs: number = Date.now(),
|
|
188
|
+
): {
|
|
189
|
+
taskCount: number;
|
|
190
|
+
subtaskCount: number;
|
|
191
|
+
taskTimersTotalMs: number;
|
|
192
|
+
subtaskTimersTotalMs: number;
|
|
193
|
+
} {
|
|
194
|
+
const tasks = collectUniqueSessionTasks(session);
|
|
195
|
+
|
|
196
|
+
let taskTimersTotalMs = 0;
|
|
197
|
+
let subtaskTimersTotalMs = 0;
|
|
198
|
+
let subtaskCount = 0;
|
|
199
|
+
|
|
200
|
+
for (const task of tasks) {
|
|
201
|
+
const baseMs = asNonNegativeIntMs(task.durationMs);
|
|
202
|
+
const subtaskSnapshot = getTaskSubtasksSnapshot(task);
|
|
203
|
+
const inflight = getTaskInflightTimerMs(task, nowMs);
|
|
204
|
+
subtaskCount += subtaskSnapshot.subtaskCount;
|
|
205
|
+
taskTimersTotalMs += baseMs + inflight.taskInflightMs;
|
|
206
|
+
subtaskTimersTotalMs += subtaskSnapshot.subtaskStoredMs + inflight.subtaskInflightMs;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
taskCount: tasks.length,
|
|
211
|
+
subtaskCount,
|
|
212
|
+
taskTimersTotalMs,
|
|
213
|
+
subtaskTimersTotalMs,
|
|
214
|
+
};
|
|
215
|
+
}
|