@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
package/app/reporting/page.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useMemo,
|
|
10
10
|
useState,
|
|
11
11
|
} from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
12
13
|
import Link from "next/link";
|
|
13
14
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
14
15
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
@@ -18,9 +19,11 @@ import {
|
|
|
18
19
|
} from "@/lib/kronosysApi";
|
|
19
20
|
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
20
21
|
import {
|
|
22
|
+
DEFAULT_FALLBACK_TASK_TAG,
|
|
21
23
|
formatDuration,
|
|
22
24
|
formatTagDisplay,
|
|
23
25
|
isFallbackTaskTagKey,
|
|
26
|
+
parseProjectScopedTag,
|
|
24
27
|
normalizeProjectKey,
|
|
25
28
|
normalizeTagKey,
|
|
26
29
|
readTaskDefaultTagBucketEnabled,
|
|
@@ -33,15 +36,23 @@ import {
|
|
|
33
36
|
aggregateTagTaskMinutesByDayAndWeek,
|
|
34
37
|
buildTagFilterSet,
|
|
35
38
|
mergeSessionsFromPayload,
|
|
39
|
+
collectTasksDeduped,
|
|
40
|
+
dayInRange,
|
|
41
|
+
taskCountsTowardArchivedSessionReporting,
|
|
42
|
+
taskMatchesTags,
|
|
36
43
|
sortedDayKeys,
|
|
44
|
+
REPORTING_SESSION_CLOSURE_DISPLAY_ORDER,
|
|
37
45
|
type ReportingTagTimeDayRow,
|
|
38
|
-
type ReportingTagTimeWeekRow,
|
|
39
46
|
} from "@/lib/reportingAggregate";
|
|
40
47
|
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
41
48
|
import {
|
|
42
49
|
appShellHeaderClassName,
|
|
43
|
-
|
|
50
|
+
appShellHeaderTitleMetaRowClassName,
|
|
51
|
+
appShellHeaderToolbarClassName,
|
|
44
52
|
} from "@/lib/appShellHeaderClasses";
|
|
53
|
+
import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
|
|
54
|
+
import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
|
|
55
|
+
import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
|
|
45
56
|
import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
|
|
46
57
|
import {
|
|
47
58
|
reportingPresetDay,
|
|
@@ -62,7 +73,6 @@ import {
|
|
|
62
73
|
buildTagWeekCalendarRows,
|
|
63
74
|
formatWeekRangeLabel,
|
|
64
75
|
localWeekStartKeyFromDayKey,
|
|
65
|
-
readWeekStartsOnFromStorage,
|
|
66
76
|
type ReportingWeekStartsOn,
|
|
67
77
|
weekdayDateColumnHeaders,
|
|
68
78
|
writeWeekStartsOnToStorage,
|
|
@@ -70,29 +80,85 @@ import {
|
|
|
70
80
|
type TagWeekCalendarRow,
|
|
71
81
|
} from "@/lib/reportingWeekLayout";
|
|
72
82
|
import { computeReportingNonFinalFlags } from "@/lib/reportingNonFinalIndicators";
|
|
73
|
-
import {
|
|
74
|
-
buildTagWeekDisplayBlocks,
|
|
75
|
-
groupTagDayRowsForDisplay,
|
|
76
|
-
} from "@/lib/reportingTagWeekBreakdown";
|
|
83
|
+
import { buildTagWeekDisplayBlocks } from "@/lib/reportingTagWeekBreakdown";
|
|
77
84
|
import { ReportingTour } from "@/components/dashboard/ReportingTour";
|
|
78
85
|
import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
|
|
79
86
|
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
80
87
|
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
81
88
|
import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
|
|
82
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
calendarDateKeyInTimeZone,
|
|
91
|
+
readDashboardTimeZoneFromCfg,
|
|
92
|
+
} from "@/lib/dashboardTimeZone";
|
|
83
93
|
import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
|
|
84
94
|
import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
|
|
85
95
|
import {
|
|
86
96
|
isReportingTourCompleted,
|
|
87
97
|
resetReportingTour,
|
|
88
98
|
} from "@/lib/dashboardTourStorage";
|
|
99
|
+
import {
|
|
100
|
+
TaskSessionLiveCard,
|
|
101
|
+
type TaskSessionLiveCardTask,
|
|
102
|
+
} from "@/components/dashboard/TaskSessionLiveCard";
|
|
103
|
+
import { useReportingInteractionState } from "@/components/dashboard/useReportingInteractionState";
|
|
89
104
|
|
|
90
105
|
type LiveShape = { language?: string };
|
|
106
|
+
type ReportingTaskInspectScope = {
|
|
107
|
+
kind: "tag" | "project";
|
|
108
|
+
title: string;
|
|
109
|
+
dayKey?: string;
|
|
110
|
+
weekStart?: string;
|
|
111
|
+
tagKey?: string;
|
|
112
|
+
projectKey?: string;
|
|
113
|
+
sourceLabel?: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type ReportingTaskDetail = {
|
|
117
|
+
sessionId: string;
|
|
118
|
+
task: TaskSessionLiveCardTask;
|
|
119
|
+
pausePlayMode: "pause" | "resume" | null;
|
|
120
|
+
dayKey: string;
|
|
121
|
+
weekStart: string | null;
|
|
122
|
+
tagKeys: string[];
|
|
123
|
+
projectKey: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function taskTagKeysForReporting(
|
|
127
|
+
tags: string[] | undefined,
|
|
128
|
+
defaultTagBucketEnabled: boolean,
|
|
129
|
+
): string[] {
|
|
130
|
+
const keys = (tags ?? [])
|
|
131
|
+
.map((raw) => normalizeTagKey(String(raw)).toLowerCase())
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
if (keys.length > 0) {
|
|
134
|
+
return [...new Set(keys)];
|
|
135
|
+
}
|
|
136
|
+
return defaultTagBucketEnabled ? [DEFAULT_FALLBACK_TASK_TAG] : [""];
|
|
137
|
+
}
|
|
91
138
|
|
|
92
139
|
function dayLabel(key: string, undated: string): string {
|
|
93
140
|
return key === UNDATED_KEY ? undated : key;
|
|
94
141
|
}
|
|
95
142
|
|
|
143
|
+
function weekRangeLabelForSource(
|
|
144
|
+
weekStart: string,
|
|
145
|
+
lang: Lang,
|
|
146
|
+
locale: string,
|
|
147
|
+
): string {
|
|
148
|
+
const start = new Date(`${weekStart}T00:00:00`);
|
|
149
|
+
if (Number.isNaN(start.getTime())) {
|
|
150
|
+
return weekStart;
|
|
151
|
+
}
|
|
152
|
+
const end = new Date(start);
|
|
153
|
+
end.setDate(end.getDate() + 6);
|
|
154
|
+
const fmt = new Intl.DateTimeFormat(locale, {
|
|
155
|
+
month: "short",
|
|
156
|
+
day: "numeric",
|
|
157
|
+
});
|
|
158
|
+
const prefix = lang === "fr" ? "Semaine" : "Week";
|
|
159
|
+
return `${prefix} ${fmt.format(start)} - ${fmt.format(end)}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
96
162
|
function formatMinutesCell(minutes: number | undefined): string {
|
|
97
163
|
const m = minutes ?? 0;
|
|
98
164
|
return m > 0 ? formatDuration(m) : "—";
|
|
@@ -367,46 +433,64 @@ function ReportingContent() {
|
|
|
367
433
|
const [dateTo, setDateTo] = useState("");
|
|
368
434
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
|
369
435
|
const [workspaceSnapBusy, setWorkspaceSnapBusy] = useState(false);
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
return next;
|
|
392
|
-
});
|
|
393
|
-
}, []);
|
|
436
|
+
const {
|
|
437
|
+
weekStartsOn,
|
|
438
|
+
setWeekStartsOn,
|
|
439
|
+
chartWeekNavIndex,
|
|
440
|
+
setChartWeekNavIndex,
|
|
441
|
+
tagWeekRollupOpenKeys,
|
|
442
|
+
toggleTagWeekRollup,
|
|
443
|
+
projectWeekTagBreakdownOpenKeys,
|
|
444
|
+
toggleProjectWeekTagBreakdown,
|
|
445
|
+
reportingTourOpen,
|
|
446
|
+
setReportingTourOpen,
|
|
447
|
+
taskInspectScope,
|
|
448
|
+
setTaskInspectScope,
|
|
449
|
+
taskInspectAnchor,
|
|
450
|
+
setTaskInspectAnchor,
|
|
451
|
+
taskInspectModalRect,
|
|
452
|
+
setTaskInspectModalRect,
|
|
453
|
+
portalReady,
|
|
454
|
+
taskInspectModalRef,
|
|
455
|
+
} = useReportingInteractionState<ReportingTaskInspectScope>();
|
|
394
456
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
if (!taskInspectScope || !portalReady) {
|
|
459
|
+
setTaskInspectModalRect(null);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const updateRect = () => {
|
|
463
|
+
const node = taskInspectModalRef.current;
|
|
464
|
+
if (!node) {
|
|
465
|
+
return;
|
|
402
466
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
467
|
+
const rect = node.getBoundingClientRect();
|
|
468
|
+
setTaskInspectModalRect({
|
|
469
|
+
left: rect.left,
|
|
470
|
+
top: rect.top,
|
|
471
|
+
width: rect.width,
|
|
472
|
+
height: rect.height,
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
const raf = globalThis.requestAnimationFrame(updateRect);
|
|
476
|
+
const onResize = () => updateRect();
|
|
477
|
+
globalThis.addEventListener("resize", onResize);
|
|
478
|
+
return () => {
|
|
479
|
+
globalThis.cancelAnimationFrame(raf);
|
|
480
|
+
globalThis.removeEventListener("resize", onResize);
|
|
481
|
+
};
|
|
482
|
+
}, [portalReady, taskInspectScope]);
|
|
406
483
|
|
|
407
484
|
useEffect(() => {
|
|
408
|
-
|
|
409
|
-
|
|
485
|
+
if (!taskInspectScope) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const prevOverflow = document.body.style.overflow;
|
|
489
|
+
document.body.style.overflow = "hidden";
|
|
490
|
+
return () => {
|
|
491
|
+
document.body.style.overflow = prevOverflow;
|
|
492
|
+
};
|
|
493
|
+
}, [taskInspectScope]);
|
|
410
494
|
|
|
411
495
|
const persistWeekStartsOn = useCallback((v: ReportingWeekStartsOn) => {
|
|
412
496
|
setWeekStartsOn(v);
|
|
@@ -433,6 +517,14 @@ function ReportingContent() {
|
|
|
433
517
|
[refresh],
|
|
434
518
|
);
|
|
435
519
|
|
|
520
|
+
const postReportingTaskAction = useCallback(
|
|
521
|
+
async (body: Record<string, unknown>) => {
|
|
522
|
+
await postKronosysAction(body);
|
|
523
|
+
await refresh();
|
|
524
|
+
},
|
|
525
|
+
[refresh],
|
|
526
|
+
);
|
|
527
|
+
|
|
436
528
|
const stripReportingTourReplayParam = useCallback(() => {
|
|
437
529
|
if (searchParams.get("tour") !== "replay") {
|
|
438
530
|
return;
|
|
@@ -447,6 +539,22 @@ function ReportingContent() {
|
|
|
447
539
|
const lang: Lang = live?.language === "fr" ? "fr" : "en";
|
|
448
540
|
const t = reportingStrings(lang);
|
|
449
541
|
const dt = dashboardStrings(lang);
|
|
542
|
+
const reportingClosureKindLabels = useMemo(
|
|
543
|
+
() => ({
|
|
544
|
+
planned: dt.sessionEndReasonPlanned,
|
|
545
|
+
early: dt.sessionEndReasonEarly,
|
|
546
|
+
overrun: dt.sessionEndReasonOverrun,
|
|
547
|
+
other: dt.sessionEndReasonOther,
|
|
548
|
+
unspecified: t.closureBreakdownUnspecified,
|
|
549
|
+
}),
|
|
550
|
+
[
|
|
551
|
+
dt.sessionEndReasonPlanned,
|
|
552
|
+
dt.sessionEndReasonEarly,
|
|
553
|
+
dt.sessionEndReasonOverrun,
|
|
554
|
+
dt.sessionEndReasonOther,
|
|
555
|
+
t.closureBreakdownUnspecified,
|
|
556
|
+
],
|
|
557
|
+
);
|
|
450
558
|
const handleManualRefresh = useCallback(async () => {
|
|
451
559
|
return await refresh({ routerInvalidate: true });
|
|
452
560
|
}, [refresh]);
|
|
@@ -469,6 +577,15 @@ function ReportingContent() {
|
|
|
469
577
|
(a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }),
|
|
470
578
|
);
|
|
471
579
|
}, [payload?.knownTags]);
|
|
580
|
+
const knownProjects = useMemo(() => {
|
|
581
|
+
const raw = payload?.knownProjects;
|
|
582
|
+
if (!Array.isArray(raw)) {
|
|
583
|
+
return [] as string[];
|
|
584
|
+
}
|
|
585
|
+
return [...new Set(raw.map((x) => String(x)).filter(Boolean))].sort(
|
|
586
|
+
(a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }),
|
|
587
|
+
);
|
|
588
|
+
}, [payload?.knownProjects]);
|
|
472
589
|
|
|
473
590
|
const taskDefaultTagBucketEnabled = useMemo(
|
|
474
591
|
() => readTaskDefaultTagBucketEnabled(payload?.cfg),
|
|
@@ -476,46 +593,67 @@ function ReportingContent() {
|
|
|
476
593
|
);
|
|
477
594
|
|
|
478
595
|
const reportTimeZone = useMemo(
|
|
479
|
-
() =>
|
|
480
|
-
|
|
596
|
+
() =>
|
|
597
|
+
readDashboardTimeZoneFromCfg(
|
|
598
|
+
payload?.cfg as Record<string, unknown> | undefined,
|
|
599
|
+
),
|
|
600
|
+
[payload?.cfg],
|
|
481
601
|
);
|
|
482
602
|
|
|
483
603
|
const tagSet = useMemo(() => buildTagFilterSet(selectedTags), [selectedTags]);
|
|
604
|
+
const mergedSessions = useMemo(
|
|
605
|
+
() => (payload ? mergeSessionsFromPayload(payload) : []),
|
|
606
|
+
[payload],
|
|
607
|
+
);
|
|
484
608
|
|
|
485
609
|
const reportingFiltersActive = useMemo(
|
|
486
610
|
() => Boolean(dateFrom.trim() || dateTo.trim() || selectedTags.length > 0),
|
|
487
611
|
[dateFrom, dateTo, selectedTags],
|
|
488
612
|
);
|
|
613
|
+
const dateFromFilter = dateFrom.trim() || null;
|
|
614
|
+
const dateToFilter = dateTo.trim() || null;
|
|
489
615
|
|
|
490
616
|
const agg = useMemo(() => {
|
|
491
|
-
if (
|
|
617
|
+
if (mergedSessions.length === 0) {
|
|
492
618
|
return null;
|
|
493
619
|
}
|
|
494
|
-
const sessions = mergeSessionsFromPayload(payload);
|
|
495
620
|
return aggregateReporting(
|
|
496
|
-
|
|
621
|
+
mergedSessions,
|
|
497
622
|
tagSet,
|
|
498
|
-
|
|
499
|
-
|
|
623
|
+
dateFromFilter,
|
|
624
|
+
dateToFilter,
|
|
500
625
|
reportTimeZone,
|
|
501
626
|
taskDefaultTagBucketEnabled,
|
|
502
627
|
);
|
|
503
|
-
}, [
|
|
628
|
+
}, [
|
|
629
|
+
mergedSessions,
|
|
630
|
+
tagSet,
|
|
631
|
+
dateFromFilter,
|
|
632
|
+
dateToFilter,
|
|
633
|
+
reportTimeZone,
|
|
634
|
+
taskDefaultTagBucketEnabled,
|
|
635
|
+
]);
|
|
504
636
|
|
|
505
637
|
const archivedExcludedTaskMinutes = useMemo(() => {
|
|
506
|
-
if (
|
|
638
|
+
if (mergedSessions.length === 0) {
|
|
507
639
|
return 0;
|
|
508
640
|
}
|
|
509
|
-
const sessions = mergeSessionsFromPayload(payload);
|
|
510
641
|
return aggregateArchivedExcludedTaskMinutes(
|
|
511
|
-
|
|
642
|
+
mergedSessions,
|
|
512
643
|
tagSet,
|
|
513
|
-
|
|
514
|
-
|
|
644
|
+
dateFromFilter,
|
|
645
|
+
dateToFilter,
|
|
515
646
|
reportTimeZone,
|
|
516
647
|
taskDefaultTagBucketEnabled,
|
|
517
648
|
);
|
|
518
|
-
}, [
|
|
649
|
+
}, [
|
|
650
|
+
mergedSessions,
|
|
651
|
+
tagSet,
|
|
652
|
+
dateFromFilter,
|
|
653
|
+
dateToFilter,
|
|
654
|
+
reportTimeZone,
|
|
655
|
+
taskDefaultTagBucketEnabled,
|
|
656
|
+
]);
|
|
519
657
|
|
|
520
658
|
const reportingNonFinal = useMemo(
|
|
521
659
|
() =>
|
|
@@ -558,29 +696,26 @@ function ReportingContent() {
|
|
|
558
696
|
[dayKeys],
|
|
559
697
|
);
|
|
560
698
|
|
|
561
|
-
const tagTimeSplit = useMemo((): {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
} => {
|
|
565
|
-
if (!payload) {
|
|
566
|
-
return { byDay: [], byWeek: [] };
|
|
699
|
+
const tagTimeSplit = useMemo((): { byDay: ReportingTagTimeDayRow[] } => {
|
|
700
|
+
if (mergedSessions.length === 0) {
|
|
701
|
+
return { byDay: [] };
|
|
567
702
|
}
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
sessions,
|
|
703
|
+
const { byDay } = aggregateTagTaskMinutesByDayAndWeek(
|
|
704
|
+
mergedSessions,
|
|
571
705
|
tagSet,
|
|
572
|
-
|
|
573
|
-
|
|
706
|
+
dateFromFilter,
|
|
707
|
+
dateToFilter,
|
|
574
708
|
reportTimeZone,
|
|
575
709
|
weekStartsOn,
|
|
576
710
|
t.tagTimeUntagged,
|
|
577
711
|
taskDefaultTagBucketEnabled,
|
|
578
712
|
);
|
|
713
|
+
return { byDay };
|
|
579
714
|
}, [
|
|
580
|
-
|
|
715
|
+
mergedSessions,
|
|
581
716
|
tagSet,
|
|
582
|
-
|
|
583
|
-
|
|
717
|
+
dateFromFilter,
|
|
718
|
+
dateToFilter,
|
|
584
719
|
reportTimeZone,
|
|
585
720
|
weekStartsOn,
|
|
586
721
|
t.tagTimeUntagged,
|
|
@@ -593,19 +728,25 @@ function ReportingContent() {
|
|
|
593
728
|
);
|
|
594
729
|
|
|
595
730
|
const projectTaskMinutesByDay = useMemo(() => {
|
|
596
|
-
if (
|
|
731
|
+
if (mergedSessions.length === 0) {
|
|
597
732
|
return [];
|
|
598
733
|
}
|
|
599
|
-
const sessions = mergeSessionsFromPayload(payload);
|
|
600
734
|
return aggregateProjectTaskMinutesByDay(
|
|
601
|
-
|
|
735
|
+
mergedSessions,
|
|
602
736
|
tagSet,
|
|
603
|
-
|
|
604
|
-
|
|
737
|
+
dateFromFilter,
|
|
738
|
+
dateToFilter,
|
|
605
739
|
reportTimeZone,
|
|
606
740
|
taskDefaultTagBucketEnabled,
|
|
607
741
|
);
|
|
608
|
-
}, [
|
|
742
|
+
}, [
|
|
743
|
+
mergedSessions,
|
|
744
|
+
tagSet,
|
|
745
|
+
dateFromFilter,
|
|
746
|
+
dateToFilter,
|
|
747
|
+
reportTimeZone,
|
|
748
|
+
taskDefaultTagBucketEnabled,
|
|
749
|
+
]);
|
|
609
750
|
|
|
610
751
|
const projectWeekCalendarRows = useMemo(
|
|
611
752
|
() => buildProjectWeekCalendarRows(projectTaskMinutesByDay, weekStartsOn),
|
|
@@ -790,6 +931,38 @@ function ReportingContent() {
|
|
|
790
931
|
return projectWeekCalendarRows.filter((r) => r.weekStart === ws);
|
|
791
932
|
}, [projectWeekCalendarRows, navigableWeekStarts, chartWeekNavIndexSafe]);
|
|
792
933
|
|
|
934
|
+
const projectScopedTagRowsByWeekProject = useMemo(() => {
|
|
935
|
+
const byWeekProject = new Map<string, TagWeekCalendarRow[]>();
|
|
936
|
+
for (const row of tagWeekCalendarRowsVisible) {
|
|
937
|
+
if (isFallbackTaskTagKey(row.tagKey)) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const scoped = parseProjectScopedTag(row.tagKey);
|
|
941
|
+
if (!scoped) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const projectKeyLower = normalizeProjectKey(
|
|
945
|
+
scoped.projectKey,
|
|
946
|
+
).toLowerCase();
|
|
947
|
+
if (!projectKeyLower) {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const bucketKey = `${row.weekStart}:::${projectKeyLower}`;
|
|
951
|
+
const list = byWeekProject.get(bucketKey) ?? [];
|
|
952
|
+
list.push(row);
|
|
953
|
+
byWeekProject.set(bucketKey, list);
|
|
954
|
+
}
|
|
955
|
+
for (const [bucketKey, rows] of byWeekProject) {
|
|
956
|
+
byWeekProject.set(
|
|
957
|
+
bucketKey,
|
|
958
|
+
[...rows].sort((a, b) =>
|
|
959
|
+
normalizeTagKey(a.tagKey).localeCompare(normalizeTagKey(b.tagKey)),
|
|
960
|
+
),
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
return byWeekProject;
|
|
964
|
+
}, [tagWeekCalendarRowsVisible]);
|
|
965
|
+
|
|
793
966
|
const projectCalendarWeekGroups = useMemo(() => {
|
|
794
967
|
const byWeek = new Map<string, ProjectWeekCalendarRow[]>();
|
|
795
968
|
for (const r of projectWeekCalendarRowsVisible) {
|
|
@@ -812,6 +985,133 @@ function ReportingContent() {
|
|
|
812
985
|
});
|
|
813
986
|
}, [projectWeekCalendarRowsVisible]);
|
|
814
987
|
|
|
988
|
+
const reportingTaskDetails = useMemo(() => {
|
|
989
|
+
const list: ReportingTaskDetail[] = [];
|
|
990
|
+
const unbounded = !dateFromFilter && !dateToFilter;
|
|
991
|
+
for (const s of mergedSessions) {
|
|
992
|
+
const sessionId =
|
|
993
|
+
typeof s.sessionId === "string" ? s.sessionId.trim() : "";
|
|
994
|
+
if (!sessionId) {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
const allTasks = collectTasksDeduped(s);
|
|
998
|
+
const matching = allTasks.filter((task) =>
|
|
999
|
+
taskMatchesTags(task, tagSet, taskDefaultTagBucketEnabled),
|
|
1000
|
+
);
|
|
1001
|
+
if (tagSet.size > 0 && matching.length === 0) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
for (const task of matching) {
|
|
1005
|
+
if (
|
|
1006
|
+
!taskCountsTowardArchivedSessionReporting(task) &&
|
|
1007
|
+
s.archived === true
|
|
1008
|
+
) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const taskDayRaw = calendarDateKeyInTimeZone(
|
|
1012
|
+
task.endTime ?? task.startTime,
|
|
1013
|
+
reportTimeZone,
|
|
1014
|
+
);
|
|
1015
|
+
if (
|
|
1016
|
+
!unbounded &&
|
|
1017
|
+
!dayInRange(taskDayRaw, dateFromFilter, dateToFilter)
|
|
1018
|
+
) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const dayKey = taskDayRaw ?? (unbounded ? UNDATED_KEY : null);
|
|
1022
|
+
if (!dayKey) {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
const minutes = (task.durationMs ?? 0) / 60000;
|
|
1026
|
+
if (minutes <= 0) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const projectRaw =
|
|
1030
|
+
typeof task.project === "string" ? task.project.trim() : "";
|
|
1031
|
+
const projectKey = projectRaw
|
|
1032
|
+
? normalizeProjectKey(projectRaw).toLowerCase()
|
|
1033
|
+
: "";
|
|
1034
|
+
const weekStart =
|
|
1035
|
+
dayKey !== UNDATED_KEY
|
|
1036
|
+
? localWeekStartKeyFromDayKey(dayKey, weekStartsOn)
|
|
1037
|
+
: null;
|
|
1038
|
+
const taskRecord = task as Record<string, unknown>;
|
|
1039
|
+
const rawTaskName =
|
|
1040
|
+
typeof taskRecord.name === "string" ? taskRecord.name.trim() : "";
|
|
1041
|
+
const taskName =
|
|
1042
|
+
rawTaskName || (lang === "fr" ? "Tâche sans titre" : "Untitled task");
|
|
1043
|
+
list.push({
|
|
1044
|
+
sessionId,
|
|
1045
|
+
pausePlayMode:
|
|
1046
|
+
task.isDone === true
|
|
1047
|
+
? null
|
|
1048
|
+
: task.manualTaskTimerPaused === true
|
|
1049
|
+
? "resume"
|
|
1050
|
+
: "pause",
|
|
1051
|
+
dayKey,
|
|
1052
|
+
weekStart,
|
|
1053
|
+
projectKey,
|
|
1054
|
+
tagKeys: taskTagKeysForReporting(
|
|
1055
|
+
task.tags,
|
|
1056
|
+
taskDefaultTagBucketEnabled,
|
|
1057
|
+
),
|
|
1058
|
+
task: {
|
|
1059
|
+
id: String(task.id ?? `${sessionId}-${dayKey}-${list.length}`),
|
|
1060
|
+
name: taskName,
|
|
1061
|
+
durationMs: Math.max(0, Number(task.durationMs ?? 0)),
|
|
1062
|
+
isDone: task.isDone === true,
|
|
1063
|
+
startTime: task.startTime,
|
|
1064
|
+
endTime: task.endTime,
|
|
1065
|
+
tags: task.tags ?? [],
|
|
1066
|
+
project: task.project ?? null,
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return list;
|
|
1072
|
+
}, [
|
|
1073
|
+
dateFromFilter,
|
|
1074
|
+
dateToFilter,
|
|
1075
|
+
lang,
|
|
1076
|
+
mergedSessions,
|
|
1077
|
+
reportTimeZone,
|
|
1078
|
+
tagSet,
|
|
1079
|
+
taskDefaultTagBucketEnabled,
|
|
1080
|
+
weekStartsOn,
|
|
1081
|
+
]);
|
|
1082
|
+
|
|
1083
|
+
const taskInspectList = useMemo(() => {
|
|
1084
|
+
if (!taskInspectScope) {
|
|
1085
|
+
return [];
|
|
1086
|
+
}
|
|
1087
|
+
return reportingTaskDetails.filter((entry) => {
|
|
1088
|
+
if (taskInspectScope.dayKey && entry.dayKey !== taskInspectScope.dayKey) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
if (
|
|
1092
|
+
taskInspectScope.weekStart &&
|
|
1093
|
+
entry.weekStart !== taskInspectScope.weekStart
|
|
1094
|
+
) {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
if (taskInspectScope.kind === "project") {
|
|
1098
|
+
return entry.projectKey === (taskInspectScope.projectKey ?? "");
|
|
1099
|
+
}
|
|
1100
|
+
return entry.tagKeys.includes(taskInspectScope.tagKey ?? "");
|
|
1101
|
+
});
|
|
1102
|
+
}, [reportingTaskDetails, taskInspectScope]);
|
|
1103
|
+
|
|
1104
|
+
const openTaskInspectScope = useCallback(
|
|
1105
|
+
(
|
|
1106
|
+
scope: ReportingTaskInspectScope,
|
|
1107
|
+
anchor: { x: number; y: number } | null,
|
|
1108
|
+
) => {
|
|
1109
|
+
setTaskInspectScope(scope);
|
|
1110
|
+
setTaskInspectAnchor(anchor);
|
|
1111
|
+
},
|
|
1112
|
+
[],
|
|
1113
|
+
);
|
|
1114
|
+
|
|
815
1115
|
const toggleTag = (tag: string) => {
|
|
816
1116
|
const key = normalizeTagKey(tag).toLowerCase();
|
|
817
1117
|
setSelectedTags((prev) => {
|
|
@@ -823,6 +1123,201 @@ function ReportingContent() {
|
|
|
823
1123
|
});
|
|
824
1124
|
};
|
|
825
1125
|
|
|
1126
|
+
const renderReportingDurationButton = (
|
|
1127
|
+
minutes: number,
|
|
1128
|
+
scope: ReportingTaskInspectScope,
|
|
1129
|
+
className: string,
|
|
1130
|
+
) => {
|
|
1131
|
+
const label = formatDuration(minutes);
|
|
1132
|
+
return (
|
|
1133
|
+
<button
|
|
1134
|
+
type="button"
|
|
1135
|
+
className={`${className} underline decoration-dotted underline-offset-2 hover:text-violet-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-400/60`}
|
|
1136
|
+
onClick={(event) => {
|
|
1137
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1138
|
+
openTaskInspectScope(scope, {
|
|
1139
|
+
x: rect.left + rect.width / 2,
|
|
1140
|
+
y: rect.bottom,
|
|
1141
|
+
});
|
|
1142
|
+
}}
|
|
1143
|
+
title={
|
|
1144
|
+
lang === "fr" ? `Voir les tâches (${label})` : `View tasks (${label})`
|
|
1145
|
+
}
|
|
1146
|
+
>
|
|
1147
|
+
{label}
|
|
1148
|
+
</button>
|
|
1149
|
+
);
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
const taskInspectPlacement = useMemo(() => {
|
|
1153
|
+
if (!taskInspectAnchor) {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
const viewportWidth = globalThis.innerWidth;
|
|
1157
|
+
const viewportHeight = globalThis.innerHeight;
|
|
1158
|
+
const preferredPanelWidth = Math.min(
|
|
1159
|
+
760,
|
|
1160
|
+
Math.max(320, viewportWidth - 32),
|
|
1161
|
+
);
|
|
1162
|
+
const estimatedPanelHeight = Math.min(
|
|
1163
|
+
Math.max(300, viewportHeight * 0.72),
|
|
1164
|
+
viewportHeight - 32,
|
|
1165
|
+
);
|
|
1166
|
+
const margin = 16;
|
|
1167
|
+
const offset = 72;
|
|
1168
|
+
const sourceSafeZoneX = 72;
|
|
1169
|
+
const sourceSafeZoneY = 36;
|
|
1170
|
+
const minPanelWidth = 220;
|
|
1171
|
+
const spaceRight = viewportWidth - margin - (taskInspectAnchor.x + offset);
|
|
1172
|
+
const spaceLeft = taskInspectAnchor.x - margin - offset;
|
|
1173
|
+
const placeRight = spaceRight >= spaceLeft;
|
|
1174
|
+
const availableSideSpace = placeRight
|
|
1175
|
+
? Math.max(0, spaceRight)
|
|
1176
|
+
: Math.max(0, spaceLeft);
|
|
1177
|
+
let panelWidth = Math.min(preferredPanelWidth, availableSideSpace);
|
|
1178
|
+
if (panelWidth < minPanelWidth) {
|
|
1179
|
+
const otherSideSpace = placeRight
|
|
1180
|
+
? Math.max(0, spaceLeft)
|
|
1181
|
+
: Math.max(0, spaceRight);
|
|
1182
|
+
panelWidth = Math.min(
|
|
1183
|
+
preferredPanelWidth,
|
|
1184
|
+
Math.max(minPanelWidth, otherSideSpace),
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const clampLeft = (value: number) =>
|
|
1188
|
+
Math.max(margin, Math.min(value, viewportWidth - panelWidth - margin));
|
|
1189
|
+
const clampTop = (value: number) =>
|
|
1190
|
+
Math.max(
|
|
1191
|
+
margin,
|
|
1192
|
+
Math.min(value, viewportHeight - estimatedPanelHeight - margin),
|
|
1193
|
+
);
|
|
1194
|
+
const rawLeft = placeRight
|
|
1195
|
+
? taskInspectAnchor.x + offset
|
|
1196
|
+
: taskInspectAnchor.x - panelWidth - offset;
|
|
1197
|
+
let adjustedLeft = clampLeft(rawLeft);
|
|
1198
|
+
const overlapsSourceX =
|
|
1199
|
+
adjustedLeft <= taskInspectAnchor.x + sourceSafeZoneX &&
|
|
1200
|
+
taskInspectAnchor.x - sourceSafeZoneX <= adjustedLeft + panelWidth;
|
|
1201
|
+
if (overlapsSourceX) {
|
|
1202
|
+
const fallbackLeft = placeRight
|
|
1203
|
+
? taskInspectAnchor.x - panelWidth - offset
|
|
1204
|
+
: taskInspectAnchor.x + offset;
|
|
1205
|
+
adjustedLeft = clampLeft(fallbackLeft);
|
|
1206
|
+
}
|
|
1207
|
+
const centeredTop = clampTop(
|
|
1208
|
+
taskInspectAnchor.y - estimatedPanelHeight / 2,
|
|
1209
|
+
);
|
|
1210
|
+
const sourceBandTop = taskInspectAnchor.y - sourceSafeZoneY;
|
|
1211
|
+
const sourceBandBottom = taskInspectAnchor.y + sourceSafeZoneY;
|
|
1212
|
+
const candidatesTop = [
|
|
1213
|
+
centeredTop,
|
|
1214
|
+
clampTop(sourceBandBottom + offset),
|
|
1215
|
+
clampTop(sourceBandTop - estimatedPanelHeight - offset),
|
|
1216
|
+
];
|
|
1217
|
+
const nonOverlapTop = candidatesTop.filter((top) => {
|
|
1218
|
+
const modalTop = top;
|
|
1219
|
+
const modalBottom = top + estimatedPanelHeight;
|
|
1220
|
+
return !(modalTop <= sourceBandBottom && sourceBandTop <= modalBottom);
|
|
1221
|
+
});
|
|
1222
|
+
const pool = nonOverlapTop.length > 0 ? nonOverlapTop : candidatesTop;
|
|
1223
|
+
let adjustedTop = pool[0];
|
|
1224
|
+
let bestTopScore = Number.POSITIVE_INFINITY;
|
|
1225
|
+
for (const top of pool) {
|
|
1226
|
+
const score = Math.abs(top - centeredTop);
|
|
1227
|
+
if (score < bestTopScore) {
|
|
1228
|
+
bestTopScore = score;
|
|
1229
|
+
adjustedTop = top;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
left: adjustedLeft,
|
|
1234
|
+
top: adjustedTop,
|
|
1235
|
+
width: panelWidth,
|
|
1236
|
+
height: estimatedPanelHeight,
|
|
1237
|
+
};
|
|
1238
|
+
}, [taskInspectAnchor]);
|
|
1239
|
+
const taskInspectPlacementTop = useMemo(() => {
|
|
1240
|
+
if (!taskInspectPlacement) {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
if (!taskInspectModalRect) {
|
|
1244
|
+
return taskInspectPlacement.top;
|
|
1245
|
+
}
|
|
1246
|
+
const viewportHeight = globalThis.innerHeight;
|
|
1247
|
+
const margin = 16;
|
|
1248
|
+
const overflowBottom =
|
|
1249
|
+
taskInspectModalRect.top +
|
|
1250
|
+
taskInspectModalRect.height -
|
|
1251
|
+
(viewportHeight - margin);
|
|
1252
|
+
const overflowTop = margin - taskInspectModalRect.top;
|
|
1253
|
+
let nextTop =
|
|
1254
|
+
taskInspectPlacement.top -
|
|
1255
|
+
Math.max(0, overflowBottom) +
|
|
1256
|
+
Math.max(0, overflowTop);
|
|
1257
|
+
const maxTop = Math.max(
|
|
1258
|
+
margin,
|
|
1259
|
+
viewportHeight - taskInspectModalRect.height - margin,
|
|
1260
|
+
);
|
|
1261
|
+
nextTop = Math.max(margin, Math.min(nextTop, maxTop));
|
|
1262
|
+
return nextTop;
|
|
1263
|
+
}, [taskInspectModalRect, taskInspectPlacement]);
|
|
1264
|
+
const taskInspectConnector = useMemo(() => {
|
|
1265
|
+
if (!taskInspectAnchor || !taskInspectPlacement) {
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
const modalLeft = taskInspectModalRect?.left ?? taskInspectPlacement.left;
|
|
1269
|
+
const modalTop = taskInspectModalRect?.top ?? taskInspectPlacement.top;
|
|
1270
|
+
const modalWidth =
|
|
1271
|
+
taskInspectModalRect?.width ?? taskInspectPlacement.width;
|
|
1272
|
+
const modalHeight =
|
|
1273
|
+
taskInspectModalRect?.height ?? taskInspectPlacement.height;
|
|
1274
|
+
const modalRight = modalLeft + modalWidth;
|
|
1275
|
+
const modalBottom = modalTop + modalHeight;
|
|
1276
|
+
const inset = 14;
|
|
1277
|
+
const anchors = [
|
|
1278
|
+
// Coins
|
|
1279
|
+
{ x: modalLeft + inset, y: modalTop + inset },
|
|
1280
|
+
{ x: modalRight - inset, y: modalTop + inset },
|
|
1281
|
+
{ x: modalRight - inset, y: modalBottom - inset },
|
|
1282
|
+
{ x: modalLeft + inset, y: modalBottom - inset },
|
|
1283
|
+
// Milieux des côtés
|
|
1284
|
+
{ x: modalLeft + modalWidth / 2, y: modalTop + inset },
|
|
1285
|
+
{ x: modalRight - inset, y: modalTop + modalHeight / 2 },
|
|
1286
|
+
{ x: modalLeft + modalWidth / 2, y: modalBottom - inset },
|
|
1287
|
+
{ x: modalLeft + inset, y: modalTop + modalHeight / 2 },
|
|
1288
|
+
];
|
|
1289
|
+
let startX = anchors[0].x;
|
|
1290
|
+
let startY = anchors[0].y;
|
|
1291
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1292
|
+
for (const anchor of anchors) {
|
|
1293
|
+
const d = Math.hypot(
|
|
1294
|
+
anchor.x - taskInspectAnchor.x,
|
|
1295
|
+
anchor.y - taskInspectAnchor.y,
|
|
1296
|
+
);
|
|
1297
|
+
if (d < bestDistance) {
|
|
1298
|
+
bestDistance = d;
|
|
1299
|
+
startX = anchor.x;
|
|
1300
|
+
startY = anchor.y;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
const dx = startX - taskInspectAnchor.x;
|
|
1304
|
+
const dy = startY - taskInspectAnchor.y;
|
|
1305
|
+
const length = Math.hypot(dx, dy);
|
|
1306
|
+
const sourceGap = 14;
|
|
1307
|
+
const ux = length > 0 ? dx / length : 0;
|
|
1308
|
+
const uy = length > 0 ? dy / length : 0;
|
|
1309
|
+
const lineStartX = taskInspectAnchor.x + ux * sourceGap;
|
|
1310
|
+
const lineStartY = taskInspectAnchor.y + uy * sourceGap;
|
|
1311
|
+
return {
|
|
1312
|
+
startX,
|
|
1313
|
+
startY,
|
|
1314
|
+
lineStartX,
|
|
1315
|
+
lineStartY,
|
|
1316
|
+
length: Math.max(0, length - sourceGap),
|
|
1317
|
+
angleRad: Math.atan2(dy, dx),
|
|
1318
|
+
};
|
|
1319
|
+
}, [taskInspectAnchor, taskInspectModalRect, taskInspectPlacement]);
|
|
1320
|
+
|
|
826
1321
|
const headerApiError =
|
|
827
1322
|
lang === "fr"
|
|
828
1323
|
? "Impossible de joindre l’API locale Kronosys (127.0.0.1:5566 par défaut). Vérifiez que le serveur tourne."
|
|
@@ -870,6 +1365,7 @@ function ReportingContent() {
|
|
|
870
1365
|
return rows;
|
|
871
1366
|
}
|
|
872
1367
|
rows.push({ id: "report-summary-kpis", label: t.tocSummaryKpis });
|
|
1368
|
+
rows.push({ id: "report-closure-by-kind", label: t.tocClosureBreakdown });
|
|
873
1369
|
if (trackCodeMetrics) {
|
|
874
1370
|
rows.push({ id: "report-loc-metrics", label: t.tocLocSection });
|
|
875
1371
|
}
|
|
@@ -894,7 +1390,7 @@ function ReportingContent() {
|
|
|
894
1390
|
return (
|
|
895
1391
|
<div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
|
|
896
1392
|
<header className={appShellHeaderClassName}>
|
|
897
|
-
<div className={
|
|
1393
|
+
<div className={appShellHeaderTitleMetaRowClassName}>
|
|
898
1394
|
<div className="flex min-w-0 flex-col gap-1">
|
|
899
1395
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
900
1396
|
<Link
|
|
@@ -916,17 +1412,24 @@ function ReportingContent() {
|
|
|
916
1412
|
<AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
|
|
917
1413
|
</p>
|
|
918
1414
|
</div>
|
|
919
|
-
<
|
|
1415
|
+
<AppShellHeaderSessionMeta payload={payload} dt={dt} />
|
|
1416
|
+
</div>
|
|
1417
|
+
<div className="flex w-full justify-end">
|
|
1418
|
+
<div className={appShellHeaderToolbarClassName}>
|
|
1419
|
+
<AppShellHeaderWallClock lang={lang} dt={dt} />
|
|
1420
|
+
<AppShellCommandCenterPlaceholder />
|
|
920
1421
|
<AppShellRouteNav
|
|
921
1422
|
current="reporting"
|
|
922
1423
|
labels={{
|
|
923
1424
|
dashboard: t.dashboard,
|
|
924
1425
|
reporting: t.reporting,
|
|
925
1426
|
settings: t.settings,
|
|
1427
|
+
logs: t.logs,
|
|
926
1428
|
guide: t.guide,
|
|
927
1429
|
}}
|
|
928
1430
|
navAriaLabel={dt.appShellRouteNavAria}
|
|
929
1431
|
dashboardSessionId={dashboardSessionNavId}
|
|
1432
|
+
reserveGlobalPauseSlot
|
|
930
1433
|
/>
|
|
931
1434
|
<ThemeToggle lang={lang} />
|
|
932
1435
|
<PageRefreshButton
|
|
@@ -1365,17 +1868,17 @@ function ReportingContent() {
|
|
|
1365
1868
|
</p>
|
|
1366
1869
|
);
|
|
1367
1870
|
})()}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1871
|
+
{archivedExcludedTaskMinutes > 1e-9 ? (
|
|
1872
|
+
<div
|
|
1873
|
+
role="status"
|
|
1874
|
+
className="rounded-lg border border-amber-600/45 bg-amber-950/30 px-3 py-2.5 text-sm leading-relaxed text-amber-100/95 dark:border-amber-500/40"
|
|
1875
|
+
>
|
|
1876
|
+
{reportingArchivedExcludedRichText(
|
|
1877
|
+
t.reportingArchivedExcludedAside,
|
|
1878
|
+
archivedExcludedTaskMinutes,
|
|
1879
|
+
)}
|
|
1880
|
+
</div>
|
|
1881
|
+
) : null}
|
|
1379
1882
|
</section>
|
|
1380
1883
|
) : null}
|
|
1381
1884
|
|
|
@@ -1584,10 +2087,52 @@ function ReportingContent() {
|
|
|
1584
2087
|
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1585
2088
|
{agg.assiduityAverageLateMinutesWhenLate == null
|
|
1586
2089
|
? "—"
|
|
1587
|
-
: formatDuration(
|
|
2090
|
+
: formatDuration(
|
|
2091
|
+
agg.assiduityAverageLateMinutesWhenLate,
|
|
2092
|
+
)}
|
|
1588
2093
|
</div>
|
|
1589
2094
|
</div>
|
|
1590
2095
|
</div>
|
|
2096
|
+
<div
|
|
2097
|
+
id="report-closure-by-kind"
|
|
2098
|
+
className="mt-4 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4"
|
|
2099
|
+
>
|
|
2100
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
2101
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
|
2102
|
+
{t.closureBreakdownTitle}
|
|
2103
|
+
</h3>
|
|
2104
|
+
<ReportingFilteredBadge
|
|
2105
|
+
active={reportingFiltersActive}
|
|
2106
|
+
label={t.sectionFilteredBadge}
|
|
2107
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2108
|
+
/>
|
|
2109
|
+
<InlineMetricHelpTrigger
|
|
2110
|
+
ariaLabel={t.metricHelpClosureBreakdownAria}
|
|
2111
|
+
body={t.metricHelpClosureBreakdownBody}
|
|
2112
|
+
/>
|
|
2113
|
+
</div>
|
|
2114
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
|
2115
|
+
{REPORTING_SESSION_CLOSURE_DISPLAY_ORDER.map(
|
|
2116
|
+
(closureKey) => {
|
|
2117
|
+
const count =
|
|
2118
|
+
agg.sessionCountByClosureKind[closureKey] ?? 0;
|
|
2119
|
+
return (
|
|
2120
|
+
<div
|
|
2121
|
+
key={closureKey}
|
|
2122
|
+
className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3"
|
|
2123
|
+
>
|
|
2124
|
+
<div className="text-xs uppercase text-zinc-500">
|
|
2125
|
+
{reportingClosureKindLabels[closureKey]}
|
|
2126
|
+
</div>
|
|
2127
|
+
<div className="mt-1 text-xl font-semibold tabular-nums text-zinc-100">
|
|
2128
|
+
{count}
|
|
2129
|
+
</div>
|
|
2130
|
+
</div>
|
|
2131
|
+
);
|
|
2132
|
+
},
|
|
2133
|
+
)}
|
|
2134
|
+
</div>
|
|
2135
|
+
</div>
|
|
1591
2136
|
{archivedExcludedTaskMinutes > 1e-9 ? (
|
|
1592
2137
|
<div
|
|
1593
2138
|
role="status"
|
|
@@ -1886,162 +2431,164 @@ function ReportingContent() {
|
|
|
1886
2431
|
</div>
|
|
1887
2432
|
</section>
|
|
1888
2433
|
) : null}
|
|
1889
|
-
<
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
<
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
ariaLabel={t.metricHelpChartSessionsAria}
|
|
1904
|
-
body={t.metricHelpChartSessionsBody}
|
|
1905
|
-
/>
|
|
1906
|
-
</div>
|
|
1907
|
-
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
|
|
1908
|
-
<span>{t.legendSessions}</span>
|
|
1909
|
-
<InlineMetricHelpTrigger
|
|
1910
|
-
ariaLabel={t.metricHelpLegendSessionsAria}
|
|
1911
|
-
body={t.metricHelpLegendSessionsBody}
|
|
1912
|
-
/>
|
|
1913
|
-
</div>
|
|
1914
|
-
<div className="mt-4">
|
|
1915
|
-
<MiniBars
|
|
1916
|
-
days={reportingDayKeys}
|
|
1917
|
-
values={agg.sessionsByDay}
|
|
1918
|
-
max={peak}
|
|
1919
|
-
className="bg-violet-500/90"
|
|
1920
|
-
undatedLabel={t.undatedLabel}
|
|
1921
|
-
/>
|
|
1922
|
-
</div>
|
|
1923
|
-
</section>
|
|
1924
|
-
|
|
1925
|
-
<section
|
|
1926
|
-
id="report-chart-tasks"
|
|
1927
|
-
className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
1928
|
-
>
|
|
1929
|
-
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
1930
|
-
<h3 className="text-sm font-semibold text-zinc-200">
|
|
1931
|
-
{t.chartTasksByStatusPerDay}
|
|
1932
|
-
</h3>
|
|
1933
|
-
<ReportingFilteredBadge
|
|
1934
|
-
active={reportingFiltersActive}
|
|
1935
|
-
label={t.sectionFilteredBadge}
|
|
1936
|
-
titleText={t.sectionFilteredBadgeTitle}
|
|
1937
|
-
/>
|
|
1938
|
-
<InlineMetricHelpTrigger
|
|
1939
|
-
ariaLabel={t.metricHelpChartTasksAria}
|
|
1940
|
-
body={t.metricHelpChartTasksBody}
|
|
1941
|
-
/>
|
|
1942
|
-
</div>
|
|
1943
|
-
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-zinc-500">
|
|
1944
|
-
<div className="inline-flex items-center gap-1">
|
|
1945
|
-
<span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-emerald-600" />
|
|
1946
|
-
{t.legendDone}
|
|
2434
|
+
<div className="mb-10 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
2435
|
+
<section
|
|
2436
|
+
id="report-chart-sessions"
|
|
2437
|
+
className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
2438
|
+
>
|
|
2439
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2440
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2441
|
+
{t.chartSessionsPerDay}
|
|
2442
|
+
</h3>
|
|
2443
|
+
<ReportingFilteredBadge
|
|
2444
|
+
active={reportingFiltersActive}
|
|
2445
|
+
label={t.sectionFilteredBadge}
|
|
2446
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2447
|
+
/>
|
|
1947
2448
|
<InlineMetricHelpTrigger
|
|
1948
|
-
ariaLabel={t.
|
|
1949
|
-
body={t.
|
|
2449
|
+
ariaLabel={t.metricHelpChartSessionsAria}
|
|
2450
|
+
body={t.metricHelpChartSessionsBody}
|
|
1950
2451
|
/>
|
|
1951
2452
|
</div>
|
|
1952
|
-
<div className="
|
|
1953
|
-
<span
|
|
1954
|
-
{t.legendActive}
|
|
2453
|
+
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
|
|
2454
|
+
<span>{t.legendSessions}</span>
|
|
1955
2455
|
<InlineMetricHelpTrigger
|
|
1956
|
-
ariaLabel={t.
|
|
1957
|
-
body={t.
|
|
2456
|
+
ariaLabel={t.metricHelpLegendSessionsAria}
|
|
2457
|
+
body={t.metricHelpLegendSessionsBody}
|
|
1958
2458
|
/>
|
|
1959
2459
|
</div>
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
</
|
|
1970
|
-
</section>
|
|
2460
|
+
<div className="mt-4">
|
|
2461
|
+
<MiniBars
|
|
2462
|
+
days={reportingDayKeys}
|
|
2463
|
+
values={agg.sessionsByDay}
|
|
2464
|
+
max={peak}
|
|
2465
|
+
className="bg-violet-500/90"
|
|
2466
|
+
undatedLabel={t.undatedLabel}
|
|
2467
|
+
/>
|
|
2468
|
+
</div>
|
|
2469
|
+
</section>
|
|
1971
2470
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2471
|
+
<section
|
|
2472
|
+
id="report-chart-tasks"
|
|
2473
|
+
className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
2474
|
+
>
|
|
2475
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2476
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2477
|
+
{t.chartTasksByStatusPerDay}
|
|
2478
|
+
</h3>
|
|
2479
|
+
<ReportingFilteredBadge
|
|
2480
|
+
active={reportingFiltersActive}
|
|
2481
|
+
label={t.sectionFilteredBadge}
|
|
2482
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2483
|
+
/>
|
|
2484
|
+
<InlineMetricHelpTrigger
|
|
2485
|
+
ariaLabel={t.metricHelpChartTasksAria}
|
|
2486
|
+
body={t.metricHelpChartTasksBody}
|
|
2487
|
+
/>
|
|
2488
|
+
</div>
|
|
2489
|
+
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-zinc-500">
|
|
2490
|
+
<div className="inline-flex items-center gap-1">
|
|
2491
|
+
<span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-emerald-600" />
|
|
2492
|
+
{t.legendDone}
|
|
2493
|
+
<InlineMetricHelpTrigger
|
|
2494
|
+
ariaLabel={t.metricHelpLegendDoneAria}
|
|
2495
|
+
body={t.metricHelpLegendDoneBody}
|
|
2496
|
+
/>
|
|
2497
|
+
</div>
|
|
2498
|
+
<div className="inline-flex items-center gap-1">
|
|
2499
|
+
<span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-amber-500" />
|
|
2500
|
+
{t.legendActive}
|
|
2501
|
+
<InlineMetricHelpTrigger
|
|
2502
|
+
ariaLabel={t.metricHelpLegendActiveAria}
|
|
2503
|
+
body={t.metricHelpLegendActiveBody}
|
|
2504
|
+
/>
|
|
2505
|
+
</div>
|
|
2506
|
+
</div>
|
|
2507
|
+
<div className="mt-4">
|
|
2508
|
+
<StackedTaskBars
|
|
2509
|
+
days={reportingDayKeys}
|
|
2510
|
+
done={agg.tasksByDayDone}
|
|
2511
|
+
active={agg.tasksByDayActive}
|
|
2512
|
+
max={peakTasks}
|
|
2513
|
+
undatedLabel={t.undatedLabel}
|
|
2514
|
+
/>
|
|
2515
|
+
</div>
|
|
2516
|
+
</section>
|
|
2008
2517
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2518
|
+
<section
|
|
2519
|
+
id="report-chart-task-time"
|
|
2520
|
+
className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
2521
|
+
>
|
|
2522
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2523
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2524
|
+
{t.chartTaskTimePerDay}
|
|
2525
|
+
</h3>
|
|
2526
|
+
<ReportingFilteredBadge
|
|
2527
|
+
active={reportingFiltersActive}
|
|
2528
|
+
label={t.sectionFilteredBadge}
|
|
2529
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2530
|
+
/>
|
|
2531
|
+
<InlineMetricHelpTrigger
|
|
2532
|
+
ariaLabel={t.metricHelpChartTaskTimeAria}
|
|
2533
|
+
body={t.metricHelpChartTaskTimeBody}
|
|
2534
|
+
/>
|
|
2535
|
+
</div>
|
|
2536
|
+
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
|
|
2537
|
+
<span>{t.summaryTaskTimeRecorded}</span>
|
|
2538
|
+
<InlineMetricHelpTrigger
|
|
2539
|
+
ariaLabel={t.metricHelpTaskTimeAria}
|
|
2540
|
+
body={t.metricHelpTaskTimeBody}
|
|
2541
|
+
/>
|
|
2542
|
+
</div>
|
|
2543
|
+
<div className="mt-4">
|
|
2544
|
+
<MiniBars
|
|
2545
|
+
days={reportingDayKeys}
|
|
2546
|
+
values={agg.taskMinutesByDay}
|
|
2547
|
+
max={peakTaskMinutes}
|
|
2548
|
+
className="bg-sky-500/90"
|
|
2549
|
+
undatedLabel={t.undatedLabel}
|
|
2550
|
+
valueTitle={(m) => formatDuration(m)}
|
|
2551
|
+
/>
|
|
2552
|
+
</div>
|
|
2553
|
+
</section>
|
|
2554
|
+
|
|
2555
|
+
<section
|
|
2556
|
+
id="report-chart-session-wall"
|
|
2557
|
+
className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
2558
|
+
>
|
|
2559
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2560
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2561
|
+
{t.chartSessionWallPerDay}
|
|
2562
|
+
</h3>
|
|
2563
|
+
<ReportingFilteredBadge
|
|
2564
|
+
active={reportingFiltersActive}
|
|
2565
|
+
label={t.sectionFilteredBadge}
|
|
2566
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2567
|
+
/>
|
|
2568
|
+
<InlineMetricHelpTrigger
|
|
2569
|
+
ariaLabel={t.metricHelpChartSessionWallAria}
|
|
2570
|
+
body={t.metricHelpChartSessionWallBody}
|
|
2571
|
+
/>
|
|
2572
|
+
</div>
|
|
2573
|
+
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
|
|
2574
|
+
<span>{t.summarySessionWallClock}</span>
|
|
2575
|
+
<InlineMetricHelpTrigger
|
|
2576
|
+
ariaLabel={t.metricHelpSessionWallSummaryAria}
|
|
2577
|
+
body={t.metricHelpSessionWallSummaryBody}
|
|
2578
|
+
/>
|
|
2579
|
+
</div>
|
|
2580
|
+
<div className="mt-4">
|
|
2581
|
+
<MiniBars
|
|
2582
|
+
days={reportingDayKeys}
|
|
2583
|
+
values={agg.sessionWallClockMinutesByDay}
|
|
2584
|
+
max={peakSessionWallMinutes}
|
|
2585
|
+
className="bg-fuchsia-500/85"
|
|
2586
|
+
undatedLabel={t.undatedLabel}
|
|
2587
|
+
valueTitle={(m) => formatDuration(m)}
|
|
2588
|
+
/>
|
|
2589
|
+
</div>
|
|
2590
|
+
</section>
|
|
2591
|
+
</div>
|
|
2045
2592
|
|
|
2046
2593
|
<section
|
|
2047
2594
|
id="report-daily-table"
|
|
@@ -2115,6 +2662,11 @@ function ReportingContent() {
|
|
|
2115
2662
|
/>
|
|
2116
2663
|
</div>
|
|
2117
2664
|
</th>
|
|
2665
|
+
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2666
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2667
|
+
<span>{t.tableTaskTimeNonConcurrent}</span>
|
|
2668
|
+
</div>
|
|
2669
|
+
</th>
|
|
2118
2670
|
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2119
2671
|
<div className="flex min-h-5 items-center gap-0.5">
|
|
2120
2672
|
<span>{t.tableSessionCoding}</span>
|
|
@@ -2153,6 +2705,11 @@ function ReportingContent() {
|
|
|
2153
2705
|
<td className="px-4 py-2.5 tabular-nums text-sky-300/90">
|
|
2154
2706
|
{formatMinutesCell(agg.taskMinutesByDay[d])}
|
|
2155
2707
|
</td>
|
|
2708
|
+
<td className="px-4 py-2.5 tabular-nums text-cyan-300/90">
|
|
2709
|
+
{formatMinutesCell(
|
|
2710
|
+
agg.nonConcurrentTaskMinutesByDay[d],
|
|
2711
|
+
)}
|
|
2712
|
+
</td>
|
|
2156
2713
|
<td className="px-4 py-2.5 tabular-nums text-zinc-300">
|
|
2157
2714
|
{formatMinutesCell(
|
|
2158
2715
|
agg.sessionCodingMinutesByDay[d],
|
|
@@ -2186,196 +2743,6 @@ function ReportingContent() {
|
|
|
2186
2743
|
{t.tagTimeSectionHint}
|
|
2187
2744
|
</p>
|
|
2188
2745
|
|
|
2189
|
-
<div id="report-tag-time-day">
|
|
2190
|
-
<div className="flex min-h-6 items-center gap-0.5">
|
|
2191
|
-
<h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-400">
|
|
2192
|
-
{t.tagTimeByDayTitle}
|
|
2193
|
-
</h4>
|
|
2194
|
-
<InlineMetricHelpTrigger
|
|
2195
|
-
ariaLabel={t.metricHelpTagTimeDayTableAria}
|
|
2196
|
-
body={t.metricHelpTagTimeDayTableBody}
|
|
2197
|
-
/>
|
|
2198
|
-
</div>
|
|
2199
|
-
{tagTimeSplit.byDay.length === 0 ? (
|
|
2200
|
-
<p className="mt-3 text-sm text-zinc-500">—</p>
|
|
2201
|
-
) : (
|
|
2202
|
-
<div className="mt-3 overflow-x-auto">
|
|
2203
|
-
<table className="w-full min-w-[22rem] text-left text-sm">
|
|
2204
|
-
<thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
|
|
2205
|
-
<tr>
|
|
2206
|
-
<th className="py-2 pr-3 font-medium">
|
|
2207
|
-
<div className="flex min-h-5 items-center gap-0.5">
|
|
2208
|
-
<span>{t.tagTimeColTag}</span>
|
|
2209
|
-
<InlineMetricHelpTrigger
|
|
2210
|
-
ariaLabel={
|
|
2211
|
-
t.metricHelpTagTimeColTagAria
|
|
2212
|
-
}
|
|
2213
|
-
body={t.metricHelpTagTimeColTagBody}
|
|
2214
|
-
/>
|
|
2215
|
-
</div>
|
|
2216
|
-
</th>
|
|
2217
|
-
<th className="py-2 pr-3 font-medium tabular-nums">
|
|
2218
|
-
<div className="flex min-h-5 items-center gap-0.5">
|
|
2219
|
-
<span>{t.tagTimeColDay}</span>
|
|
2220
|
-
<InlineMetricHelpTrigger
|
|
2221
|
-
ariaLabel={
|
|
2222
|
-
t.metricHelpTagTimeColDayAria
|
|
2223
|
-
}
|
|
2224
|
-
body={t.metricHelpTagTimeColDayBody}
|
|
2225
|
-
/>
|
|
2226
|
-
</div>
|
|
2227
|
-
</th>
|
|
2228
|
-
<th className="py-2 font-medium tabular-nums">
|
|
2229
|
-
<div className="flex min-h-5 items-center gap-0.5">
|
|
2230
|
-
<span>{t.tagTimeColMinutes}</span>
|
|
2231
|
-
<InlineMetricHelpTrigger
|
|
2232
|
-
align="end"
|
|
2233
|
-
ariaLabel={
|
|
2234
|
-
t.metricHelpTagTimeColMinutesAria
|
|
2235
|
-
}
|
|
2236
|
-
body={
|
|
2237
|
-
t.metricHelpTagTimeColMinutesBody
|
|
2238
|
-
}
|
|
2239
|
-
/>
|
|
2240
|
-
</div>
|
|
2241
|
-
</th>
|
|
2242
|
-
</tr>
|
|
2243
|
-
</thead>
|
|
2244
|
-
<tbody>
|
|
2245
|
-
{groupTagDayRowsForDisplay(
|
|
2246
|
-
tagTimeSplit.byDay,
|
|
2247
|
-
).flatMap(({ day, blocks }) =>
|
|
2248
|
-
blocks.flatMap((block) => {
|
|
2249
|
-
if (block.kind === "leaf") {
|
|
2250
|
-
const row = block.row;
|
|
2251
|
-
return [
|
|
2252
|
-
<tr
|
|
2253
|
-
key={`${row.tagKey}\0${row.day}`}
|
|
2254
|
-
className="border-b border-zinc-800/80 last:border-0"
|
|
2255
|
-
>
|
|
2256
|
-
<ReportingTagNameCell
|
|
2257
|
-
tagKey={row.tagKey}
|
|
2258
|
-
displayLabel={
|
|
2259
|
-
row.displayTag || row.tagKey
|
|
2260
|
-
}
|
|
2261
|
-
untaggedLabel={t.tagTimeUntagged}
|
|
2262
|
-
descriptions={tagDescriptions}
|
|
2263
|
-
className="py-2 pr-3 text-violet-200/90"
|
|
2264
|
-
/>
|
|
2265
|
-
<td className="py-2 pr-3 tabular-nums text-zinc-300">
|
|
2266
|
-
{dayLabel(
|
|
2267
|
-
row.day,
|
|
2268
|
-
t.undatedLabel,
|
|
2269
|
-
)}
|
|
2270
|
-
</td>
|
|
2271
|
-
<td className="py-2 tabular-nums text-zinc-200">
|
|
2272
|
-
{formatDuration(row.minutes)}
|
|
2273
|
-
</td>
|
|
2274
|
-
</tr>,
|
|
2275
|
-
];
|
|
2276
|
-
}
|
|
2277
|
-
const rollupKey = `${day}:::${block.projectKeyLower}`;
|
|
2278
|
-
const open =
|
|
2279
|
-
tagDayRollupOpenKeys.has(rollupKey);
|
|
2280
|
-
const rollupDesc =
|
|
2281
|
-
reportingProjectDescriptionLine(
|
|
2282
|
-
block.projectKeyLower,
|
|
2283
|
-
projectDescriptions,
|
|
2284
|
-
) ?? "";
|
|
2285
|
-
const rollupHead = (
|
|
2286
|
-
<tr
|
|
2287
|
-
key={`${rollupKey}-rollup`}
|
|
2288
|
-
className="border-b border-zinc-800/80 bg-zinc-900/40"
|
|
2289
|
-
>
|
|
2290
|
-
<td className="py-2 pr-3 align-top text-violet-200/90">
|
|
2291
|
-
<div className="flex items-start gap-1.5">
|
|
2292
|
-
<button
|
|
2293
|
-
type="button"
|
|
2294
|
-
aria-expanded={
|
|
2295
|
-
open ? "true" : "false"
|
|
2296
|
-
}
|
|
2297
|
-
aria-label={
|
|
2298
|
-
t.tagWeekScopedRollupToggleAria
|
|
2299
|
-
}
|
|
2300
|
-
className="mt-0.5 shrink-0 rounded p-0.5 text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
|
2301
|
-
onClick={() =>
|
|
2302
|
-
toggleTagDayRollup(rollupKey)
|
|
2303
|
-
}
|
|
2304
|
-
>
|
|
2305
|
-
<ChevronRight
|
|
2306
|
-
className={`size-4 transition-transform duration-200 ${
|
|
2307
|
-
open ? "rotate-90" : ""
|
|
2308
|
-
}`}
|
|
2309
|
-
strokeWidth={2}
|
|
2310
|
-
aria-hidden
|
|
2311
|
-
/>
|
|
2312
|
-
</button>
|
|
2313
|
-
<div className="min-w-0">
|
|
2314
|
-
<div className="flex flex-wrap items-baseline gap-x-1.5 font-medium">
|
|
2315
|
-
<span>
|
|
2316
|
-
@{block.displayProject}
|
|
2317
|
-
</span>
|
|
2318
|
-
<span className="text-[0.65rem] font-normal tabular-nums text-zinc-500">
|
|
2319
|
-
({block.children.length})
|
|
2320
|
-
</span>
|
|
2321
|
-
</div>
|
|
2322
|
-
{rollupDesc ? (
|
|
2323
|
-
<p className="mt-1 max-w-[min(22rem,55vw)] whitespace-pre-line text-[0.65rem] font-normal leading-snug text-zinc-500">
|
|
2324
|
-
{rollupDesc}
|
|
2325
|
-
</p>
|
|
2326
|
-
) : null}
|
|
2327
|
-
</div>
|
|
2328
|
-
</div>
|
|
2329
|
-
</td>
|
|
2330
|
-
<td className="py-2 pr-3 tabular-nums text-zinc-300">
|
|
2331
|
-
{dayLabel(day, t.undatedLabel)}
|
|
2332
|
-
</td>
|
|
2333
|
-
<td className="py-2 tabular-nums text-zinc-200">
|
|
2334
|
-
{formatDuration(
|
|
2335
|
-
block.parentMinutes,
|
|
2336
|
-
)}
|
|
2337
|
-
</td>
|
|
2338
|
-
</tr>
|
|
2339
|
-
);
|
|
2340
|
-
if (!open) {
|
|
2341
|
-
return [rollupHead];
|
|
2342
|
-
}
|
|
2343
|
-
return [
|
|
2344
|
-
rollupHead,
|
|
2345
|
-
...block.children.map((child) => (
|
|
2346
|
-
<tr
|
|
2347
|
-
key={`${child.tagKey}\0${child.day}`}
|
|
2348
|
-
className="border-b border-zinc-800/55 bg-zinc-950/30"
|
|
2349
|
-
>
|
|
2350
|
-
<ReportingTagNameCell
|
|
2351
|
-
tagKey={child.tagKey}
|
|
2352
|
-
displayLabel={
|
|
2353
|
-
child.displayTag || child.tagKey
|
|
2354
|
-
}
|
|
2355
|
-
untaggedLabel={t.tagTimeUntagged}
|
|
2356
|
-
descriptions={tagDescriptions}
|
|
2357
|
-
className="py-2 pr-3 pl-8 text-violet-200/85"
|
|
2358
|
-
/>
|
|
2359
|
-
<td className="py-2 pr-3 tabular-nums text-zinc-300/95">
|
|
2360
|
-
{dayLabel(
|
|
2361
|
-
child.day,
|
|
2362
|
-
t.undatedLabel,
|
|
2363
|
-
)}
|
|
2364
|
-
</td>
|
|
2365
|
-
<td className="py-2 tabular-nums text-zinc-300/95">
|
|
2366
|
-
{formatDuration(child.minutes)}
|
|
2367
|
-
</td>
|
|
2368
|
-
</tr>
|
|
2369
|
-
)),
|
|
2370
|
-
];
|
|
2371
|
-
}),
|
|
2372
|
-
)}
|
|
2373
|
-
</tbody>
|
|
2374
|
-
</table>
|
|
2375
|
-
</div>
|
|
2376
|
-
)}
|
|
2377
|
-
</div>
|
|
2378
|
-
|
|
2379
2746
|
<div id="report-tag-time-week" className="space-y-4">
|
|
2380
2747
|
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2381
2748
|
<h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-400">
|
|
@@ -2556,8 +2923,24 @@ function ReportingContent() {
|
|
|
2556
2923
|
title={dateKey}
|
|
2557
2924
|
>
|
|
2558
2925
|
{mins > 0
|
|
2559
|
-
?
|
|
2926
|
+
? renderReportingDurationButton(
|
|
2560
2927
|
mins,
|
|
2928
|
+
{
|
|
2929
|
+
kind: "tag",
|
|
2930
|
+
title:
|
|
2931
|
+
row.displayTag ||
|
|
2932
|
+
row.tagKey,
|
|
2933
|
+
dayKey:
|
|
2934
|
+
dateKey,
|
|
2935
|
+
tagKey:
|
|
2936
|
+
row.tagKey,
|
|
2937
|
+
sourceLabel:
|
|
2938
|
+
dayLabel(
|
|
2939
|
+
dateKey,
|
|
2940
|
+
t.undatedLabel,
|
|
2941
|
+
),
|
|
2942
|
+
},
|
|
2943
|
+
"text-zinc-200",
|
|
2561
2944
|
)
|
|
2562
2945
|
: "—"}
|
|
2563
2946
|
</td>
|
|
@@ -2565,8 +2948,24 @@ function ReportingContent() {
|
|
|
2565
2948
|
},
|
|
2566
2949
|
)}
|
|
2567
2950
|
<td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
|
|
2568
|
-
{
|
|
2951
|
+
{renderReportingDurationButton(
|
|
2569
2952
|
row.total,
|
|
2953
|
+
{
|
|
2954
|
+
kind: "tag",
|
|
2955
|
+
title:
|
|
2956
|
+
row.displayTag ||
|
|
2957
|
+
row.tagKey,
|
|
2958
|
+
weekStart:
|
|
2959
|
+
row.weekStart,
|
|
2960
|
+
tagKey: row.tagKey,
|
|
2961
|
+
sourceLabel:
|
|
2962
|
+
weekRangeLabelForSource(
|
|
2963
|
+
row.weekStart,
|
|
2964
|
+
lang,
|
|
2965
|
+
reportLocale,
|
|
2966
|
+
),
|
|
2967
|
+
},
|
|
2968
|
+
"text-zinc-100",
|
|
2570
2969
|
)}
|
|
2571
2970
|
</td>
|
|
2572
2971
|
</tr>
|
|
@@ -2653,8 +3052,22 @@ function ReportingContent() {
|
|
|
2653
3052
|
title={dateKey}
|
|
2654
3053
|
>
|
|
2655
3054
|
{mins > 0
|
|
2656
|
-
?
|
|
3055
|
+
? renderReportingDurationButton(
|
|
2657
3056
|
mins,
|
|
3057
|
+
{
|
|
3058
|
+
kind: "project",
|
|
3059
|
+
title: `@${block.displayProject}`,
|
|
3060
|
+
dayKey:
|
|
3061
|
+
dateKey,
|
|
3062
|
+
projectKey:
|
|
3063
|
+
block.projectKeyLower,
|
|
3064
|
+
sourceLabel:
|
|
3065
|
+
dayLabel(
|
|
3066
|
+
dateKey,
|
|
3067
|
+
t.undatedLabel,
|
|
3068
|
+
),
|
|
3069
|
+
},
|
|
3070
|
+
"text-zinc-200",
|
|
2658
3071
|
)
|
|
2659
3072
|
: "—"}
|
|
2660
3073
|
</td>
|
|
@@ -2662,8 +3075,23 @@ function ReportingContent() {
|
|
|
2662
3075
|
},
|
|
2663
3076
|
)}
|
|
2664
3077
|
<td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
|
|
2665
|
-
{
|
|
3078
|
+
{renderReportingDurationButton(
|
|
2666
3079
|
block.parentTotal,
|
|
3080
|
+
{
|
|
3081
|
+
kind: "project",
|
|
3082
|
+
title: `@${block.displayProject}`,
|
|
3083
|
+
weekStart:
|
|
3084
|
+
block.weekStart,
|
|
3085
|
+
projectKey:
|
|
3086
|
+
block.projectKeyLower,
|
|
3087
|
+
sourceLabel:
|
|
3088
|
+
weekRangeLabelForSource(
|
|
3089
|
+
block.weekStart,
|
|
3090
|
+
lang,
|
|
3091
|
+
reportLocale,
|
|
3092
|
+
),
|
|
3093
|
+
},
|
|
3094
|
+
"text-zinc-100",
|
|
2667
3095
|
)}
|
|
2668
3096
|
</td>
|
|
2669
3097
|
</tr>
|
|
@@ -2708,8 +3136,24 @@ function ReportingContent() {
|
|
|
2708
3136
|
}
|
|
2709
3137
|
>
|
|
2710
3138
|
{mins > 0
|
|
2711
|
-
?
|
|
3139
|
+
? renderReportingDurationButton(
|
|
2712
3140
|
mins,
|
|
3141
|
+
{
|
|
3142
|
+
kind: "tag",
|
|
3143
|
+
title:
|
|
3144
|
+
child.displayTag ||
|
|
3145
|
+
child.tagKey,
|
|
3146
|
+
dayKey:
|
|
3147
|
+
dateKey,
|
|
3148
|
+
tagKey:
|
|
3149
|
+
child.tagKey,
|
|
3150
|
+
sourceLabel:
|
|
3151
|
+
dayLabel(
|
|
3152
|
+
dateKey,
|
|
3153
|
+
t.undatedLabel,
|
|
3154
|
+
),
|
|
3155
|
+
},
|
|
3156
|
+
"text-zinc-300/95",
|
|
2713
3157
|
)
|
|
2714
3158
|
: "—"}
|
|
2715
3159
|
</td>
|
|
@@ -2717,8 +3161,25 @@ function ReportingContent() {
|
|
|
2717
3161
|
},
|
|
2718
3162
|
)}
|
|
2719
3163
|
<td className="px-2 py-2 pr-3 text-center tabular-nums font-medium text-zinc-200">
|
|
2720
|
-
{
|
|
3164
|
+
{renderReportingDurationButton(
|
|
2721
3165
|
child.total,
|
|
3166
|
+
{
|
|
3167
|
+
kind: "tag",
|
|
3168
|
+
title:
|
|
3169
|
+
child.displayTag ||
|
|
3170
|
+
child.tagKey,
|
|
3171
|
+
weekStart:
|
|
3172
|
+
child.weekStart,
|
|
3173
|
+
tagKey:
|
|
3174
|
+
child.tagKey,
|
|
3175
|
+
sourceLabel:
|
|
3176
|
+
weekRangeLabelForSource(
|
|
3177
|
+
child.weekStart,
|
|
3178
|
+
lang,
|
|
3179
|
+
reportLocale,
|
|
3180
|
+
),
|
|
3181
|
+
},
|
|
3182
|
+
"text-zinc-200",
|
|
2722
3183
|
)}
|
|
2723
3184
|
</td>
|
|
2724
3185
|
</tr>
|
|
@@ -2856,45 +3317,239 @@ function ReportingContent() {
|
|
|
2856
3317
|
</tr>
|
|
2857
3318
|
</thead>
|
|
2858
3319
|
<tbody>
|
|
2859
|
-
{rows.map((row) =>
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3320
|
+
{rows.map((row) => {
|
|
3321
|
+
const projectKeyLower =
|
|
3322
|
+
normalizeProjectKey(
|
|
3323
|
+
row.projectKey,
|
|
3324
|
+
).toLowerCase();
|
|
3325
|
+
const breakdownKey = `${weekStart}:::${projectKeyLower}`;
|
|
3326
|
+
const childTagRows =
|
|
3327
|
+
projectScopedTagRowsByWeekProject.get(
|
|
3328
|
+
breakdownKey,
|
|
3329
|
+
) ?? [];
|
|
3330
|
+
const canExpand =
|
|
3331
|
+
childTagRows.length > 0;
|
|
3332
|
+
const isOpen =
|
|
3333
|
+
canExpand &&
|
|
3334
|
+
projectWeekTagBreakdownOpenKeys.has(
|
|
3335
|
+
breakdownKey,
|
|
3336
|
+
);
|
|
3337
|
+
const rowTitle =
|
|
3338
|
+
row.displayProject ||
|
|
3339
|
+
row.projectKey;
|
|
3340
|
+
const rowProjectDescription =
|
|
3341
|
+
reportingProjectDescriptionLine(
|
|
3342
|
+
row.projectKey,
|
|
3343
|
+
projectDescriptions,
|
|
3344
|
+
) ?? "";
|
|
3345
|
+
return (
|
|
3346
|
+
<Fragment
|
|
3347
|
+
key={`${weekStart}\0${row.projectKey}`}
|
|
3348
|
+
>
|
|
3349
|
+
<tr className="border-b border-zinc-800/70 last:border-0">
|
|
3350
|
+
{canExpand ? (
|
|
3351
|
+
<td className="align-top px-2 py-2 pl-3 text-sky-200/90">
|
|
3352
|
+
<div className="flex items-start gap-1.5">
|
|
3353
|
+
<button
|
|
3354
|
+
type="button"
|
|
3355
|
+
aria-expanded={isOpen}
|
|
3356
|
+
aria-label={
|
|
3357
|
+
t.tagWeekScopedRollupToggleAria
|
|
3358
|
+
}
|
|
3359
|
+
className="mt-0.5 shrink-0 rounded p-0.5 text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
|
3360
|
+
onClick={() =>
|
|
3361
|
+
toggleProjectWeekTagBreakdown(
|
|
3362
|
+
breakdownKey,
|
|
3363
|
+
)
|
|
3364
|
+
}
|
|
3365
|
+
>
|
|
3366
|
+
<ChevronRight
|
|
3367
|
+
className={`size-4 transition-transform duration-200 ${
|
|
3368
|
+
isOpen
|
|
3369
|
+
? "rotate-90"
|
|
3370
|
+
: ""
|
|
3371
|
+
}`}
|
|
3372
|
+
strokeWidth={2}
|
|
3373
|
+
aria-hidden
|
|
3374
|
+
/>
|
|
3375
|
+
</button>
|
|
3376
|
+
<div className="min-w-0">
|
|
3377
|
+
<div>
|
|
3378
|
+
{row.projectKey === ""
|
|
3379
|
+
? t.projectUnassigned
|
|
3380
|
+
: row.displayProject ||
|
|
3381
|
+
row.projectKey}
|
|
3382
|
+
</div>
|
|
3383
|
+
{rowProjectDescription ? (
|
|
3384
|
+
<p className="mt-1 max-w-[min(22rem,55vw)] whitespace-pre-line text-[0.65rem] font-normal leading-snug text-zinc-500">
|
|
3385
|
+
{
|
|
3386
|
+
rowProjectDescription
|
|
3387
|
+
}
|
|
3388
|
+
</p>
|
|
3389
|
+
) : null}
|
|
3390
|
+
<p className="mt-1 text-[0.65rem] font-normal tabular-nums text-zinc-500">
|
|
3391
|
+
{childTagRows.length}{" "}
|
|
3392
|
+
{lang === "fr"
|
|
3393
|
+
? "étiquette(s) liée(s)"
|
|
3394
|
+
: "linked tag(s)"}
|
|
3395
|
+
</p>
|
|
3396
|
+
</div>
|
|
3397
|
+
</div>
|
|
3398
|
+
</td>
|
|
3399
|
+
) : (
|
|
3400
|
+
<ReportingProjectNameCell
|
|
3401
|
+
projectKey={row.projectKey}
|
|
3402
|
+
displayLabel={
|
|
3403
|
+
row.displayProject ||
|
|
3404
|
+
row.projectKey
|
|
3405
|
+
}
|
|
3406
|
+
unassignedLabel={
|
|
3407
|
+
t.projectUnassigned
|
|
3408
|
+
}
|
|
3409
|
+
descriptions={
|
|
3410
|
+
projectDescriptions
|
|
3411
|
+
}
|
|
3412
|
+
className="px-2 py-2 pl-3 text-sky-200/90"
|
|
3413
|
+
/>
|
|
3414
|
+
)}
|
|
3415
|
+
{row.slots.map((mins, i) => {
|
|
3416
|
+
const dateKey = addDaysYmd(
|
|
3417
|
+
row.weekStart,
|
|
3418
|
+
i,
|
|
3419
|
+
);
|
|
3420
|
+
return (
|
|
3421
|
+
<td
|
|
3422
|
+
key={dateKey}
|
|
3423
|
+
className="px-1 py-2 text-center tabular-nums text-zinc-200"
|
|
3424
|
+
title={dateKey}
|
|
3425
|
+
>
|
|
3426
|
+
{mins > 0
|
|
3427
|
+
? renderReportingDurationButton(
|
|
3428
|
+
mins,
|
|
3429
|
+
{
|
|
3430
|
+
kind: "project",
|
|
3431
|
+
title: rowTitle,
|
|
3432
|
+
dayKey: dateKey,
|
|
3433
|
+
projectKey:
|
|
3434
|
+
row.projectKey,
|
|
3435
|
+
sourceLabel:
|
|
3436
|
+
dayLabel(
|
|
3437
|
+
dateKey,
|
|
3438
|
+
t.undatedLabel,
|
|
3439
|
+
),
|
|
3440
|
+
},
|
|
3441
|
+
"text-zinc-200",
|
|
3442
|
+
)
|
|
3443
|
+
: "—"}
|
|
3444
|
+
</td>
|
|
3445
|
+
);
|
|
3446
|
+
})}
|
|
3447
|
+
<td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
|
|
3448
|
+
{renderReportingDurationButton(
|
|
3449
|
+
row.total,
|
|
3450
|
+
{
|
|
3451
|
+
kind: "project",
|
|
3452
|
+
title: rowTitle,
|
|
3453
|
+
weekStart: row.weekStart,
|
|
3454
|
+
projectKey:
|
|
3455
|
+
row.projectKey,
|
|
3456
|
+
sourceLabel:
|
|
3457
|
+
weekRangeLabelForSource(
|
|
3458
|
+
row.weekStart,
|
|
3459
|
+
lang,
|
|
3460
|
+
reportLocale,
|
|
3461
|
+
),
|
|
3462
|
+
},
|
|
3463
|
+
"text-zinc-100",
|
|
3464
|
+
)}
|
|
2890
3465
|
</td>
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3466
|
+
</tr>
|
|
3467
|
+
{isOpen
|
|
3468
|
+
? childTagRows.map((child) => (
|
|
3469
|
+
<tr
|
|
3470
|
+
key={`${weekStart}\0${row.projectKey}\0${child.tagKey}`}
|
|
3471
|
+
className="border-b border-zinc-800/55 bg-zinc-950/30"
|
|
3472
|
+
>
|
|
3473
|
+
<ReportingTagNameCell
|
|
3474
|
+
tagKey={child.tagKey}
|
|
3475
|
+
displayLabel={
|
|
3476
|
+
child.displayTag ||
|
|
3477
|
+
child.tagKey
|
|
3478
|
+
}
|
|
3479
|
+
untaggedLabel={
|
|
3480
|
+
t.tagTimeUntagged
|
|
3481
|
+
}
|
|
3482
|
+
descriptions={
|
|
3483
|
+
tagDescriptions
|
|
3484
|
+
}
|
|
3485
|
+
className="px-2 py-2 pl-10 text-violet-200/85"
|
|
3486
|
+
/>
|
|
3487
|
+
{child.slots.map(
|
|
3488
|
+
(mins, i) => {
|
|
3489
|
+
const dateKey =
|
|
3490
|
+
addDaysYmd(
|
|
3491
|
+
child.weekStart,
|
|
3492
|
+
i,
|
|
3493
|
+
);
|
|
3494
|
+
return (
|
|
3495
|
+
<td
|
|
3496
|
+
key={dateKey}
|
|
3497
|
+
className="px-1 py-2 text-center tabular-nums text-zinc-300/95"
|
|
3498
|
+
title={dateKey}
|
|
3499
|
+
>
|
|
3500
|
+
{mins > 0
|
|
3501
|
+
? renderReportingDurationButton(
|
|
3502
|
+
mins,
|
|
3503
|
+
{
|
|
3504
|
+
kind: "tag",
|
|
3505
|
+
title:
|
|
3506
|
+
child.displayTag ||
|
|
3507
|
+
child.tagKey,
|
|
3508
|
+
dayKey:
|
|
3509
|
+
dateKey,
|
|
3510
|
+
tagKey:
|
|
3511
|
+
child.tagKey,
|
|
3512
|
+
sourceLabel:
|
|
3513
|
+
dayLabel(
|
|
3514
|
+
dateKey,
|
|
3515
|
+
t.undatedLabel,
|
|
3516
|
+
),
|
|
3517
|
+
},
|
|
3518
|
+
"text-zinc-300/95",
|
|
3519
|
+
)
|
|
3520
|
+
: "—"}
|
|
3521
|
+
</td>
|
|
3522
|
+
);
|
|
3523
|
+
},
|
|
3524
|
+
)}
|
|
3525
|
+
<td className="px-2 py-2 pr-3 text-center tabular-nums font-medium text-zinc-200">
|
|
3526
|
+
{renderReportingDurationButton(
|
|
3527
|
+
child.total,
|
|
3528
|
+
{
|
|
3529
|
+
kind: "tag",
|
|
3530
|
+
title:
|
|
3531
|
+
child.displayTag ||
|
|
3532
|
+
child.tagKey,
|
|
3533
|
+
weekStart:
|
|
3534
|
+
child.weekStart,
|
|
3535
|
+
tagKey:
|
|
3536
|
+
child.tagKey,
|
|
3537
|
+
sourceLabel:
|
|
3538
|
+
weekRangeLabelForSource(
|
|
3539
|
+
child.weekStart,
|
|
3540
|
+
lang,
|
|
3541
|
+
reportLocale,
|
|
3542
|
+
),
|
|
3543
|
+
},
|
|
3544
|
+
"text-zinc-200",
|
|
3545
|
+
)}
|
|
3546
|
+
</td>
|
|
3547
|
+
</tr>
|
|
3548
|
+
))
|
|
3549
|
+
: null}
|
|
3550
|
+
</Fragment>
|
|
3551
|
+
);
|
|
3552
|
+
})}
|
|
2898
3553
|
</tbody>
|
|
2899
3554
|
</table>
|
|
2900
3555
|
</div>
|
|
@@ -2917,7 +3572,144 @@ function ReportingContent() {
|
|
|
2917
3572
|
</div>
|
|
2918
3573
|
)}
|
|
2919
3574
|
</div>
|
|
3575
|
+
<div
|
|
3576
|
+
aria-hidden
|
|
3577
|
+
className="pointer-events-none h-[45vh] min-h-44 max-h-80"
|
|
3578
|
+
/>
|
|
2920
3579
|
<ScrollToTopFab ariaLabel={t.scrollToTopAria} />
|
|
3580
|
+
{portalReady && taskInspectScope
|
|
3581
|
+
? createPortal(
|
|
3582
|
+
<div
|
|
3583
|
+
className="fixed inset-0 z-[140] bg-black/65 p-4"
|
|
3584
|
+
role="dialog"
|
|
3585
|
+
aria-modal="true"
|
|
3586
|
+
aria-label={lang === "fr" ? "Détails des tâches" : "Task details"}
|
|
3587
|
+
onClick={(e) => {
|
|
3588
|
+
if (e.target === e.currentTarget) {
|
|
3589
|
+
setTaskInspectScope(null);
|
|
3590
|
+
setTaskInspectAnchor(null);
|
|
3591
|
+
setTaskInspectModalRect(null);
|
|
3592
|
+
}
|
|
3593
|
+
}}
|
|
3594
|
+
>
|
|
3595
|
+
{taskInspectAnchor && taskInspectConnector ? (
|
|
3596
|
+
<>
|
|
3597
|
+
<div
|
|
3598
|
+
className="pointer-events-none fixed z-[141] size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-200/70 shadow-[0_0_0_1px_rgba(255,255,255,0.08)]"
|
|
3599
|
+
style={{
|
|
3600
|
+
left: `${taskInspectAnchor.x}px`,
|
|
3601
|
+
top: `${taskInspectAnchor.y}px`,
|
|
3602
|
+
}}
|
|
3603
|
+
/>
|
|
3604
|
+
<div
|
|
3605
|
+
className="pointer-events-none fixed z-[141] h-px origin-left border-t border-dashed border-violet-200/45"
|
|
3606
|
+
style={{
|
|
3607
|
+
left: `${taskInspectConnector.lineStartX}px`,
|
|
3608
|
+
top: `${taskInspectConnector.lineStartY}px`,
|
|
3609
|
+
width: `${taskInspectConnector.length}px`,
|
|
3610
|
+
transform: `rotate(${taskInspectConnector.angleRad}rad)`,
|
|
3611
|
+
}}
|
|
3612
|
+
/>
|
|
3613
|
+
</>
|
|
3614
|
+
) : null}
|
|
3615
|
+
<div
|
|
3616
|
+
ref={taskInspectModalRef}
|
|
3617
|
+
className="max-h-[72vh] overflow-hidden rounded-xl border border-zinc-700 bg-zinc-900 shadow-2xl"
|
|
3618
|
+
style={
|
|
3619
|
+
taskInspectPlacement
|
|
3620
|
+
? {
|
|
3621
|
+
position: "fixed" as const,
|
|
3622
|
+
left: `${taskInspectPlacement.left}px`,
|
|
3623
|
+
top: `${
|
|
3624
|
+
taskInspectPlacementTop ?? taskInspectPlacement.top
|
|
3625
|
+
}px`,
|
|
3626
|
+
width: `${taskInspectPlacement.width}px`,
|
|
3627
|
+
}
|
|
3628
|
+
: {
|
|
3629
|
+
position: "fixed" as const,
|
|
3630
|
+
left: "50%",
|
|
3631
|
+
top: "50%",
|
|
3632
|
+
width: "min(56rem, calc(100vw - 2rem))",
|
|
3633
|
+
transform: "translate(-50%, -50%)",
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
>
|
|
3637
|
+
<div className="flex items-center justify-between gap-3 border-b border-zinc-700 px-4 py-3">
|
|
3638
|
+
<div className="min-w-0">
|
|
3639
|
+
<h2 className="min-w-0 truncate text-sm font-semibold text-zinc-100 sm:text-base">
|
|
3640
|
+
{lang === "fr"
|
|
3641
|
+
? `Détails des tâches - ${taskInspectScope.title}`
|
|
3642
|
+
: `Task details - ${taskInspectScope.title}`}
|
|
3643
|
+
</h2>
|
|
3644
|
+
{taskInspectScope.sourceLabel ? (
|
|
3645
|
+
<p className="mt-0.5 truncate text-[0.7rem] text-zinc-400">
|
|
3646
|
+
{lang === "fr"
|
|
3647
|
+
? `Source : ${
|
|
3648
|
+
taskInspectScope.kind === "tag"
|
|
3649
|
+
? "Étiquette"
|
|
3650
|
+
: "Projet"
|
|
3651
|
+
} - ${taskInspectScope.sourceLabel}`
|
|
3652
|
+
: `Source: ${
|
|
3653
|
+
taskInspectScope.kind === "tag"
|
|
3654
|
+
? "Tag"
|
|
3655
|
+
: "Project"
|
|
3656
|
+
} - ${taskInspectScope.sourceLabel}`}
|
|
3657
|
+
</p>
|
|
3658
|
+
) : null}
|
|
3659
|
+
</div>
|
|
3660
|
+
<button
|
|
3661
|
+
type="button"
|
|
3662
|
+
className="rounded border border-zinc-600 px-2 py-1 text-xs text-zinc-200 hover:bg-zinc-800"
|
|
3663
|
+
onClick={() => {
|
|
3664
|
+
setTaskInspectScope(null);
|
|
3665
|
+
setTaskInspectAnchor(null);
|
|
3666
|
+
setTaskInspectModalRect(null);
|
|
3667
|
+
}}
|
|
3668
|
+
>
|
|
3669
|
+
{lang === "fr" ? "Fermer" : "Close"}
|
|
3670
|
+
</button>
|
|
3671
|
+
</div>
|
|
3672
|
+
<div className="max-h-[calc(72vh-3.25rem)] overflow-y-auto p-4">
|
|
3673
|
+
{taskInspectList.length > 0 ? (
|
|
3674
|
+
<div className="space-y-3">
|
|
3675
|
+
{taskInspectList.map((entry) => (
|
|
3676
|
+
<TaskSessionLiveCard
|
|
3677
|
+
key={`${entry.sessionId}\0${entry.task.id}`}
|
|
3678
|
+
task={entry.task}
|
|
3679
|
+
lang={lang}
|
|
3680
|
+
isInspecting
|
|
3681
|
+
inspectingId={entry.sessionId}
|
|
3682
|
+
knownTags={tagKeys}
|
|
3683
|
+
knownProjects={knownProjects}
|
|
3684
|
+
post={postReportingTaskAction}
|
|
3685
|
+
t={dt}
|
|
3686
|
+
confirmDeleteTask={() => {}}
|
|
3687
|
+
kronoFocusIsRunningOrPaused={false}
|
|
3688
|
+
showKronoFocusTaskActions={false}
|
|
3689
|
+
startKronoFocusFromTask={() => {}}
|
|
3690
|
+
onDuplicateTask={() => {}}
|
|
3691
|
+
pausePlayMode={entry.pausePlayMode}
|
|
3692
|
+
onPausePlay={() => {}}
|
|
3693
|
+
showTaskActionButtons={false}
|
|
3694
|
+
allowAddSubtasks={false}
|
|
3695
|
+
showLiveTimerWhenInspecting
|
|
3696
|
+
deriveLiveTimerFromStartTime={false}
|
|
3697
|
+
/>
|
|
3698
|
+
))}
|
|
3699
|
+
</div>
|
|
3700
|
+
) : (
|
|
3701
|
+
<p className="text-sm text-zinc-400">
|
|
3702
|
+
{lang === "fr"
|
|
3703
|
+
? "Aucune tâche correspondante pour ce segment de temps."
|
|
3704
|
+
: "No matching tasks for this time segment."}
|
|
3705
|
+
</p>
|
|
3706
|
+
)}
|
|
3707
|
+
</div>
|
|
3708
|
+
</div>
|
|
3709
|
+
</div>,
|
|
3710
|
+
document.body,
|
|
3711
|
+
)
|
|
3712
|
+
: null}
|
|
2921
3713
|
<ReportingTour
|
|
2922
3714
|
open={reportingTourOpen}
|
|
2923
3715
|
onOpenChange={setReportingTourOpen}
|