@nightkatana/kronosys-app 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/app/api/action/route.ts +16 -0
- package/app/api/backup/route.ts +84 -0
- package/app/api/health/route.ts +22 -0
- package/app/api/state/route.ts +27 -0
- package/app/apple-icon.png +0 -0
- package/app/changelog/page.tsx +122 -0
- package/app/globals.css +210 -0
- package/app/guide/layout.tsx +11 -0
- package/app/guide/page.tsx +278 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +77 -0
- package/app/licenses/layout.tsx +11 -0
- package/app/licenses/page.tsx +246 -0
- package/app/manifest.ts +32 -0
- package/app/page.tsx +1610 -0
- package/app/reporting/page.tsx +2943 -0
- package/app/settings/layout.tsx +10 -0
- package/app/settings/page.tsx +3518 -0
- package/bin/kronosys.mjs +46 -0
- package/components/KronosysPackageVersionProvider.tsx +19 -0
- package/components/KronosysPayloadProvider.tsx +109 -0
- package/components/PwaRegister.tsx +25 -0
- package/components/SiteLegalFooter.tsx +21 -0
- package/components/ThemeProvider.tsx +78 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
- package/components/dashboard/AppShellRouteNav.tsx +131 -0
- package/components/dashboard/AppVersionStamp.tsx +16 -0
- package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
- package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
- package/components/dashboard/DashboardCommandCenter.tsx +470 -0
- package/components/dashboard/DashboardLangGateModal.tsx +118 -0
- package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
- package/components/dashboard/DashboardSimpleModal.tsx +337 -0
- package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
- package/components/dashboard/DashboardToastProvider.tsx +64 -0
- package/components/dashboard/DashboardTour.tsx +435 -0
- package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
- package/components/dashboard/DeleteSessionModal.tsx +130 -0
- package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
- package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
- package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
- package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
- package/components/dashboard/IssuePickerModal.tsx +168 -0
- package/components/dashboard/KronoFocusPanel.tsx +834 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
- package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
- package/components/dashboard/LanguageMenu.tsx +123 -0
- package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
- package/components/dashboard/NewSessionScopeModal.tsx +410 -0
- package/components/dashboard/PageRefreshButton.tsx +130 -0
- package/components/dashboard/PlainHelpPopover.tsx +97 -0
- package/components/dashboard/ReportingPageToc.tsx +68 -0
- package/components/dashboard/ReportingTour.tsx +342 -0
- package/components/dashboard/SavedProjectPicker.tsx +92 -0
- package/components/dashboard/SavedTagPicker.tsx +115 -0
- package/components/dashboard/ScrollToTopFab.tsx +41 -0
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
- package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
- package/components/dashboard/SessionListPanel.tsx +320 -0
- package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
- package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
- package/components/dashboard/SettingsTour.tsx +332 -0
- package/components/dashboard/TagPills.tsx +149 -0
- package/components/dashboard/TagsHelpTrigger.tsx +84 -0
- package/components/dashboard/TaskFocusPanel.tsx +1261 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
- package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
- package/components/dashboard/ThemeToggle.test.tsx +26 -0
- package/components/dashboard/ThemeToggle.tsx +36 -0
- package/components/dashboard/UserGuideBodyText.tsx +62 -0
- package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
- package/components/dashboard/taskFieldStyles.ts +139 -0
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
- package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
- package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
- package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
- package/lib/appShellHeaderClasses.ts +12 -0
- package/lib/backupCsvExport.test.ts +149 -0
- package/lib/backupCsvExport.ts +392 -0
- package/lib/changelogCopy.ts +34 -0
- package/lib/concurrentTaskStartPreference.ts +29 -0
- package/lib/dashboardClockFormat.ts +13 -0
- package/lib/dashboardColumnChrome.ts +3 -0
- package/lib/dashboardColumnHintsStorage.ts +57 -0
- package/lib/dashboardCopy.ts +1831 -0
- package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
- package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
- package/lib/dashboardLangStorage.ts +72 -0
- package/lib/dashboardQuickSearch.ts +476 -0
- package/lib/dashboardQuickSearchQuery.test.ts +63 -0
- package/lib/dashboardQuickSearchQuery.ts +179 -0
- package/lib/dashboardSessionNav.ts +33 -0
- package/lib/dashboardShortcuts.ts +268 -0
- package/lib/dashboardTimeZone.ts +91 -0
- package/lib/dashboardTourStorage.ts +68 -0
- package/lib/dataDir.test.ts +87 -0
- package/lib/dataDir.ts +83 -0
- package/lib/devDataPreferenceFile.ts +55 -0
- package/lib/devDataRuntimeInfo.ts +34 -0
- package/lib/formatIsoShort.test.ts +46 -0
- package/lib/formatIsoShort.ts +29 -0
- package/lib/generatedUserChangelog.ts +34 -0
- package/lib/gitlabIssueSearch.ts +8 -0
- package/lib/kronoFocusDurationHistory.ts +71 -0
- package/lib/kronoFocusRhythm.test.ts +130 -0
- package/lib/kronoFocusRhythm.ts +46 -0
- package/lib/kronoFocusTimerUrgency.test.ts +74 -0
- package/lib/kronoFocusTimerUrgency.ts +24 -0
- package/lib/kronosysApi.ts +143 -0
- package/lib/legacyEditorPayloadKeys.ts +52 -0
- package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
- package/lib/legacyKronoFocusStorageKeys.ts +32 -0
- package/lib/licensesCopy.ts +128 -0
- package/lib/openPlainTextInNewTab.ts +49 -0
- package/lib/readKronosysPackageVersion.ts +10 -0
- package/lib/reportingAggregate.test.ts +325 -0
- package/lib/reportingAggregate.ts +819 -0
- package/lib/reportingDatePresets.ts +41 -0
- package/lib/reportingMetricHelp.ts +430 -0
- package/lib/reportingNonFinalIndicators.test.ts +157 -0
- package/lib/reportingNonFinalIndicators.ts +102 -0
- package/lib/reportingStrings.ts +491 -0
- package/lib/reportingTagWeekBreakdown.test.ts +141 -0
- package/lib/reportingTagWeekBreakdown.ts +181 -0
- package/lib/reportingWeekLayout.test.ts +239 -0
- package/lib/reportingWeekLayout.ts +313 -0
- package/lib/sessionAssiduity.test.ts +25 -0
- package/lib/sessionAssiduity.ts +33 -0
- package/lib/sessionEndReason.ts +55 -0
- package/lib/sessionEndWarnings.test.ts +200 -0
- package/lib/sessionEndWarnings.ts +125 -0
- package/lib/sessionListMerge.test.ts +101 -0
- package/lib/sessionListMerge.ts +70 -0
- package/lib/sessionTaskSidebarStats.test.ts +24 -0
- package/lib/sessionTaskSidebarStats.ts +54 -0
- package/lib/settingsCopy.ts +1276 -0
- package/lib/taskParsing.test.ts +153 -0
- package/lib/taskParsing.ts +737 -0
- package/lib/theme.ts +15 -0
- package/lib/translucentButtonClasses.ts +34 -0
- package/lib/usageProfile.test.ts +84 -0
- package/lib/usageProfile.ts +52 -0
- package/lib/userGuideCopy.ts +464 -0
- package/lib/workspaceLocDefaults.ts +21 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +15 -0
- package/package.json +87 -0
- package/postcss.config.mjs +12 -0
- package/public/apple-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.png +0 -0
- package/public/next.svg +1 -0
- package/public/sw.js +13 -0
- package/public/traceback.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/actionDispatch.test.ts +723 -0
- package/server/actionDispatch.ts +1476 -0
- package/server/actionTaskSession.test.ts +713 -0
- package/server/actionTaskSession.ts +717 -0
- package/server/db.ts +42 -0
- package/server/defaultCfg.ts +87 -0
- package/server/gitlabTokenStore.ts +34 -0
- package/server/kronoFocusHydrate.test.ts +142 -0
- package/server/kronoFocusHydrate.ts +69 -0
- package/server/kronoFocusMigrate.test.ts +53 -0
- package/server/kronoFocusMigrate.ts +78 -0
- package/server/mainTimerHydrate.test.ts +65 -0
- package/server/mainTimerHydrate.ts +53 -0
- package/server/payloadStore.test.ts +78 -0
- package/server/payloadStore.ts +83 -0
- package/server/sessionWallHydrate.test.ts +46 -0
- package/server/sessionWallHydrate.ts +88 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,2943 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Fragment,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
Suspense,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
import Link from "next/link";
|
|
13
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
14
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
15
|
+
import {
|
|
16
|
+
postKronosysAction,
|
|
17
|
+
type WorkspaceCodeSnapshotPayload,
|
|
18
|
+
} from "@/lib/kronosysApi";
|
|
19
|
+
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
20
|
+
import {
|
|
21
|
+
formatDuration,
|
|
22
|
+
formatTagDisplay,
|
|
23
|
+
isFallbackTaskTagKey,
|
|
24
|
+
normalizeProjectKey,
|
|
25
|
+
normalizeTagKey,
|
|
26
|
+
readTaskDefaultTagBucketEnabled,
|
|
27
|
+
} from "@/lib/taskParsing";
|
|
28
|
+
import {
|
|
29
|
+
UNDATED_KEY,
|
|
30
|
+
aggregateArchivedExcludedTaskMinutes,
|
|
31
|
+
aggregateReporting,
|
|
32
|
+
aggregateProjectTaskMinutesByDay,
|
|
33
|
+
aggregateTagTaskMinutesByDayAndWeek,
|
|
34
|
+
buildTagFilterSet,
|
|
35
|
+
mergeSessionsFromPayload,
|
|
36
|
+
sortedDayKeys,
|
|
37
|
+
type ReportingTagTimeDayRow,
|
|
38
|
+
type ReportingTagTimeWeekRow,
|
|
39
|
+
} from "@/lib/reportingAggregate";
|
|
40
|
+
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
41
|
+
import {
|
|
42
|
+
appShellHeaderClassName,
|
|
43
|
+
appShellHeaderToolRowClassName,
|
|
44
|
+
} from "@/lib/appShellHeaderClasses";
|
|
45
|
+
import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
|
|
46
|
+
import {
|
|
47
|
+
reportingPresetDay,
|
|
48
|
+
reportingPresetMonth,
|
|
49
|
+
reportingPresetWeek,
|
|
50
|
+
reportingPresetYear,
|
|
51
|
+
} from "@/lib/reportingDatePresets";
|
|
52
|
+
import { InlineMetricHelpTrigger } from "@/components/dashboard/InlineMetricHelpTrigger";
|
|
53
|
+
import {
|
|
54
|
+
ReportingPageTocDesktop,
|
|
55
|
+
ReportingPageTocMobile,
|
|
56
|
+
} from "@/components/dashboard/ReportingPageToc";
|
|
57
|
+
import { reportingStrings } from "@/lib/reportingStrings";
|
|
58
|
+
import { trackCodeMetricsFromCfg } from "@/lib/usageProfile";
|
|
59
|
+
import {
|
|
60
|
+
addDaysYmd,
|
|
61
|
+
buildProjectWeekCalendarRows,
|
|
62
|
+
buildTagWeekCalendarRows,
|
|
63
|
+
formatWeekRangeLabel,
|
|
64
|
+
localWeekStartKeyFromDayKey,
|
|
65
|
+
readWeekStartsOnFromStorage,
|
|
66
|
+
type ReportingWeekStartsOn,
|
|
67
|
+
weekdayDateColumnHeaders,
|
|
68
|
+
writeWeekStartsOnToStorage,
|
|
69
|
+
type ProjectWeekCalendarRow,
|
|
70
|
+
type TagWeekCalendarRow,
|
|
71
|
+
} from "@/lib/reportingWeekLayout";
|
|
72
|
+
import { computeReportingNonFinalFlags } from "@/lib/reportingNonFinalIndicators";
|
|
73
|
+
import {
|
|
74
|
+
buildTagWeekDisplayBlocks,
|
|
75
|
+
groupTagDayRowsForDisplay,
|
|
76
|
+
} from "@/lib/reportingTagWeekBreakdown";
|
|
77
|
+
import { ReportingTour } from "@/components/dashboard/ReportingTour";
|
|
78
|
+
import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
|
|
79
|
+
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
80
|
+
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
81
|
+
import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
|
|
82
|
+
import { readDashboardTimeZoneFromCfg } from "@/lib/dashboardTimeZone";
|
|
83
|
+
import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
|
|
84
|
+
import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
|
|
85
|
+
import {
|
|
86
|
+
isReportingTourCompleted,
|
|
87
|
+
resetReportingTour,
|
|
88
|
+
} from "@/lib/dashboardTourStorage";
|
|
89
|
+
|
|
90
|
+
type LiveShape = { language?: string };
|
|
91
|
+
|
|
92
|
+
function dayLabel(key: string, undated: string): string {
|
|
93
|
+
return key === UNDATED_KEY ? undated : key;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatMinutesCell(minutes: number | undefined): string {
|
|
97
|
+
const m = minutes ?? 0;
|
|
98
|
+
return m > 0 ? formatDuration(m) : "—";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function reportingArchivedExcludedRichText(
|
|
102
|
+
template: string,
|
|
103
|
+
durationMinutes: number,
|
|
104
|
+
): ReactNode {
|
|
105
|
+
const marker = "{duration}";
|
|
106
|
+
const i = template.indexOf(marker);
|
|
107
|
+
if (i < 0) {
|
|
108
|
+
return template;
|
|
109
|
+
}
|
|
110
|
+
const label = formatDuration(durationMinutes);
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
{template.slice(0, i)}
|
|
114
|
+
<strong className="font-semibold text-amber-50 tabular-nums">
|
|
115
|
+
{label}
|
|
116
|
+
</strong>
|
|
117
|
+
{template.slice(i + marker.length)}
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const BAR_TRACK_PX = 100;
|
|
123
|
+
|
|
124
|
+
function MiniBars({
|
|
125
|
+
days,
|
|
126
|
+
values,
|
|
127
|
+
max,
|
|
128
|
+
className,
|
|
129
|
+
undatedLabel: undated,
|
|
130
|
+
valueTitle,
|
|
131
|
+
}: {
|
|
132
|
+
days: string[];
|
|
133
|
+
values: Record<string, number>;
|
|
134
|
+
max: number;
|
|
135
|
+
className: string;
|
|
136
|
+
undatedLabel: string;
|
|
137
|
+
/** If set, used for the bar tooltip instead of the raw numeric value. */
|
|
138
|
+
valueTitle?: (value: number) => string;
|
|
139
|
+
}) {
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex items-end gap-1 overflow-x-auto pb-2">
|
|
142
|
+
{days.map((d) => {
|
|
143
|
+
const v = values[d] ?? 0;
|
|
144
|
+
const px = max > 0 ? Math.round((v / max) * BAR_TRACK_PX) : 0;
|
|
145
|
+
const label = dayLabel(d, undated);
|
|
146
|
+
const tip = valueTitle
|
|
147
|
+
? `${label}: ${valueTitle(v)}`
|
|
148
|
+
: `${label}: ${v}`;
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
key={d}
|
|
152
|
+
className="flex w-8 shrink-0 flex-col items-center gap-1"
|
|
153
|
+
>
|
|
154
|
+
<div
|
|
155
|
+
className="flex h-[100px] w-full items-end justify-center"
|
|
156
|
+
title={tip}
|
|
157
|
+
>
|
|
158
|
+
<div
|
|
159
|
+
className={`w-full rounded-t ${className}`}
|
|
160
|
+
style={{ height: `${Math.max(px, v > 0 ? 3 : 0)}px` }}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
<span
|
|
164
|
+
className="max-w-[2.5rem] truncate text-[0.6rem] text-zinc-500"
|
|
165
|
+
title={label}
|
|
166
|
+
>
|
|
167
|
+
{d === UNDATED_KEY ? "—" : d.slice(5).replace(/^-/, "")}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function StackedTaskBars({
|
|
177
|
+
days,
|
|
178
|
+
done,
|
|
179
|
+
active,
|
|
180
|
+
max,
|
|
181
|
+
undatedLabel: undated,
|
|
182
|
+
}: {
|
|
183
|
+
days: string[];
|
|
184
|
+
done: Record<string, number>;
|
|
185
|
+
active: Record<string, number>;
|
|
186
|
+
max: number;
|
|
187
|
+
undatedLabel: string;
|
|
188
|
+
}) {
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex items-end gap-1 overflow-x-auto pb-2">
|
|
191
|
+
{days.map((d) => {
|
|
192
|
+
const a = active[d] ?? 0;
|
|
193
|
+
const b = done[d] ?? 0;
|
|
194
|
+
const sum = a + b;
|
|
195
|
+
const totalPx = max > 0 ? Math.round((sum / max) * BAR_TRACK_PX) : 0;
|
|
196
|
+
const activePx = sum > 0 ? Math.round((a / sum) * totalPx) : 0;
|
|
197
|
+
const donePx = sum > 0 ? totalPx - activePx : 0;
|
|
198
|
+
const label = dayLabel(d, undated);
|
|
199
|
+
return (
|
|
200
|
+
<div
|
|
201
|
+
key={d}
|
|
202
|
+
className="flex w-8 shrink-0 flex-col items-center gap-1"
|
|
203
|
+
>
|
|
204
|
+
<div
|
|
205
|
+
className="flex h-[100px] w-full flex-col justify-end rounded-t bg-zinc-800/80"
|
|
206
|
+
title={`${label}: ${b} / ${a}`}
|
|
207
|
+
>
|
|
208
|
+
{donePx > 0 ? (
|
|
209
|
+
<div
|
|
210
|
+
className="w-full rounded-t bg-emerald-600/90"
|
|
211
|
+
style={{ height: `${donePx}px` }}
|
|
212
|
+
/>
|
|
213
|
+
) : null}
|
|
214
|
+
{activePx > 0 ? (
|
|
215
|
+
<div
|
|
216
|
+
className="w-full bg-amber-500/90"
|
|
217
|
+
style={{ height: `${activePx}px` }}
|
|
218
|
+
/>
|
|
219
|
+
) : null}
|
|
220
|
+
</div>
|
|
221
|
+
<span
|
|
222
|
+
className="max-w-[2.5rem] truncate text-[0.6rem] text-zinc-500"
|
|
223
|
+
title={label}
|
|
224
|
+
>
|
|
225
|
+
{d === UNDATED_KEY ? "—" : d.slice(5).replace(/^-/, "")}
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
})}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function reportingTagDescriptionLine(
|
|
235
|
+
tagKey: string,
|
|
236
|
+
descriptions: Record<string, string>,
|
|
237
|
+
): string | null {
|
|
238
|
+
if (!tagKey.trim()) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const k = normalizeTagKey(tagKey).toLowerCase();
|
|
242
|
+
const line = (
|
|
243
|
+
descriptions[k] ??
|
|
244
|
+
descriptions[normalizeTagKey(tagKey)] ??
|
|
245
|
+
""
|
|
246
|
+
).trim();
|
|
247
|
+
return line || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function reportingProjectDescriptionLine(
|
|
251
|
+
projectKey: string,
|
|
252
|
+
descriptions: Record<string, string>,
|
|
253
|
+
): string | null {
|
|
254
|
+
if (!projectKey.trim()) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const canon = normalizeProjectKey(projectKey);
|
|
258
|
+
const k = canon.toLowerCase();
|
|
259
|
+
const candidates = [
|
|
260
|
+
descriptions[k],
|
|
261
|
+
descriptions[canon],
|
|
262
|
+
descriptions[projectKey],
|
|
263
|
+
descriptions[projectKey.trim().toLowerCase()],
|
|
264
|
+
];
|
|
265
|
+
for (const c of candidates) {
|
|
266
|
+
const line = (c ?? "").trim();
|
|
267
|
+
if (line) {
|
|
268
|
+
return line;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const [dk, val] of Object.entries(descriptions)) {
|
|
272
|
+
const line = (val ?? "").trim();
|
|
273
|
+
if (!line) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (normalizeProjectKey(dk).toLowerCase() === k) {
|
|
277
|
+
return line;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function ReportingTagNameCell({
|
|
284
|
+
tagKey,
|
|
285
|
+
displayLabel,
|
|
286
|
+
untaggedLabel,
|
|
287
|
+
descriptions,
|
|
288
|
+
className,
|
|
289
|
+
}: {
|
|
290
|
+
tagKey: string;
|
|
291
|
+
displayLabel: string;
|
|
292
|
+
untaggedLabel: string;
|
|
293
|
+
descriptions: Record<string, string>;
|
|
294
|
+
/** Classes de la pastille / libellé principal. */
|
|
295
|
+
className: string;
|
|
296
|
+
}) {
|
|
297
|
+
const desc = reportingTagDescriptionLine(tagKey, descriptions);
|
|
298
|
+
const label = isFallbackTaskTagKey(tagKey) ? untaggedLabel : displayLabel;
|
|
299
|
+
return (
|
|
300
|
+
<td className={`align-top ${className}`}>
|
|
301
|
+
<div>{label}</div>
|
|
302
|
+
{desc ? (
|
|
303
|
+
<p className="mt-1 max-w-[min(22rem,55vw)] whitespace-pre-line text-[0.65rem] font-normal leading-snug text-zinc-500">
|
|
304
|
+
{desc}
|
|
305
|
+
</p>
|
|
306
|
+
) : null}
|
|
307
|
+
</td>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function ReportingProjectNameCell({
|
|
312
|
+
projectKey,
|
|
313
|
+
displayLabel,
|
|
314
|
+
unassignedLabel,
|
|
315
|
+
descriptions,
|
|
316
|
+
className,
|
|
317
|
+
}: {
|
|
318
|
+
projectKey: string;
|
|
319
|
+
displayLabel: string;
|
|
320
|
+
unassignedLabel: string;
|
|
321
|
+
descriptions: Record<string, string>;
|
|
322
|
+
className: string;
|
|
323
|
+
}) {
|
|
324
|
+
const desc = reportingProjectDescriptionLine(projectKey, descriptions);
|
|
325
|
+
const label = projectKey === "" ? unassignedLabel : displayLabel;
|
|
326
|
+
return (
|
|
327
|
+
<td className={`align-top ${className}`}>
|
|
328
|
+
<div>{label}</div>
|
|
329
|
+
{desc ? (
|
|
330
|
+
<p className="mt-1 max-w-[min(22rem,55vw)] whitespace-pre-line text-[0.65rem] font-normal leading-snug text-zinc-500">
|
|
331
|
+
{desc}
|
|
332
|
+
</p>
|
|
333
|
+
) : null}
|
|
334
|
+
</td>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function ReportingFilteredBadge({
|
|
339
|
+
active,
|
|
340
|
+
label,
|
|
341
|
+
titleText,
|
|
342
|
+
}: Readonly<{
|
|
343
|
+
active: boolean;
|
|
344
|
+
label: string;
|
|
345
|
+
titleText: string;
|
|
346
|
+
}>) {
|
|
347
|
+
if (!active) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return (
|
|
351
|
+
<span
|
|
352
|
+
className="inline-flex shrink-0 items-center rounded border border-amber-500/35 bg-amber-950/40 px-1.5 py-px text-[0.65rem] font-semibold uppercase tracking-wide text-amber-200/90"
|
|
353
|
+
title={titleText}
|
|
354
|
+
>
|
|
355
|
+
{label}
|
|
356
|
+
</span>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function ReportingContent() {
|
|
361
|
+
const router = useRouter();
|
|
362
|
+
const pathname = usePathname();
|
|
363
|
+
const searchParams = useSearchParams();
|
|
364
|
+
const dashboardSessionNavId = searchParams.get("session");
|
|
365
|
+
const { payload, error, refresh } = useKronosysPayload();
|
|
366
|
+
const [dateFrom, setDateFrom] = useState("");
|
|
367
|
+
const [dateTo, setDateTo] = useState("");
|
|
368
|
+
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
|
369
|
+
const [workspaceSnapBusy, setWorkspaceSnapBusy] = useState(false);
|
|
370
|
+
const [weekStartsOn, setWeekStartsOn] =
|
|
371
|
+
useState<ReportingWeekStartsOn>("monday");
|
|
372
|
+
/** `-1` = afficher la dernière semaine avec données (pas d’éclair incorrect au chargement). */
|
|
373
|
+
const [chartWeekNavIndex, setChartWeekNavIndex] = useState(-1);
|
|
374
|
+
/** Clés `semaine:::projet` pour les lignes @projet regroupant plusieurs `projet#code`. */
|
|
375
|
+
const [tagWeekRollupOpenKeys, setTagWeekRollupOpenKeys] = useState<
|
|
376
|
+
Set<string>
|
|
377
|
+
>(() => new Set<string>());
|
|
378
|
+
const [tagDayRollupOpenKeys, setTagDayRollupOpenKeys] = useState<Set<string>>(
|
|
379
|
+
() => new Set<string>(),
|
|
380
|
+
);
|
|
381
|
+
const [reportingTourOpen, setReportingTourOpen] = useState(false);
|
|
382
|
+
|
|
383
|
+
const toggleTagWeekRollup = useCallback((key: string) => {
|
|
384
|
+
setTagWeekRollupOpenKeys((prev) => {
|
|
385
|
+
const next = new Set(prev);
|
|
386
|
+
if (next.has(key)) {
|
|
387
|
+
next.delete(key);
|
|
388
|
+
} else {
|
|
389
|
+
next.add(key);
|
|
390
|
+
}
|
|
391
|
+
return next;
|
|
392
|
+
});
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
395
|
+
const toggleTagDayRollup = useCallback((key: string) => {
|
|
396
|
+
setTagDayRollupOpenKeys((prev) => {
|
|
397
|
+
const next = new Set(prev);
|
|
398
|
+
if (next.has(key)) {
|
|
399
|
+
next.delete(key);
|
|
400
|
+
} else {
|
|
401
|
+
next.add(key);
|
|
402
|
+
}
|
|
403
|
+
return next;
|
|
404
|
+
});
|
|
405
|
+
}, []);
|
|
406
|
+
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
setWeekStartsOn(readWeekStartsOnFromStorage());
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
411
|
+
const persistWeekStartsOn = useCallback((v: ReportingWeekStartsOn) => {
|
|
412
|
+
setWeekStartsOn(v);
|
|
413
|
+
writeWeekStartsOnToStorage(v);
|
|
414
|
+
}, []);
|
|
415
|
+
|
|
416
|
+
const refreshWorkspaceSnapshot = useCallback(async () => {
|
|
417
|
+
setWorkspaceSnapBusy(true);
|
|
418
|
+
try {
|
|
419
|
+
await postKronosysAction({ type: "refreshWorkspaceCodeSnapshot" });
|
|
420
|
+
await refresh();
|
|
421
|
+
} catch {
|
|
422
|
+
await refresh();
|
|
423
|
+
} finally {
|
|
424
|
+
setWorkspaceSnapBusy(false);
|
|
425
|
+
}
|
|
426
|
+
}, [refresh]);
|
|
427
|
+
|
|
428
|
+
const postLang = useCallback(
|
|
429
|
+
async (next: Lang) => {
|
|
430
|
+
await postKronosysAction({ type: "setLanguage", lang: next });
|
|
431
|
+
await refresh();
|
|
432
|
+
},
|
|
433
|
+
[refresh],
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const stripReportingTourReplayParam = useCallback(() => {
|
|
437
|
+
if (searchParams.get("tour") !== "replay") {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const next = new URLSearchParams(searchParams.toString());
|
|
441
|
+
next.delete("tour");
|
|
442
|
+
const q = next.toString();
|
|
443
|
+
router.replace(q ? `${pathname}?${q}` : pathname);
|
|
444
|
+
}, [pathname, router, searchParams]);
|
|
445
|
+
|
|
446
|
+
const live = payload?.current as LiveShape | undefined;
|
|
447
|
+
const lang: Lang = live?.language === "fr" ? "fr" : "en";
|
|
448
|
+
const t = reportingStrings(lang);
|
|
449
|
+
const dt = dashboardStrings(lang);
|
|
450
|
+
const handleManualRefresh = useCallback(async () => {
|
|
451
|
+
return await refresh({ routerInvalidate: true });
|
|
452
|
+
}, [refresh]);
|
|
453
|
+
|
|
454
|
+
const reportLocale = lang === "fr" ? "fr-CA" : "en-CA";
|
|
455
|
+
const tagDescriptions = (payload?.tagDescriptions ?? {}) as Record<
|
|
456
|
+
string,
|
|
457
|
+
string
|
|
458
|
+
>;
|
|
459
|
+
const projectDescriptions = (payload?.projectDescriptions ?? {}) as Record<
|
|
460
|
+
string,
|
|
461
|
+
string
|
|
462
|
+
>;
|
|
463
|
+
const tagKeys = useMemo(() => {
|
|
464
|
+
const raw = payload?.knownTags;
|
|
465
|
+
if (!Array.isArray(raw)) {
|
|
466
|
+
return [] as string[];
|
|
467
|
+
}
|
|
468
|
+
return [...new Set(raw.map((x) => String(x)).filter(Boolean))].sort(
|
|
469
|
+
(a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }),
|
|
470
|
+
);
|
|
471
|
+
}, [payload?.knownTags]);
|
|
472
|
+
|
|
473
|
+
const taskDefaultTagBucketEnabled = useMemo(
|
|
474
|
+
() => readTaskDefaultTagBucketEnabled(payload?.cfg),
|
|
475
|
+
[payload?.cfg],
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const reportTimeZone = useMemo(
|
|
479
|
+
() => readDashboardTimeZoneFromCfg(payload?.cfg as Record<string, unknown> | undefined),
|
|
480
|
+
[payload?.cfg]
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const tagSet = useMemo(() => buildTagFilterSet(selectedTags), [selectedTags]);
|
|
484
|
+
|
|
485
|
+
const reportingFiltersActive = useMemo(
|
|
486
|
+
() => Boolean(dateFrom.trim() || dateTo.trim() || selectedTags.length > 0),
|
|
487
|
+
[dateFrom, dateTo, selectedTags],
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const agg = useMemo(() => {
|
|
491
|
+
if (!payload) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const sessions = mergeSessionsFromPayload(payload);
|
|
495
|
+
return aggregateReporting(
|
|
496
|
+
sessions,
|
|
497
|
+
tagSet,
|
|
498
|
+
dateFrom.trim() || null,
|
|
499
|
+
dateTo.trim() || null,
|
|
500
|
+
reportTimeZone,
|
|
501
|
+
taskDefaultTagBucketEnabled,
|
|
502
|
+
);
|
|
503
|
+
}, [payload, tagSet, dateFrom, dateTo, reportTimeZone, taskDefaultTagBucketEnabled]);
|
|
504
|
+
|
|
505
|
+
const archivedExcludedTaskMinutes = useMemo(() => {
|
|
506
|
+
if (!payload) {
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
const sessions = mergeSessionsFromPayload(payload);
|
|
510
|
+
return aggregateArchivedExcludedTaskMinutes(
|
|
511
|
+
sessions,
|
|
512
|
+
tagSet,
|
|
513
|
+
dateFrom.trim() || null,
|
|
514
|
+
dateTo.trim() || null,
|
|
515
|
+
reportTimeZone,
|
|
516
|
+
taskDefaultTagBucketEnabled,
|
|
517
|
+
);
|
|
518
|
+
}, [payload, tagSet, dateFrom, dateTo, reportTimeZone, taskDefaultTagBucketEnabled]);
|
|
519
|
+
|
|
520
|
+
const reportingNonFinal = useMemo(
|
|
521
|
+
() =>
|
|
522
|
+
computeReportingNonFinalFlags(
|
|
523
|
+
payload,
|
|
524
|
+
tagSet,
|
|
525
|
+
dateFrom,
|
|
526
|
+
dateTo,
|
|
527
|
+
agg?.tasksByDayActive,
|
|
528
|
+
reportTimeZone,
|
|
529
|
+
taskDefaultTagBucketEnabled,
|
|
530
|
+
),
|
|
531
|
+
[
|
|
532
|
+
payload,
|
|
533
|
+
tagSet,
|
|
534
|
+
dateFrom,
|
|
535
|
+
dateTo,
|
|
536
|
+
agg?.tasksByDayActive,
|
|
537
|
+
reportTimeZone,
|
|
538
|
+
taskDefaultTagBucketEnabled,
|
|
539
|
+
],
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const dayKeys = useMemo(() => {
|
|
543
|
+
if (!agg) {
|
|
544
|
+
return [] as string[];
|
|
545
|
+
}
|
|
546
|
+
return sortedDayKeys(
|
|
547
|
+
agg.sessionsByDay,
|
|
548
|
+
agg.tasksByDayDone,
|
|
549
|
+
agg.tasksByDayActive,
|
|
550
|
+
agg.taskMinutesByDay,
|
|
551
|
+
agg.sessionCodingMinutesByDay,
|
|
552
|
+
agg.sessionWallClockMinutesByDay,
|
|
553
|
+
);
|
|
554
|
+
}, [agg]);
|
|
555
|
+
|
|
556
|
+
const chartStrictDayKeys = useMemo(
|
|
557
|
+
() => dayKeys.filter((d) => d !== UNDATED_KEY),
|
|
558
|
+
[dayKeys],
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const tagTimeSplit = useMemo((): {
|
|
562
|
+
byDay: ReportingTagTimeDayRow[];
|
|
563
|
+
byWeek: ReportingTagTimeWeekRow[];
|
|
564
|
+
} => {
|
|
565
|
+
if (!payload) {
|
|
566
|
+
return { byDay: [], byWeek: [] };
|
|
567
|
+
}
|
|
568
|
+
const sessions = mergeSessionsFromPayload(payload);
|
|
569
|
+
return aggregateTagTaskMinutesByDayAndWeek(
|
|
570
|
+
sessions,
|
|
571
|
+
tagSet,
|
|
572
|
+
dateFrom.trim() || null,
|
|
573
|
+
dateTo.trim() || null,
|
|
574
|
+
reportTimeZone,
|
|
575
|
+
weekStartsOn,
|
|
576
|
+
t.tagTimeUntagged,
|
|
577
|
+
taskDefaultTagBucketEnabled,
|
|
578
|
+
);
|
|
579
|
+
}, [
|
|
580
|
+
payload,
|
|
581
|
+
tagSet,
|
|
582
|
+
dateFrom,
|
|
583
|
+
dateTo,
|
|
584
|
+
reportTimeZone,
|
|
585
|
+
weekStartsOn,
|
|
586
|
+
t.tagTimeUntagged,
|
|
587
|
+
taskDefaultTagBucketEnabled,
|
|
588
|
+
]);
|
|
589
|
+
|
|
590
|
+
const tagWeekCalendarRows = useMemo(
|
|
591
|
+
() => buildTagWeekCalendarRows(tagTimeSplit.byDay, weekStartsOn),
|
|
592
|
+
[tagTimeSplit.byDay, weekStartsOn],
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const projectTaskMinutesByDay = useMemo(() => {
|
|
596
|
+
if (!payload) {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
const sessions = mergeSessionsFromPayload(payload);
|
|
600
|
+
return aggregateProjectTaskMinutesByDay(
|
|
601
|
+
sessions,
|
|
602
|
+
tagSet,
|
|
603
|
+
dateFrom.trim() || null,
|
|
604
|
+
dateTo.trim() || null,
|
|
605
|
+
reportTimeZone,
|
|
606
|
+
taskDefaultTagBucketEnabled,
|
|
607
|
+
);
|
|
608
|
+
}, [payload, tagSet, dateFrom, dateTo, reportTimeZone, taskDefaultTagBucketEnabled]);
|
|
609
|
+
|
|
610
|
+
const projectWeekCalendarRows = useMemo(
|
|
611
|
+
() => buildProjectWeekCalendarRows(projectTaskMinutesByDay, weekStartsOn),
|
|
612
|
+
[projectTaskMinutesByDay, weekStartsOn],
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const navigableWeekStarts = useMemo(() => {
|
|
616
|
+
if (!agg) {
|
|
617
|
+
return [] as string[];
|
|
618
|
+
}
|
|
619
|
+
const set = new Set<string>();
|
|
620
|
+
for (const d of chartStrictDayKeys) {
|
|
621
|
+
const sw = agg.sessionWallClockMinutesByDay[d] ?? 0;
|
|
622
|
+
const sc = agg.sessionsByDay[d] ?? 0;
|
|
623
|
+
if (sw > 0 || sc > 0) {
|
|
624
|
+
const ws = localWeekStartKeyFromDayKey(d, weekStartsOn);
|
|
625
|
+
if (ws) {
|
|
626
|
+
set.add(ws);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (set.size === 0) {
|
|
631
|
+
for (const r of tagWeekCalendarRows) {
|
|
632
|
+
set.add(r.weekStart);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (set.size === 0) {
|
|
636
|
+
for (const r of projectWeekCalendarRows) {
|
|
637
|
+
set.add(r.weekStart);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return [...set].sort();
|
|
641
|
+
}, [
|
|
642
|
+
agg,
|
|
643
|
+
chartStrictDayKeys,
|
|
644
|
+
weekStartsOn,
|
|
645
|
+
tagWeekCalendarRows,
|
|
646
|
+
projectWeekCalendarRows,
|
|
647
|
+
]);
|
|
648
|
+
|
|
649
|
+
const navigableWeeksKey = navigableWeekStarts.join("|");
|
|
650
|
+
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
setChartWeekNavIndex(-1);
|
|
653
|
+
}, [navigableWeeksKey]);
|
|
654
|
+
|
|
655
|
+
const chartWeekNavIndexSafe =
|
|
656
|
+
navigableWeekStarts.length <= 1
|
|
657
|
+
? 0
|
|
658
|
+
: chartWeekNavIndex < 0
|
|
659
|
+
? navigableWeekStarts.length - 1
|
|
660
|
+
: Math.min(chartWeekNavIndex, navigableWeekStarts.length - 1);
|
|
661
|
+
|
|
662
|
+
const reportingDayKeys = useMemo(() => {
|
|
663
|
+
if (navigableWeekStarts.length > 1) {
|
|
664
|
+
const ws = navigableWeekStarts[chartWeekNavIndexSafe];
|
|
665
|
+
if (!ws) {
|
|
666
|
+
return dayKeys;
|
|
667
|
+
}
|
|
668
|
+
return [0, 1, 2, 3, 4, 5, 6].map((i) => addDaysYmd(ws, i));
|
|
669
|
+
}
|
|
670
|
+
const strict = dayKeys.filter((d) => d !== UNDATED_KEY);
|
|
671
|
+
if (strict.length > 0) {
|
|
672
|
+
return dayKeys;
|
|
673
|
+
}
|
|
674
|
+
if (navigableWeekStarts.length === 1) {
|
|
675
|
+
const ws = navigableWeekStarts[0];
|
|
676
|
+
if (ws) {
|
|
677
|
+
return [0, 1, 2, 3, 4, 5, 6].map((i) => addDaysYmd(ws, i));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return dayKeys;
|
|
681
|
+
}, [navigableWeekStarts, chartWeekNavIndexSafe, dayKeys]);
|
|
682
|
+
|
|
683
|
+
const peak = useMemo(() => {
|
|
684
|
+
if (!agg) {
|
|
685
|
+
return 1;
|
|
686
|
+
}
|
|
687
|
+
let m = 0;
|
|
688
|
+
for (const d of reportingDayKeys) {
|
|
689
|
+
if (d === UNDATED_KEY) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const s = agg.sessionsByDay[d] ?? 0;
|
|
693
|
+
if (s > m) {
|
|
694
|
+
m = s;
|
|
695
|
+
}
|
|
696
|
+
const td = (agg.tasksByDayDone[d] ?? 0) + (agg.tasksByDayActive[d] ?? 0);
|
|
697
|
+
if (td > m) {
|
|
698
|
+
m = td;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return m || 1;
|
|
702
|
+
}, [agg, reportingDayKeys]);
|
|
703
|
+
|
|
704
|
+
const peakTasks = useMemo(() => {
|
|
705
|
+
if (!agg) {
|
|
706
|
+
return 1;
|
|
707
|
+
}
|
|
708
|
+
let m = 0;
|
|
709
|
+
for (const d of reportingDayKeys) {
|
|
710
|
+
if (d === UNDATED_KEY) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const sum = (agg.tasksByDayDone[d] ?? 0) + (agg.tasksByDayActive[d] ?? 0);
|
|
714
|
+
if (sum > m) {
|
|
715
|
+
m = sum;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return m || 1;
|
|
719
|
+
}, [agg, reportingDayKeys]);
|
|
720
|
+
|
|
721
|
+
const peakTaskMinutes = useMemo(() => {
|
|
722
|
+
if (!agg) {
|
|
723
|
+
return 1;
|
|
724
|
+
}
|
|
725
|
+
let m = 0;
|
|
726
|
+
for (const d of reportingDayKeys) {
|
|
727
|
+
if (d === UNDATED_KEY) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const v = agg.taskMinutesByDay[d] ?? 0;
|
|
731
|
+
if (v > m) {
|
|
732
|
+
m = v;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return m || 1;
|
|
736
|
+
}, [agg, reportingDayKeys]);
|
|
737
|
+
|
|
738
|
+
const peakSessionWallMinutes = useMemo(() => {
|
|
739
|
+
if (!agg) {
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
742
|
+
let m = 0;
|
|
743
|
+
for (const d of reportingDayKeys) {
|
|
744
|
+
if (d === UNDATED_KEY) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const v = agg.sessionWallClockMinutesByDay[d] ?? 0;
|
|
748
|
+
if (v > m) {
|
|
749
|
+
m = v;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return m || 1;
|
|
753
|
+
}, [agg, reportingDayKeys]);
|
|
754
|
+
|
|
755
|
+
const tagWeekCalendarRowsVisible = useMemo(() => {
|
|
756
|
+
if (navigableWeekStarts.length <= 1) {
|
|
757
|
+
return tagWeekCalendarRows;
|
|
758
|
+
}
|
|
759
|
+
const ws = navigableWeekStarts[chartWeekNavIndexSafe];
|
|
760
|
+
return tagWeekCalendarRows.filter((r) => r.weekStart === ws);
|
|
761
|
+
}, [tagWeekCalendarRows, navigableWeekStarts, chartWeekNavIndexSafe]);
|
|
762
|
+
|
|
763
|
+
const tagCalendarWeekGroups = useMemo(() => {
|
|
764
|
+
const byWeek = new Map<string, TagWeekCalendarRow[]>();
|
|
765
|
+
for (const r of tagWeekCalendarRowsVisible) {
|
|
766
|
+
const list = byWeek.get(r.weekStart) ?? [];
|
|
767
|
+
list.push(r);
|
|
768
|
+
byWeek.set(r.weekStart, list);
|
|
769
|
+
}
|
|
770
|
+
const weekOrder = [...byWeek.keys()].sort();
|
|
771
|
+
return weekOrder.map((weekStart) => {
|
|
772
|
+
const rows = [...(byWeek.get(weekStart) ?? [])].sort((a, b) => {
|
|
773
|
+
if (isFallbackTaskTagKey(a.tagKey) && !isFallbackTaskTagKey(b.tagKey)) {
|
|
774
|
+
return 1;
|
|
775
|
+
}
|
|
776
|
+
if (!isFallbackTaskTagKey(a.tagKey) && isFallbackTaskTagKey(b.tagKey)) {
|
|
777
|
+
return -1;
|
|
778
|
+
}
|
|
779
|
+
return a.tagKey.localeCompare(b.tagKey);
|
|
780
|
+
});
|
|
781
|
+
return { weekStart, rows };
|
|
782
|
+
});
|
|
783
|
+
}, [tagWeekCalendarRowsVisible]);
|
|
784
|
+
|
|
785
|
+
const projectWeekCalendarRowsVisible = useMemo(() => {
|
|
786
|
+
if (navigableWeekStarts.length <= 1) {
|
|
787
|
+
return projectWeekCalendarRows;
|
|
788
|
+
}
|
|
789
|
+
const ws = navigableWeekStarts[chartWeekNavIndexSafe];
|
|
790
|
+
return projectWeekCalendarRows.filter((r) => r.weekStart === ws);
|
|
791
|
+
}, [projectWeekCalendarRows, navigableWeekStarts, chartWeekNavIndexSafe]);
|
|
792
|
+
|
|
793
|
+
const projectCalendarWeekGroups = useMemo(() => {
|
|
794
|
+
const byWeek = new Map<string, ProjectWeekCalendarRow[]>();
|
|
795
|
+
for (const r of projectWeekCalendarRowsVisible) {
|
|
796
|
+
const list = byWeek.get(r.weekStart) ?? [];
|
|
797
|
+
list.push(r);
|
|
798
|
+
byWeek.set(r.weekStart, list);
|
|
799
|
+
}
|
|
800
|
+
const weekOrder = [...byWeek.keys()].sort();
|
|
801
|
+
return weekOrder.map((weekStart) => {
|
|
802
|
+
const rows = [...(byWeek.get(weekStart) ?? [])].sort((a, b) => {
|
|
803
|
+
if (a.projectKey === "" && b.projectKey !== "") {
|
|
804
|
+
return 1;
|
|
805
|
+
}
|
|
806
|
+
if (b.projectKey === "" && a.projectKey !== "") {
|
|
807
|
+
return -1;
|
|
808
|
+
}
|
|
809
|
+
return a.projectKey.localeCompare(b.projectKey);
|
|
810
|
+
});
|
|
811
|
+
return { weekStart, rows };
|
|
812
|
+
});
|
|
813
|
+
}, [projectWeekCalendarRowsVisible]);
|
|
814
|
+
|
|
815
|
+
const toggleTag = (tag: string) => {
|
|
816
|
+
const key = normalizeTagKey(tag).toLowerCase();
|
|
817
|
+
setSelectedTags((prev) => {
|
|
818
|
+
const has = prev.some((p) => normalizeTagKey(p).toLowerCase() === key);
|
|
819
|
+
if (has) {
|
|
820
|
+
return prev.filter((p) => normalizeTagKey(p).toLowerCase() !== key);
|
|
821
|
+
}
|
|
822
|
+
return [...prev, tag];
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const headerApiError =
|
|
827
|
+
lang === "fr"
|
|
828
|
+
? "Impossible de joindre l’API locale Kronosys (127.0.0.1:5566 par défaut). Vérifiez que le serveur tourne."
|
|
829
|
+
: "Cannot reach the local Kronosys API (default 127.0.0.1:5566). Make sure the server is running.";
|
|
830
|
+
|
|
831
|
+
const trackCodeMetrics = trackCodeMetricsFromCfg(
|
|
832
|
+
payload ? (payload.cfg as Record<string, unknown> | undefined) : undefined,
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
const hasReportingChartData =
|
|
836
|
+
!!agg &&
|
|
837
|
+
(dayKeys.length > 0 ||
|
|
838
|
+
navigableWeekStarts.length > 1 ||
|
|
839
|
+
tagWeekCalendarRows.length > 0 ||
|
|
840
|
+
projectWeekCalendarRows.length > 0);
|
|
841
|
+
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
if (!payload || !agg) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (searchParams.get("tour") === "replay") {
|
|
847
|
+
setReportingTourOpen(true);
|
|
848
|
+
stripReportingTourReplayParam();
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (!isReportingTourCompleted()) {
|
|
852
|
+
setReportingTourOpen(true);
|
|
853
|
+
}
|
|
854
|
+
}, [payload, agg, searchParams, stripReportingTourReplayParam]);
|
|
855
|
+
|
|
856
|
+
const reportingTocEntries = (() => {
|
|
857
|
+
if (!payload) {
|
|
858
|
+
return [] as { id: string; label: string }[];
|
|
859
|
+
}
|
|
860
|
+
const rows: { id: string; label: string }[] = [
|
|
861
|
+
{ id: "report-filters", label: t.filtersTitle },
|
|
862
|
+
];
|
|
863
|
+
if (trackCodeMetrics) {
|
|
864
|
+
rows.push({
|
|
865
|
+
id: "report-workspace-snapshot",
|
|
866
|
+
label: t.workspaceSnapshotTitle,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (!agg) {
|
|
870
|
+
return rows;
|
|
871
|
+
}
|
|
872
|
+
rows.push({ id: "report-summary-kpis", label: t.tocSummaryKpis });
|
|
873
|
+
if (trackCodeMetrics) {
|
|
874
|
+
rows.push({ id: "report-loc-metrics", label: t.tocLocSection });
|
|
875
|
+
}
|
|
876
|
+
if (!hasReportingChartData) {
|
|
877
|
+
return rows;
|
|
878
|
+
}
|
|
879
|
+
if (navigableWeekStarts.length > 1) {
|
|
880
|
+
rows.push({ id: "report-week-nav", label: t.tocWeekNav });
|
|
881
|
+
}
|
|
882
|
+
rows.push(
|
|
883
|
+
{ id: "report-chart-sessions", label: t.chartSessionsPerDay },
|
|
884
|
+
{ id: "report-chart-tasks", label: t.chartTasksByStatusPerDay },
|
|
885
|
+
{ id: "report-chart-task-time", label: t.chartTaskTimePerDay },
|
|
886
|
+
{ id: "report-chart-session-wall", label: t.chartSessionWallPerDay },
|
|
887
|
+
{ id: "report-daily-table", label: t.tocDailyTable },
|
|
888
|
+
{ id: "report-tag-time", label: t.tocTagTimeSection },
|
|
889
|
+
{ id: "report-projects", label: t.projectSectionTitle },
|
|
890
|
+
);
|
|
891
|
+
return rows;
|
|
892
|
+
})();
|
|
893
|
+
|
|
894
|
+
return (
|
|
895
|
+
<div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
|
|
896
|
+
<header className={appShellHeaderClassName}>
|
|
897
|
+
<div className={appShellHeaderToolRowClassName}>
|
|
898
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
899
|
+
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
900
|
+
<Link
|
|
901
|
+
href={withDashboardSessionParam("/", dashboardSessionNavId)}
|
|
902
|
+
className="text-xl font-semibold tracking-tight text-zinc-900 hover:text-violet-700 dark:text-zinc-100 dark:hover:text-violet-300"
|
|
903
|
+
>
|
|
904
|
+
Kronosys
|
|
905
|
+
</Link>
|
|
906
|
+
<span className="text-zinc-400 dark:text-zinc-600">/</span>
|
|
907
|
+
<span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">
|
|
908
|
+
{t.title}
|
|
909
|
+
</span>
|
|
910
|
+
</div>
|
|
911
|
+
<p className="flex flex-wrap items-center gap-x-2 text-xs font-medium leading-snug text-zinc-500 dark:text-zinc-400">
|
|
912
|
+
<span>{dt.brandTagline}</span>
|
|
913
|
+
<span className="text-zinc-400/70 dark:text-zinc-600" aria-hidden>
|
|
914
|
+
·
|
|
915
|
+
</span>
|
|
916
|
+
<AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
|
|
917
|
+
</p>
|
|
918
|
+
</div>
|
|
919
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
920
|
+
<AppShellRouteNav
|
|
921
|
+
current="reporting"
|
|
922
|
+
labels={{
|
|
923
|
+
dashboard: t.dashboard,
|
|
924
|
+
reporting: t.reporting,
|
|
925
|
+
settings: t.settings,
|
|
926
|
+
guide: t.guide,
|
|
927
|
+
}}
|
|
928
|
+
navAriaLabel={dt.appShellRouteNavAria}
|
|
929
|
+
dashboardSessionId={dashboardSessionNavId}
|
|
930
|
+
/>
|
|
931
|
+
<ThemeToggle lang={lang} />
|
|
932
|
+
<PageRefreshButton
|
|
933
|
+
title={dt.pageRefreshTitle}
|
|
934
|
+
ariaLabel={dt.pageRefreshAriaLabel}
|
|
935
|
+
inlineMessages={{
|
|
936
|
+
loading: dt.pageRefreshProgressLabel,
|
|
937
|
+
success: dt.pageRefreshDoneToast,
|
|
938
|
+
error: dt.pageRefreshFailedToast,
|
|
939
|
+
}}
|
|
940
|
+
onRefresh={handleManualRefresh}
|
|
941
|
+
/>
|
|
942
|
+
<LanguageMenu
|
|
943
|
+
lang={lang}
|
|
944
|
+
labelEn="English"
|
|
945
|
+
labelFr="Français"
|
|
946
|
+
menuHeading={lang === "fr" ? "Langue" : "Language"}
|
|
947
|
+
triggerAriaLabel={
|
|
948
|
+
lang === "fr" ? "Langue de l’interface" : "Interface language"
|
|
949
|
+
}
|
|
950
|
+
onSelect={(next) => void postLang(next)}
|
|
951
|
+
/>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
</header>
|
|
955
|
+
|
|
956
|
+
<div className="w-full px-5 py-8 sm:px-8 lg:px-10 xl:px-12">
|
|
957
|
+
<div
|
|
958
|
+
id="reporting-tour-anchor-intro"
|
|
959
|
+
className="mb-8 scroll-mt-28 space-y-3 text-sm text-zinc-600 dark:text-zinc-500"
|
|
960
|
+
>
|
|
961
|
+
<p>{t.subtitle}</p>
|
|
962
|
+
<p className="text-zinc-500 dark:text-zinc-400">
|
|
963
|
+
{t.archivedSessionsReportingNote}
|
|
964
|
+
</p>
|
|
965
|
+
{payload ? (
|
|
966
|
+
<div className="pt-1">
|
|
967
|
+
<button
|
|
968
|
+
type="button"
|
|
969
|
+
className="rounded-lg border border-zinc-500/50 bg-zinc-500/10 px-3 py-2 text-sm font-medium text-zinc-800 transition hover:border-zinc-500/75 hover:bg-zinc-500/18 dark:border-zinc-400/45 dark:bg-zinc-600/20 dark:text-zinc-100 dark:hover:border-zinc-400/60 dark:hover:bg-zinc-600/30"
|
|
970
|
+
onClick={() => {
|
|
971
|
+
resetReportingTour();
|
|
972
|
+
setReportingTourOpen(true);
|
|
973
|
+
}}
|
|
974
|
+
>
|
|
975
|
+
{dt.tourUndismissBtn}
|
|
976
|
+
</button>
|
|
977
|
+
</div>
|
|
978
|
+
) : null}
|
|
979
|
+
</div>
|
|
980
|
+
|
|
981
|
+
{error && (
|
|
982
|
+
<div
|
|
983
|
+
className="mb-8 rounded-lg border border-red-900/60 bg-red-950/40 px-4 py-3 text-sm text-red-100"
|
|
984
|
+
role="alert"
|
|
985
|
+
>
|
|
986
|
+
<strong className="block text-red-200">API</strong>
|
|
987
|
+
{headerApiError}
|
|
988
|
+
<pre className="mt-2 overflow-x-auto text-xs text-red-200/80">
|
|
989
|
+
{error}
|
|
990
|
+
</pre>
|
|
991
|
+
</div>
|
|
992
|
+
)}
|
|
993
|
+
|
|
994
|
+
{!payload && !error && (
|
|
995
|
+
<p className="text-sm text-zinc-500">{t.loading}</p>
|
|
996
|
+
)}
|
|
997
|
+
|
|
998
|
+
{payload && (
|
|
999
|
+
<div
|
|
1000
|
+
id="reporting-tour-anchor-toc-layout"
|
|
1001
|
+
className="lg:flex lg:items-start lg:gap-8 xl:gap-10"
|
|
1002
|
+
>
|
|
1003
|
+
<div className="min-w-0 flex-1">
|
|
1004
|
+
<ReportingPageTocMobile
|
|
1005
|
+
title={t.tocTitle}
|
|
1006
|
+
ariaLabel={t.tocNavAria}
|
|
1007
|
+
entries={reportingTocEntries}
|
|
1008
|
+
/>
|
|
1009
|
+
<section
|
|
1010
|
+
id="report-filters"
|
|
1011
|
+
className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
1012
|
+
>
|
|
1013
|
+
<div className="flex flex-wrap items-center gap-2 gap-y-2">
|
|
1014
|
+
<h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
|
|
1015
|
+
{t.filtersTitle}
|
|
1016
|
+
</h2>
|
|
1017
|
+
<InlineMetricHelpTrigger
|
|
1018
|
+
ariaLabel={t.filtersHelpAriaLabel}
|
|
1019
|
+
body={t.filtersHelpBody}
|
|
1020
|
+
preserveLineBreaks
|
|
1021
|
+
panelClassName="w-[min(calc(100vw-2rem),24rem)]"
|
|
1022
|
+
/>
|
|
1023
|
+
{(reportingNonFinal.sessionsNonFinal ||
|
|
1024
|
+
reportingNonFinal.projectsNonFinal ||
|
|
1025
|
+
reportingNonFinal.tagsNonFinal) && (
|
|
1026
|
+
<div className="flex min-w-0 max-w-full flex-wrap items-center gap-x-1.5 gap-y-1 border-l border-zinc-600/70 pl-2.5 dark:border-zinc-500/50">
|
|
1027
|
+
<span className="shrink-0 text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
|
1028
|
+
{t.reportingNonFinalLegend}
|
|
1029
|
+
</span>
|
|
1030
|
+
{reportingNonFinal.sessionsNonFinal ? (
|
|
1031
|
+
<span className="rounded border border-emerald-600/50 bg-emerald-950/40 px-1.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/45 dark:bg-emerald-950/50 dark:text-emerald-400">
|
|
1032
|
+
{t.reportingNonFinalSessionsBadge}
|
|
1033
|
+
</span>
|
|
1034
|
+
) : null}
|
|
1035
|
+
{reportingNonFinal.projectsNonFinal ? (
|
|
1036
|
+
<span className="rounded border border-emerald-600/50 bg-emerald-950/40 px-1.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/45 dark:bg-emerald-950/50 dark:text-emerald-400">
|
|
1037
|
+
{t.reportingNonFinalProjectsBadge}
|
|
1038
|
+
</span>
|
|
1039
|
+
) : null}
|
|
1040
|
+
{reportingNonFinal.tagsNonFinal ? (
|
|
1041
|
+
<span className="rounded border border-emerald-600/50 bg-emerald-950/40 px-1.5 py-0.5 text-[0.65rem] font-semibold uppercase tracking-wide text-emerald-700 dark:border-emerald-500/45 dark:bg-emerald-950/50 dark:text-emerald-400">
|
|
1042
|
+
{t.reportingNonFinalTagsBadge}
|
|
1043
|
+
</span>
|
|
1044
|
+
) : null}
|
|
1045
|
+
</div>
|
|
1046
|
+
)}
|
|
1047
|
+
</div>
|
|
1048
|
+
<div className="mt-4 flex flex-wrap items-end gap-4">
|
|
1049
|
+
<label className="flex flex-col gap-1 text-xs text-zinc-500">
|
|
1050
|
+
{t.dateFrom}
|
|
1051
|
+
<input
|
|
1052
|
+
type="date"
|
|
1053
|
+
value={dateFrom}
|
|
1054
|
+
onChange={(e) => setDateFrom(e.target.value)}
|
|
1055
|
+
className="rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-100"
|
|
1056
|
+
/>
|
|
1057
|
+
</label>
|
|
1058
|
+
<label className="flex flex-col gap-1 text-xs text-zinc-500">
|
|
1059
|
+
{t.dateTo}
|
|
1060
|
+
<input
|
|
1061
|
+
type="date"
|
|
1062
|
+
value={dateTo}
|
|
1063
|
+
onChange={(e) => setDateTo(e.target.value)}
|
|
1064
|
+
className="rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-100"
|
|
1065
|
+
/>
|
|
1066
|
+
</label>
|
|
1067
|
+
<button
|
|
1068
|
+
type="button"
|
|
1069
|
+
className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800"
|
|
1070
|
+
onClick={() => {
|
|
1071
|
+
const p = reportingPresetDay();
|
|
1072
|
+
setDateFrom(p.from);
|
|
1073
|
+
setDateTo(p.to);
|
|
1074
|
+
}}
|
|
1075
|
+
>
|
|
1076
|
+
{t.today}
|
|
1077
|
+
</button>
|
|
1078
|
+
<button
|
|
1079
|
+
type="button"
|
|
1080
|
+
className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800"
|
|
1081
|
+
onClick={() => {
|
|
1082
|
+
setDateFrom("");
|
|
1083
|
+
setDateTo("");
|
|
1084
|
+
}}
|
|
1085
|
+
>
|
|
1086
|
+
{t.clearDates}
|
|
1087
|
+
</button>
|
|
1088
|
+
</div>
|
|
1089
|
+
<div
|
|
1090
|
+
className="mt-4 flex flex-col gap-2"
|
|
1091
|
+
role="group"
|
|
1092
|
+
aria-label={t.presetPeriodLabel}
|
|
1093
|
+
>
|
|
1094
|
+
<span className="text-xs font-medium text-zinc-500">
|
|
1095
|
+
{t.presetPeriodLabel}
|
|
1096
|
+
</span>
|
|
1097
|
+
<div className="flex flex-wrap gap-2">
|
|
1098
|
+
<button
|
|
1099
|
+
type="button"
|
|
1100
|
+
className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
|
|
1101
|
+
onClick={() => {
|
|
1102
|
+
const p = reportingPresetDay();
|
|
1103
|
+
setDateFrom(p.from);
|
|
1104
|
+
setDateTo(p.to);
|
|
1105
|
+
}}
|
|
1106
|
+
>
|
|
1107
|
+
{t.presetDay}
|
|
1108
|
+
</button>
|
|
1109
|
+
<button
|
|
1110
|
+
type="button"
|
|
1111
|
+
className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
|
|
1112
|
+
onClick={() => {
|
|
1113
|
+
const p = reportingPresetWeek();
|
|
1114
|
+
setDateFrom(p.from);
|
|
1115
|
+
setDateTo(p.to);
|
|
1116
|
+
}}
|
|
1117
|
+
>
|
|
1118
|
+
{t.presetWeek}
|
|
1119
|
+
</button>
|
|
1120
|
+
<button
|
|
1121
|
+
type="button"
|
|
1122
|
+
className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
|
|
1123
|
+
onClick={() => {
|
|
1124
|
+
const p = reportingPresetMonth();
|
|
1125
|
+
setDateFrom(p.from);
|
|
1126
|
+
setDateTo(p.to);
|
|
1127
|
+
}}
|
|
1128
|
+
>
|
|
1129
|
+
{t.presetMonth}
|
|
1130
|
+
</button>
|
|
1131
|
+
<button
|
|
1132
|
+
type="button"
|
|
1133
|
+
className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
|
|
1134
|
+
onClick={() => {
|
|
1135
|
+
const p = reportingPresetYear();
|
|
1136
|
+
setDateFrom(p.from);
|
|
1137
|
+
setDateTo(p.to);
|
|
1138
|
+
}}
|
|
1139
|
+
>
|
|
1140
|
+
{t.presetYear}
|
|
1141
|
+
</button>
|
|
1142
|
+
</div>
|
|
1143
|
+
<p className="text-[0.65rem] leading-snug text-zinc-600">
|
|
1144
|
+
{t.presetPeriodHint}
|
|
1145
|
+
</p>
|
|
1146
|
+
</div>
|
|
1147
|
+
<p className="mt-3 text-xs text-zinc-500">{t.tagsHint}</p>
|
|
1148
|
+
<div className="mt-2">
|
|
1149
|
+
<span className="text-xs font-medium uppercase text-zinc-500">
|
|
1150
|
+
{t.tagsLabel}
|
|
1151
|
+
</span>
|
|
1152
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
1153
|
+
{tagKeys.length === 0 ? (
|
|
1154
|
+
<span className="text-sm text-zinc-500">—</span>
|
|
1155
|
+
) : (
|
|
1156
|
+
tagKeys.map((tag) => {
|
|
1157
|
+
const on = selectedTags.some(
|
|
1158
|
+
(p) =>
|
|
1159
|
+
normalizeTagKey(p).toLowerCase() ===
|
|
1160
|
+
normalizeTagKey(tag).toLowerCase(),
|
|
1161
|
+
);
|
|
1162
|
+
return (
|
|
1163
|
+
<button
|
|
1164
|
+
key={tag}
|
|
1165
|
+
type="button"
|
|
1166
|
+
onClick={() => toggleTag(tag)}
|
|
1167
|
+
className={`rounded-full border px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
1168
|
+
on
|
|
1169
|
+
? "border-violet-500 bg-violet-600/30 text-violet-100"
|
|
1170
|
+
: "border-zinc-600 bg-zinc-950 text-zinc-400 hover:border-zinc-500"
|
|
1171
|
+
}`}
|
|
1172
|
+
>
|
|
1173
|
+
{formatTagDisplay(tag)}
|
|
1174
|
+
</button>
|
|
1175
|
+
);
|
|
1176
|
+
})
|
|
1177
|
+
)}
|
|
1178
|
+
</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
<div className="mt-4 flex flex-wrap items-center gap-3">
|
|
1181
|
+
<button
|
|
1182
|
+
type="button"
|
|
1183
|
+
disabled={!reportingFiltersActive}
|
|
1184
|
+
className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-40"
|
|
1185
|
+
onClick={() => {
|
|
1186
|
+
setDateFrom("");
|
|
1187
|
+
setDateTo("");
|
|
1188
|
+
setSelectedTags([]);
|
|
1189
|
+
}}
|
|
1190
|
+
>
|
|
1191
|
+
{t.resetAllFilters}
|
|
1192
|
+
</button>
|
|
1193
|
+
</div>
|
|
1194
|
+
</section>
|
|
1195
|
+
|
|
1196
|
+
{trackCodeMetrics ? (
|
|
1197
|
+
<section
|
|
1198
|
+
id="report-workspace-snapshot"
|
|
1199
|
+
className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
1200
|
+
>
|
|
1201
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1202
|
+
<div className="flex min-w-0 max-w-full items-center gap-1">
|
|
1203
|
+
<h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
|
|
1204
|
+
{t.workspaceSnapshotTitle}
|
|
1205
|
+
</h2>
|
|
1206
|
+
<InlineMetricHelpTrigger
|
|
1207
|
+
ariaLabel={t.metricHelpWorkspaceTitleAria}
|
|
1208
|
+
body={t.metricHelpWorkspaceTitleBody}
|
|
1209
|
+
/>
|
|
1210
|
+
</div>
|
|
1211
|
+
<button
|
|
1212
|
+
type="button"
|
|
1213
|
+
disabled={workspaceSnapBusy}
|
|
1214
|
+
className="shrink-0 rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-50"
|
|
1215
|
+
onClick={() => void refreshWorkspaceSnapshot()}
|
|
1216
|
+
>
|
|
1217
|
+
{workspaceSnapBusy
|
|
1218
|
+
? t.workspaceSnapshotRefreshing
|
|
1219
|
+
: t.workspaceSnapshotRefresh}
|
|
1220
|
+
</button>
|
|
1221
|
+
</div>
|
|
1222
|
+
<p className="mt-2 text-xs text-zinc-500">
|
|
1223
|
+
{t.workspaceSnapshotIntro}
|
|
1224
|
+
</p>
|
|
1225
|
+
{(() => {
|
|
1226
|
+
const ws = payload.workspaceCodeSnapshot as
|
|
1227
|
+
| WorkspaceCodeSnapshotPayload
|
|
1228
|
+
| undefined;
|
|
1229
|
+
if (!ws) {
|
|
1230
|
+
return (
|
|
1231
|
+
<p className="mt-4 text-sm text-zinc-500">
|
|
1232
|
+
{t.workspaceSnapshotRefreshHint}
|
|
1233
|
+
</p>
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (ws.ok) {
|
|
1237
|
+
return (
|
|
1238
|
+
<>
|
|
1239
|
+
<p className="mt-2 text-xs text-zinc-500">
|
|
1240
|
+
{ws.source === "git"
|
|
1241
|
+
? t.workspaceSnapshotSourceGit
|
|
1242
|
+
: t.workspaceSnapshotSourceWalk}
|
|
1243
|
+
</p>
|
|
1244
|
+
<div className="mt-3 flex flex-wrap gap-6 text-sm">
|
|
1245
|
+
<div>
|
|
1246
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1247
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1248
|
+
{t.workspaceSnapshotTotalLines}
|
|
1249
|
+
</span>
|
|
1250
|
+
<InlineMetricHelpTrigger
|
|
1251
|
+
ariaLabel={t.metricHelpWsTotalLinesAria}
|
|
1252
|
+
body={t.metricHelpWsTotalLinesBody}
|
|
1253
|
+
/>
|
|
1254
|
+
</div>
|
|
1255
|
+
<div className="tabular-nums text-lg font-semibold text-zinc-100">
|
|
1256
|
+
{ws.totalLines}
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
<div>
|
|
1260
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1261
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1262
|
+
{t.workspaceSnapshotFileCount}
|
|
1263
|
+
</span>
|
|
1264
|
+
<InlineMetricHelpTrigger
|
|
1265
|
+
ariaLabel={t.metricHelpWsFileCountAria}
|
|
1266
|
+
body={t.metricHelpWsFileCountBody}
|
|
1267
|
+
/>
|
|
1268
|
+
</div>
|
|
1269
|
+
<div className="tabular-nums text-lg font-semibold text-zinc-100">
|
|
1270
|
+
{ws.fileCount}
|
|
1271
|
+
</div>
|
|
1272
|
+
</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
{ws.byLanguage.length === 0 ? (
|
|
1275
|
+
<p className="mt-4 text-sm text-zinc-500">—</p>
|
|
1276
|
+
) : (
|
|
1277
|
+
<div className="mt-4 overflow-x-auto">
|
|
1278
|
+
<table className="w-full min-w-[22rem] text-left text-sm">
|
|
1279
|
+
<thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
|
|
1280
|
+
<tr>
|
|
1281
|
+
<th className="py-2 pr-3 font-medium">
|
|
1282
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1283
|
+
<span>
|
|
1284
|
+
{t.workspaceSnapshotColLang}
|
|
1285
|
+
</span>
|
|
1286
|
+
<InlineMetricHelpTrigger
|
|
1287
|
+
ariaLabel={t.metricHelpWsColLangAria}
|
|
1288
|
+
body={t.metricHelpWsColLangBody}
|
|
1289
|
+
/>
|
|
1290
|
+
</div>
|
|
1291
|
+
</th>
|
|
1292
|
+
<th className="py-2 pr-3 font-medium tabular-nums">
|
|
1293
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1294
|
+
<span>
|
|
1295
|
+
{t.workspaceSnapshotColLines}
|
|
1296
|
+
</span>
|
|
1297
|
+
<InlineMetricHelpTrigger
|
|
1298
|
+
ariaLabel={t.metricHelpWsColLinesAria}
|
|
1299
|
+
body={t.metricHelpWsColLinesBody}
|
|
1300
|
+
/>
|
|
1301
|
+
</div>
|
|
1302
|
+
</th>
|
|
1303
|
+
<th className="py-2 font-medium">
|
|
1304
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1305
|
+
<span>
|
|
1306
|
+
{t.workspaceSnapshotColPercent}
|
|
1307
|
+
</span>
|
|
1308
|
+
<InlineMetricHelpTrigger
|
|
1309
|
+
align="end"
|
|
1310
|
+
ariaLabel={
|
|
1311
|
+
t.metricHelpWsColPercentAria
|
|
1312
|
+
}
|
|
1313
|
+
body={t.metricHelpWsColPercentBody}
|
|
1314
|
+
/>
|
|
1315
|
+
</div>
|
|
1316
|
+
</th>
|
|
1317
|
+
</tr>
|
|
1318
|
+
</thead>
|
|
1319
|
+
<tbody>
|
|
1320
|
+
{ws.byLanguage.map((row) => (
|
|
1321
|
+
<tr
|
|
1322
|
+
key={row.languageId}
|
|
1323
|
+
className="border-b border-zinc-800/80 last:border-0"
|
|
1324
|
+
>
|
|
1325
|
+
<td className="py-2 pr-3 font-mono text-xs text-zinc-400">
|
|
1326
|
+
{row.languageId}
|
|
1327
|
+
</td>
|
|
1328
|
+
<td className="py-2 pr-3 tabular-nums text-zinc-200">
|
|
1329
|
+
{row.lines}
|
|
1330
|
+
</td>
|
|
1331
|
+
<td className="py-2">
|
|
1332
|
+
<div className="flex items-center gap-2">
|
|
1333
|
+
<div className="h-2 min-w-[4rem] flex-1 overflow-hidden rounded bg-zinc-800">
|
|
1334
|
+
<div
|
|
1335
|
+
className="h-full rounded bg-sky-600/85"
|
|
1336
|
+
style={{
|
|
1337
|
+
width: `${Math.min(
|
|
1338
|
+
100,
|
|
1339
|
+
row.percent,
|
|
1340
|
+
)}%`,
|
|
1341
|
+
}}
|
|
1342
|
+
/>
|
|
1343
|
+
</div>
|
|
1344
|
+
<span className="w-14 shrink-0 tabular-nums text-zinc-400">
|
|
1345
|
+
{row.percent.toFixed(1)}%
|
|
1346
|
+
</span>
|
|
1347
|
+
</div>
|
|
1348
|
+
</td>
|
|
1349
|
+
</tr>
|
|
1350
|
+
))}
|
|
1351
|
+
</tbody>
|
|
1352
|
+
</table>
|
|
1353
|
+
</div>
|
|
1354
|
+
)}
|
|
1355
|
+
</>
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
return (
|
|
1359
|
+
<p className="mt-4 text-sm text-amber-200/90">
|
|
1360
|
+
{ws.reason === "no_workspace"
|
|
1361
|
+
? t.workspaceSnapshotNoWorkspace
|
|
1362
|
+
: ws.reason === "empty"
|
|
1363
|
+
? ws.message || t.workspaceSnapshotEmpty
|
|
1364
|
+
: ws.message || t.workspaceSnapshotError}
|
|
1365
|
+
</p>
|
|
1366
|
+
);
|
|
1367
|
+
})()}
|
|
1368
|
+
{archivedExcludedTaskMinutes > 1e-9 ? (
|
|
1369
|
+
<div
|
|
1370
|
+
role="status"
|
|
1371
|
+
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"
|
|
1372
|
+
>
|
|
1373
|
+
{reportingArchivedExcludedRichText(
|
|
1374
|
+
t.reportingArchivedExcludedAside,
|
|
1375
|
+
archivedExcludedTaskMinutes
|
|
1376
|
+
)}
|
|
1377
|
+
</div>
|
|
1378
|
+
) : null}
|
|
1379
|
+
</section>
|
|
1380
|
+
) : null}
|
|
1381
|
+
|
|
1382
|
+
{agg && (
|
|
1383
|
+
<>
|
|
1384
|
+
<section
|
|
1385
|
+
id="report-summary-kpis"
|
|
1386
|
+
className="mb-10 scroll-mt-28 flex flex-col gap-3"
|
|
1387
|
+
>
|
|
1388
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1389
|
+
<h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
|
|
1390
|
+
{t.tocSummaryKpis}
|
|
1391
|
+
</h2>
|
|
1392
|
+
<ReportingFilteredBadge
|
|
1393
|
+
active={reportingFiltersActive}
|
|
1394
|
+
label={t.sectionFilteredBadge}
|
|
1395
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
1396
|
+
/>
|
|
1397
|
+
</div>
|
|
1398
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
1399
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1400
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1401
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1402
|
+
{t.summarySessionsInRange}
|
|
1403
|
+
</span>
|
|
1404
|
+
<InlineMetricHelpTrigger
|
|
1405
|
+
ariaLabel={t.metricHelpSessionsAria}
|
|
1406
|
+
body={t.metricHelpSessionsBody}
|
|
1407
|
+
/>
|
|
1408
|
+
</div>
|
|
1409
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1410
|
+
{agg.sessionCountContributing}
|
|
1411
|
+
</div>
|
|
1412
|
+
</div>
|
|
1413
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1414
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1415
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1416
|
+
{t.summaryTaskEvents}
|
|
1417
|
+
</span>
|
|
1418
|
+
<InlineMetricHelpTrigger
|
|
1419
|
+
ariaLabel={t.metricHelpTaskRowsAria}
|
|
1420
|
+
body={t.metricHelpTaskRowsBody}
|
|
1421
|
+
/>
|
|
1422
|
+
</div>
|
|
1423
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1424
|
+
{agg.taskCountContributing}
|
|
1425
|
+
</div>
|
|
1426
|
+
</div>
|
|
1427
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1428
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1429
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1430
|
+
{t.summaryKronoFocusCompleted}
|
|
1431
|
+
</span>
|
|
1432
|
+
<InlineMetricHelpTrigger
|
|
1433
|
+
align="end"
|
|
1434
|
+
ariaLabel={t.metricHelpKronoFocusCompletedAria}
|
|
1435
|
+
body={t.metricHelpKronoFocusCompletedBody}
|
|
1436
|
+
/>
|
|
1437
|
+
</div>
|
|
1438
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1439
|
+
{agg.kronoFocusSessionsCompleted}
|
|
1440
|
+
</div>
|
|
1441
|
+
</div>
|
|
1442
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1443
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1444
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1445
|
+
{t.summaryKronoFocusTasksUsed}
|
|
1446
|
+
</span>
|
|
1447
|
+
<InlineMetricHelpTrigger
|
|
1448
|
+
ariaLabel={t.metricHelpKronoFocusTasksUsedAria}
|
|
1449
|
+
body={t.metricHelpKronoFocusTasksUsedBody}
|
|
1450
|
+
/>
|
|
1451
|
+
</div>
|
|
1452
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1453
|
+
{agg.kronoFocusTasksUsedCount}
|
|
1454
|
+
</div>
|
|
1455
|
+
</div>
|
|
1456
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1457
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1458
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1459
|
+
{t.summaryKronoFocusCycles}
|
|
1460
|
+
</span>
|
|
1461
|
+
<InlineMetricHelpTrigger
|
|
1462
|
+
ariaLabel={t.metricHelpKronoFocusCyclesAria}
|
|
1463
|
+
body={t.metricHelpKronoFocusCyclesBody}
|
|
1464
|
+
/>
|
|
1465
|
+
</div>
|
|
1466
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1467
|
+
{agg.kronoFocusTaskCyclesSum}
|
|
1468
|
+
</div>
|
|
1469
|
+
</div>
|
|
1470
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1471
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1472
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1473
|
+
{t.summaryTaskTimeRecorded}
|
|
1474
|
+
</span>
|
|
1475
|
+
<InlineMetricHelpTrigger
|
|
1476
|
+
align="end"
|
|
1477
|
+
ariaLabel={t.metricHelpTaskTimeAria}
|
|
1478
|
+
body={t.metricHelpTaskTimeBody}
|
|
1479
|
+
/>
|
|
1480
|
+
</div>
|
|
1481
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1482
|
+
{formatDuration(agg.taskMinutesTotal)}
|
|
1483
|
+
</div>
|
|
1484
|
+
</div>
|
|
1485
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1486
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1487
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1488
|
+
{t.summarySessionCoding}
|
|
1489
|
+
</span>
|
|
1490
|
+
<InlineMetricHelpTrigger
|
|
1491
|
+
ariaLabel={t.metricHelpSessionCodingAria}
|
|
1492
|
+
body={t.metricHelpSessionCodingBody}
|
|
1493
|
+
/>
|
|
1494
|
+
</div>
|
|
1495
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1496
|
+
{formatDuration(agg.sessionCodingMinutesTotal)}
|
|
1497
|
+
</div>
|
|
1498
|
+
</div>
|
|
1499
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1500
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1501
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1502
|
+
{t.summarySessionActive}
|
|
1503
|
+
</span>
|
|
1504
|
+
<InlineMetricHelpTrigger
|
|
1505
|
+
ariaLabel={t.metricHelpSessionActiveAria}
|
|
1506
|
+
body={t.metricHelpSessionActiveBody}
|
|
1507
|
+
/>
|
|
1508
|
+
</div>
|
|
1509
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1510
|
+
{formatDuration(agg.sessionActiveMinutesTotal)}
|
|
1511
|
+
</div>
|
|
1512
|
+
</div>
|
|
1513
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1514
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1515
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1516
|
+
{t.summarySessionWallClock}
|
|
1517
|
+
</span>
|
|
1518
|
+
<InlineMetricHelpTrigger
|
|
1519
|
+
align="end"
|
|
1520
|
+
ariaLabel={t.metricHelpSessionWallSummaryAria}
|
|
1521
|
+
body={t.metricHelpSessionWallSummaryBody}
|
|
1522
|
+
/>
|
|
1523
|
+
</div>
|
|
1524
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1525
|
+
{formatDuration(agg.sessionWallClockMinutesTotal)}
|
|
1526
|
+
</div>
|
|
1527
|
+
</div>
|
|
1528
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1529
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1530
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1531
|
+
{t.summaryAssiduityWithReference}
|
|
1532
|
+
</span>
|
|
1533
|
+
<InlineMetricHelpTrigger
|
|
1534
|
+
align="end"
|
|
1535
|
+
ariaLabel={t.metricHelpAssiduityRefAria}
|
|
1536
|
+
body={t.metricHelpAssiduityRefBody}
|
|
1537
|
+
/>
|
|
1538
|
+
</div>
|
|
1539
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1540
|
+
{agg.assiduityReferenceSessionCount}
|
|
1541
|
+
</div>
|
|
1542
|
+
</div>
|
|
1543
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1544
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1545
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1546
|
+
{t.summaryAssiduityLateSessions}
|
|
1547
|
+
</span>
|
|
1548
|
+
<InlineMetricHelpTrigger
|
|
1549
|
+
align="end"
|
|
1550
|
+
ariaLabel={t.metricHelpAssiduityLateCountAria}
|
|
1551
|
+
body={t.metricHelpAssiduityLateCountBody}
|
|
1552
|
+
/>
|
|
1553
|
+
</div>
|
|
1554
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1555
|
+
{agg.assiduityLateSessionCount}
|
|
1556
|
+
</div>
|
|
1557
|
+
</div>
|
|
1558
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1559
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1560
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1561
|
+
{t.summaryAssiduityLateTotal}
|
|
1562
|
+
</span>
|
|
1563
|
+
<InlineMetricHelpTrigger
|
|
1564
|
+
align="end"
|
|
1565
|
+
ariaLabel={t.metricHelpAssiduityLateTotalAria}
|
|
1566
|
+
body={t.metricHelpAssiduityLateTotalBody}
|
|
1567
|
+
/>
|
|
1568
|
+
</div>
|
|
1569
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1570
|
+
{formatDuration(agg.assiduityLateMinutesTotal)}
|
|
1571
|
+
</div>
|
|
1572
|
+
</div>
|
|
1573
|
+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
|
1574
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1575
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1576
|
+
{t.summaryAssiduityAvgLateWhenLate}
|
|
1577
|
+
</span>
|
|
1578
|
+
<InlineMetricHelpTrigger
|
|
1579
|
+
align="end"
|
|
1580
|
+
ariaLabel={t.metricHelpAssiduityAvgLateAria}
|
|
1581
|
+
body={t.metricHelpAssiduityAvgLateBody}
|
|
1582
|
+
/>
|
|
1583
|
+
</div>
|
|
1584
|
+
<div className="mt-1 text-2xl font-semibold tabular-nums">
|
|
1585
|
+
{agg.assiduityAverageLateMinutesWhenLate == null
|
|
1586
|
+
? "—"
|
|
1587
|
+
: formatDuration(agg.assiduityAverageLateMinutesWhenLate)}
|
|
1588
|
+
</div>
|
|
1589
|
+
</div>
|
|
1590
|
+
</div>
|
|
1591
|
+
{archivedExcludedTaskMinutes > 1e-9 ? (
|
|
1592
|
+
<div
|
|
1593
|
+
role="status"
|
|
1594
|
+
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"
|
|
1595
|
+
>
|
|
1596
|
+
{reportingArchivedExcludedRichText(
|
|
1597
|
+
t.reportingArchivedExcludedAside,
|
|
1598
|
+
archivedExcludedTaskMinutes,
|
|
1599
|
+
)}
|
|
1600
|
+
</div>
|
|
1601
|
+
) : null}
|
|
1602
|
+
</section>
|
|
1603
|
+
|
|
1604
|
+
{trackCodeMetrics ? (
|
|
1605
|
+
<section
|
|
1606
|
+
id="report-loc-metrics"
|
|
1607
|
+
className="mb-10 scroll-mt-28 space-y-4 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
1608
|
+
>
|
|
1609
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1610
|
+
<h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
|
|
1611
|
+
{t.tocLocSection}
|
|
1612
|
+
</h2>
|
|
1613
|
+
<ReportingFilteredBadge
|
|
1614
|
+
active={reportingFiltersActive}
|
|
1615
|
+
label={t.sectionFilteredBadge}
|
|
1616
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
1617
|
+
/>
|
|
1618
|
+
</div>
|
|
1619
|
+
<p className="text-xs text-zinc-500">
|
|
1620
|
+
{t.locMetricsHint}
|
|
1621
|
+
</p>
|
|
1622
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
1623
|
+
<div className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3">
|
|
1624
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1625
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1626
|
+
{t.summaryLinesWrittenTotal}
|
|
1627
|
+
</span>
|
|
1628
|
+
<InlineMetricHelpTrigger
|
|
1629
|
+
ariaLabel={t.metricHelpLinesTotalAria}
|
|
1630
|
+
body={t.metricHelpLinesTotalBody}
|
|
1631
|
+
/>
|
|
1632
|
+
</div>
|
|
1633
|
+
<div className="mt-1 text-xl font-semibold tabular-nums text-zinc-100">
|
|
1634
|
+
{agg.linesWrittenTotalSum}
|
|
1635
|
+
</div>
|
|
1636
|
+
</div>
|
|
1637
|
+
<div className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3">
|
|
1638
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1639
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1640
|
+
{t.summaryLinesWrittenHuman}
|
|
1641
|
+
</span>
|
|
1642
|
+
<InlineMetricHelpTrigger
|
|
1643
|
+
ariaLabel={t.metricHelpLinesHumanAria}
|
|
1644
|
+
body={t.metricHelpLinesHumanBody}
|
|
1645
|
+
/>
|
|
1646
|
+
</div>
|
|
1647
|
+
<div className="mt-1 text-xl font-semibold tabular-nums text-emerald-400/90">
|
|
1648
|
+
{agg.linesWrittenHumanSum}
|
|
1649
|
+
</div>
|
|
1650
|
+
</div>
|
|
1651
|
+
<div className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3">
|
|
1652
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1653
|
+
<span className="text-xs uppercase text-zinc-500">
|
|
1654
|
+
{t.summaryLinesWrittenAi}
|
|
1655
|
+
</span>
|
|
1656
|
+
<InlineMetricHelpTrigger
|
|
1657
|
+
align="end"
|
|
1658
|
+
ariaLabel={t.metricHelpLinesAiAria}
|
|
1659
|
+
body={t.metricHelpLinesAiBody}
|
|
1660
|
+
/>
|
|
1661
|
+
</div>
|
|
1662
|
+
<div className="mt-1 text-xl font-semibold tabular-nums text-violet-400/90">
|
|
1663
|
+
{agg.linesWrittenAiSum}
|
|
1664
|
+
</div>
|
|
1665
|
+
</div>
|
|
1666
|
+
</div>
|
|
1667
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
1668
|
+
<div>
|
|
1669
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
1670
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
1671
|
+
{t.locByLanguageSectionTitle}
|
|
1672
|
+
</h3>
|
|
1673
|
+
<ReportingFilteredBadge
|
|
1674
|
+
active={reportingFiltersActive}
|
|
1675
|
+
label={t.sectionFilteredBadge}
|
|
1676
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
1677
|
+
/>
|
|
1678
|
+
<InlineMetricHelpTrigger
|
|
1679
|
+
ariaLabel={t.metricHelpLocByLangTitleAria}
|
|
1680
|
+
body={t.metricHelpLocByLangTitleBody}
|
|
1681
|
+
/>
|
|
1682
|
+
</div>
|
|
1683
|
+
{agg.locByLanguageMerged.length === 0 ? (
|
|
1684
|
+
<p className="mt-3 text-sm text-zinc-500">—</p>
|
|
1685
|
+
) : (
|
|
1686
|
+
<table className="mt-3 w-full min-w-[12rem] text-left text-sm">
|
|
1687
|
+
<thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
|
|
1688
|
+
<tr>
|
|
1689
|
+
<th className="py-2 pr-3 font-medium">
|
|
1690
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1691
|
+
<span>{t.locByLanguageColLang}</span>
|
|
1692
|
+
<InlineMetricHelpTrigger
|
|
1693
|
+
ariaLabel={
|
|
1694
|
+
t.metricHelpAggLocColLangAria
|
|
1695
|
+
}
|
|
1696
|
+
body={t.metricHelpAggLocColLangBody}
|
|
1697
|
+
/>
|
|
1698
|
+
</div>
|
|
1699
|
+
</th>
|
|
1700
|
+
<th className="py-2 font-medium tabular-nums">
|
|
1701
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1702
|
+
<span>{t.locByLanguageColLines}</span>
|
|
1703
|
+
<InlineMetricHelpTrigger
|
|
1704
|
+
align="end"
|
|
1705
|
+
ariaLabel={
|
|
1706
|
+
t.metricHelpAggLocColLinesAria
|
|
1707
|
+
}
|
|
1708
|
+
body={t.metricHelpAggLocColLinesBody}
|
|
1709
|
+
/>
|
|
1710
|
+
</div>
|
|
1711
|
+
</th>
|
|
1712
|
+
</tr>
|
|
1713
|
+
</thead>
|
|
1714
|
+
<tbody>
|
|
1715
|
+
{agg.locByLanguageMerged.map(([lang, n]) => (
|
|
1716
|
+
<tr
|
|
1717
|
+
key={lang}
|
|
1718
|
+
className="border-b border-zinc-800/80 last:border-0"
|
|
1719
|
+
>
|
|
1720
|
+
<td className="py-2 pr-3 font-mono text-xs text-zinc-400">
|
|
1721
|
+
{lang}
|
|
1722
|
+
</td>
|
|
1723
|
+
<td className="py-2 tabular-nums text-zinc-200">
|
|
1724
|
+
{n}
|
|
1725
|
+
</td>
|
|
1726
|
+
</tr>
|
|
1727
|
+
))}
|
|
1728
|
+
</tbody>
|
|
1729
|
+
</table>
|
|
1730
|
+
)}
|
|
1731
|
+
</div>
|
|
1732
|
+
<div>
|
|
1733
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
1734
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
1735
|
+
{t.codingSignalsSectionTitle}
|
|
1736
|
+
</h3>
|
|
1737
|
+
<ReportingFilteredBadge
|
|
1738
|
+
active={reportingFiltersActive}
|
|
1739
|
+
label={t.sectionFilteredBadge}
|
|
1740
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
1741
|
+
/>
|
|
1742
|
+
<InlineMetricHelpTrigger
|
|
1743
|
+
ariaLabel={t.metricHelpCodingSignalsTitleAria}
|
|
1744
|
+
body={t.metricHelpCodingSignalsTitleBody}
|
|
1745
|
+
/>
|
|
1746
|
+
</div>
|
|
1747
|
+
{agg.codingSignalsByLanguageMerged.length === 0 ? (
|
|
1748
|
+
<p className="mt-3 text-sm text-zinc-500">—</p>
|
|
1749
|
+
) : (
|
|
1750
|
+
<table className="mt-3 w-full min-w-[12rem] text-left text-sm">
|
|
1751
|
+
<thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
|
|
1752
|
+
<tr>
|
|
1753
|
+
<th className="py-2 pr-3 font-medium">
|
|
1754
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1755
|
+
<span>{t.codingSignalsColLang}</span>
|
|
1756
|
+
<InlineMetricHelpTrigger
|
|
1757
|
+
ariaLabel={
|
|
1758
|
+
t.metricHelpAggSigColLangAria
|
|
1759
|
+
}
|
|
1760
|
+
body={t.metricHelpAggSigColLangBody}
|
|
1761
|
+
/>
|
|
1762
|
+
</div>
|
|
1763
|
+
</th>
|
|
1764
|
+
<th className="py-2 font-medium tabular-nums">
|
|
1765
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
1766
|
+
<span>{t.codingSignalsColCount}</span>
|
|
1767
|
+
<InlineMetricHelpTrigger
|
|
1768
|
+
align="end"
|
|
1769
|
+
ariaLabel={
|
|
1770
|
+
t.metricHelpAggSigColCountAria
|
|
1771
|
+
}
|
|
1772
|
+
body={t.metricHelpAggSigColCountBody}
|
|
1773
|
+
/>
|
|
1774
|
+
</div>
|
|
1775
|
+
</th>
|
|
1776
|
+
</tr>
|
|
1777
|
+
</thead>
|
|
1778
|
+
<tbody>
|
|
1779
|
+
{agg.codingSignalsByLanguageMerged.map(
|
|
1780
|
+
([lang, n]) => (
|
|
1781
|
+
<tr
|
|
1782
|
+
key={lang}
|
|
1783
|
+
className="border-b border-zinc-800/80 last:border-0"
|
|
1784
|
+
>
|
|
1785
|
+
<td className="py-2 pr-3 font-mono text-xs text-zinc-400">
|
|
1786
|
+
{lang}
|
|
1787
|
+
</td>
|
|
1788
|
+
<td className="py-2 tabular-nums text-zinc-200">
|
|
1789
|
+
{n}
|
|
1790
|
+
</td>
|
|
1791
|
+
</tr>
|
|
1792
|
+
),
|
|
1793
|
+
)}
|
|
1794
|
+
</tbody>
|
|
1795
|
+
</table>
|
|
1796
|
+
)}
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1799
|
+
</section>
|
|
1800
|
+
) : null}
|
|
1801
|
+
|
|
1802
|
+
{!hasReportingChartData ? (
|
|
1803
|
+
<p className="text-sm text-zinc-500">{t.noRowsInRange}</p>
|
|
1804
|
+
) : (
|
|
1805
|
+
<>
|
|
1806
|
+
{navigableWeekStarts.length > 1 ? (
|
|
1807
|
+
<section
|
|
1808
|
+
id="report-week-nav"
|
|
1809
|
+
className="mb-8 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-5"
|
|
1810
|
+
aria-label={t.weekNavAriaLabel}
|
|
1811
|
+
>
|
|
1812
|
+
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
1813
|
+
<h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
|
|
1814
|
+
{t.tocWeekNav}
|
|
1815
|
+
</h2>
|
|
1816
|
+
<ReportingFilteredBadge
|
|
1817
|
+
active={reportingFiltersActive}
|
|
1818
|
+
label={t.sectionFilteredBadge}
|
|
1819
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
1820
|
+
/>
|
|
1821
|
+
</div>
|
|
1822
|
+
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
1823
|
+
<p className="max-w-xl text-xs text-zinc-500">
|
|
1824
|
+
{t.weekNavHelpHint}
|
|
1825
|
+
</p>
|
|
1826
|
+
<div className="flex flex-wrap items-center justify-center gap-2 sm:justify-end">
|
|
1827
|
+
<InlineMetricHelpTrigger
|
|
1828
|
+
ariaLabel={t.metricHelpWeekNavAria}
|
|
1829
|
+
body={t.metricHelpWeekNavBody}
|
|
1830
|
+
/>
|
|
1831
|
+
<button
|
|
1832
|
+
type="button"
|
|
1833
|
+
className="inline-flex items-center gap-1 rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-200 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-40"
|
|
1834
|
+
disabled={chartWeekNavIndexSafe <= 0}
|
|
1835
|
+
aria-label={t.weekNavPrev}
|
|
1836
|
+
onClick={() =>
|
|
1837
|
+
setChartWeekNavIndex((i) => {
|
|
1838
|
+
const cur =
|
|
1839
|
+
i < 0
|
|
1840
|
+
? navigableWeekStarts.length - 1
|
|
1841
|
+
: i;
|
|
1842
|
+
return Math.max(0, cur - 1);
|
|
1843
|
+
})
|
|
1844
|
+
}
|
|
1845
|
+
>
|
|
1846
|
+
<ChevronLeft
|
|
1847
|
+
className="h-4 w-4 shrink-0"
|
|
1848
|
+
aria-hidden
|
|
1849
|
+
/>
|
|
1850
|
+
{t.weekNavPrev}
|
|
1851
|
+
</button>
|
|
1852
|
+
<span className="min-w-[10rem] px-2 text-center text-sm font-medium tabular-nums text-zinc-100">
|
|
1853
|
+
{formatWeekRangeLabel(
|
|
1854
|
+
navigableWeekStarts[chartWeekNavIndexSafe],
|
|
1855
|
+
reportLocale,
|
|
1856
|
+
)}
|
|
1857
|
+
</span>
|
|
1858
|
+
<button
|
|
1859
|
+
type="button"
|
|
1860
|
+
className="inline-flex items-center gap-1 rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-200 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-40"
|
|
1861
|
+
disabled={
|
|
1862
|
+
chartWeekNavIndexSafe >=
|
|
1863
|
+
navigableWeekStarts.length - 1
|
|
1864
|
+
}
|
|
1865
|
+
aria-label={t.weekNavNext}
|
|
1866
|
+
onClick={() =>
|
|
1867
|
+
setChartWeekNavIndex((i) => {
|
|
1868
|
+
const cur =
|
|
1869
|
+
i < 0
|
|
1870
|
+
? navigableWeekStarts.length - 1
|
|
1871
|
+
: i;
|
|
1872
|
+
return Math.min(
|
|
1873
|
+
navigableWeekStarts.length - 1,
|
|
1874
|
+
cur + 1,
|
|
1875
|
+
);
|
|
1876
|
+
})
|
|
1877
|
+
}
|
|
1878
|
+
>
|
|
1879
|
+
{t.weekNavNext}
|
|
1880
|
+
<ChevronRight
|
|
1881
|
+
className="h-4 w-4 shrink-0"
|
|
1882
|
+
aria-hidden
|
|
1883
|
+
/>
|
|
1884
|
+
</button>
|
|
1885
|
+
</div>
|
|
1886
|
+
</div>
|
|
1887
|
+
</section>
|
|
1888
|
+
) : null}
|
|
1889
|
+
<section
|
|
1890
|
+
id="report-chart-sessions"
|
|
1891
|
+
className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
1892
|
+
>
|
|
1893
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
1894
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
1895
|
+
{t.chartSessionsPerDay}
|
|
1896
|
+
</h3>
|
|
1897
|
+
<ReportingFilteredBadge
|
|
1898
|
+
active={reportingFiltersActive}
|
|
1899
|
+
label={t.sectionFilteredBadge}
|
|
1900
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
1901
|
+
/>
|
|
1902
|
+
<InlineMetricHelpTrigger
|
|
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}
|
|
1947
|
+
<InlineMetricHelpTrigger
|
|
1948
|
+
ariaLabel={t.metricHelpLegendDoneAria}
|
|
1949
|
+
body={t.metricHelpLegendDoneBody}
|
|
1950
|
+
/>
|
|
1951
|
+
</div>
|
|
1952
|
+
<div className="inline-flex items-center gap-1">
|
|
1953
|
+
<span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-amber-500" />
|
|
1954
|
+
{t.legendActive}
|
|
1955
|
+
<InlineMetricHelpTrigger
|
|
1956
|
+
ariaLabel={t.metricHelpLegendActiveAria}
|
|
1957
|
+
body={t.metricHelpLegendActiveBody}
|
|
1958
|
+
/>
|
|
1959
|
+
</div>
|
|
1960
|
+
</div>
|
|
1961
|
+
<div className="mt-4">
|
|
1962
|
+
<StackedTaskBars
|
|
1963
|
+
days={reportingDayKeys}
|
|
1964
|
+
done={agg.tasksByDayDone}
|
|
1965
|
+
active={agg.tasksByDayActive}
|
|
1966
|
+
max={peakTasks}
|
|
1967
|
+
undatedLabel={t.undatedLabel}
|
|
1968
|
+
/>
|
|
1969
|
+
</div>
|
|
1970
|
+
</section>
|
|
1971
|
+
|
|
1972
|
+
<section
|
|
1973
|
+
id="report-chart-task-time"
|
|
1974
|
+
className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
1975
|
+
>
|
|
1976
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
1977
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
1978
|
+
{t.chartTaskTimePerDay}
|
|
1979
|
+
</h3>
|
|
1980
|
+
<ReportingFilteredBadge
|
|
1981
|
+
active={reportingFiltersActive}
|
|
1982
|
+
label={t.sectionFilteredBadge}
|
|
1983
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
1984
|
+
/>
|
|
1985
|
+
<InlineMetricHelpTrigger
|
|
1986
|
+
ariaLabel={t.metricHelpChartTaskTimeAria}
|
|
1987
|
+
body={t.metricHelpChartTaskTimeBody}
|
|
1988
|
+
/>
|
|
1989
|
+
</div>
|
|
1990
|
+
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
|
|
1991
|
+
<span>{t.summaryTaskTimeRecorded}</span>
|
|
1992
|
+
<InlineMetricHelpTrigger
|
|
1993
|
+
ariaLabel={t.metricHelpTaskTimeAria}
|
|
1994
|
+
body={t.metricHelpTaskTimeBody}
|
|
1995
|
+
/>
|
|
1996
|
+
</div>
|
|
1997
|
+
<div className="mt-4">
|
|
1998
|
+
<MiniBars
|
|
1999
|
+
days={reportingDayKeys}
|
|
2000
|
+
values={agg.taskMinutesByDay}
|
|
2001
|
+
max={peakTaskMinutes}
|
|
2002
|
+
className="bg-sky-500/90"
|
|
2003
|
+
undatedLabel={t.undatedLabel}
|
|
2004
|
+
valueTitle={(m) => formatDuration(m)}
|
|
2005
|
+
/>
|
|
2006
|
+
</div>
|
|
2007
|
+
</section>
|
|
2008
|
+
|
|
2009
|
+
<section
|
|
2010
|
+
id="report-chart-session-wall"
|
|
2011
|
+
className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
2012
|
+
>
|
|
2013
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2014
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2015
|
+
{t.chartSessionWallPerDay}
|
|
2016
|
+
</h3>
|
|
2017
|
+
<ReportingFilteredBadge
|
|
2018
|
+
active={reportingFiltersActive}
|
|
2019
|
+
label={t.sectionFilteredBadge}
|
|
2020
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2021
|
+
/>
|
|
2022
|
+
<InlineMetricHelpTrigger
|
|
2023
|
+
ariaLabel={t.metricHelpChartSessionWallAria}
|
|
2024
|
+
body={t.metricHelpChartSessionWallBody}
|
|
2025
|
+
/>
|
|
2026
|
+
</div>
|
|
2027
|
+
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
|
|
2028
|
+
<span>{t.summarySessionWallClock}</span>
|
|
2029
|
+
<InlineMetricHelpTrigger
|
|
2030
|
+
ariaLabel={t.metricHelpSessionWallSummaryAria}
|
|
2031
|
+
body={t.metricHelpSessionWallSummaryBody}
|
|
2032
|
+
/>
|
|
2033
|
+
</div>
|
|
2034
|
+
<div className="mt-4">
|
|
2035
|
+
<MiniBars
|
|
2036
|
+
days={reportingDayKeys}
|
|
2037
|
+
values={agg.sessionWallClockMinutesByDay}
|
|
2038
|
+
max={peakSessionWallMinutes}
|
|
2039
|
+
className="bg-fuchsia-500/85"
|
|
2040
|
+
undatedLabel={t.undatedLabel}
|
|
2041
|
+
valueTitle={(m) => formatDuration(m)}
|
|
2042
|
+
/>
|
|
2043
|
+
</div>
|
|
2044
|
+
</section>
|
|
2045
|
+
|
|
2046
|
+
<section
|
|
2047
|
+
id="report-daily-table"
|
|
2048
|
+
className="scroll-mt-28 overflow-x-auto rounded-xl border border-zinc-800 bg-zinc-900/50"
|
|
2049
|
+
>
|
|
2050
|
+
<div className="flex flex-wrap items-center gap-2 border-b border-zinc-800 px-4 py-3">
|
|
2051
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2052
|
+
{t.tocDailyTable}
|
|
2053
|
+
</h3>
|
|
2054
|
+
<ReportingFilteredBadge
|
|
2055
|
+
active={reportingFiltersActive}
|
|
2056
|
+
label={t.sectionFilteredBadge}
|
|
2057
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2058
|
+
/>
|
|
2059
|
+
</div>
|
|
2060
|
+
<table className="w-full min-w-[48rem] text-left text-sm">
|
|
2061
|
+
<thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
|
|
2062
|
+
<tr>
|
|
2063
|
+
<th className="px-4 py-3 font-medium">
|
|
2064
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2065
|
+
<span>{t.tableDay}</span>
|
|
2066
|
+
<InlineMetricHelpTrigger
|
|
2067
|
+
ariaLabel={t.metricHelpTblDayAria}
|
|
2068
|
+
body={t.metricHelpTblDayBody}
|
|
2069
|
+
/>
|
|
2070
|
+
</div>
|
|
2071
|
+
</th>
|
|
2072
|
+
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2073
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2074
|
+
<span>{t.tableSessions}</span>
|
|
2075
|
+
<InlineMetricHelpTrigger
|
|
2076
|
+
ariaLabel={t.metricHelpTblSessionsAria}
|
|
2077
|
+
body={t.metricHelpTblSessionsBody}
|
|
2078
|
+
/>
|
|
2079
|
+
</div>
|
|
2080
|
+
</th>
|
|
2081
|
+
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2082
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2083
|
+
<span>{t.tableSessionWall}</span>
|
|
2084
|
+
<InlineMetricHelpTrigger
|
|
2085
|
+
ariaLabel={t.metricHelpTblSessionWallAria}
|
|
2086
|
+
body={t.metricHelpTblSessionWallBody}
|
|
2087
|
+
/>
|
|
2088
|
+
</div>
|
|
2089
|
+
</th>
|
|
2090
|
+
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2091
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2092
|
+
<span>{t.tableDone}</span>
|
|
2093
|
+
<InlineMetricHelpTrigger
|
|
2094
|
+
ariaLabel={t.metricHelpTblDoneAria}
|
|
2095
|
+
body={t.metricHelpTblDoneBody}
|
|
2096
|
+
/>
|
|
2097
|
+
</div>
|
|
2098
|
+
</th>
|
|
2099
|
+
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2100
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2101
|
+
<span>{t.tableActive}</span>
|
|
2102
|
+
<InlineMetricHelpTrigger
|
|
2103
|
+
align="end"
|
|
2104
|
+
ariaLabel={t.metricHelpTblActiveAria}
|
|
2105
|
+
body={t.metricHelpTblActiveBody}
|
|
2106
|
+
/>
|
|
2107
|
+
</div>
|
|
2108
|
+
</th>
|
|
2109
|
+
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2110
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2111
|
+
<span>{t.tableTaskTime}</span>
|
|
2112
|
+
<InlineMetricHelpTrigger
|
|
2113
|
+
ariaLabel={t.metricHelpTblTaskTimeAria}
|
|
2114
|
+
body={t.metricHelpTblTaskTimeBody}
|
|
2115
|
+
/>
|
|
2116
|
+
</div>
|
|
2117
|
+
</th>
|
|
2118
|
+
<th className="px-4 py-3 font-medium tabular-nums">
|
|
2119
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2120
|
+
<span>{t.tableSessionCoding}</span>
|
|
2121
|
+
<InlineMetricHelpTrigger
|
|
2122
|
+
align="end"
|
|
2123
|
+
ariaLabel={t.metricHelpTblSessionCodingAria}
|
|
2124
|
+
body={t.metricHelpTblSessionCodingBody}
|
|
2125
|
+
/>
|
|
2126
|
+
</div>
|
|
2127
|
+
</th>
|
|
2128
|
+
</tr>
|
|
2129
|
+
</thead>
|
|
2130
|
+
<tbody>
|
|
2131
|
+
{reportingDayKeys.map((d) => (
|
|
2132
|
+
<tr
|
|
2133
|
+
key={d}
|
|
2134
|
+
className="border-b border-zinc-800/80 last:border-0"
|
|
2135
|
+
>
|
|
2136
|
+
<td className="px-4 py-2.5 text-zinc-200">
|
|
2137
|
+
{dayLabel(d, t.undatedLabel)}
|
|
2138
|
+
</td>
|
|
2139
|
+
<td className="px-4 py-2.5 tabular-nums text-zinc-300">
|
|
2140
|
+
{agg.sessionsByDay[d] ?? 0}
|
|
2141
|
+
</td>
|
|
2142
|
+
<td className="px-4 py-2.5 tabular-nums text-fuchsia-300/90">
|
|
2143
|
+
{formatMinutesCell(
|
|
2144
|
+
agg.sessionWallClockMinutesByDay[d],
|
|
2145
|
+
)}
|
|
2146
|
+
</td>
|
|
2147
|
+
<td className="px-4 py-2.5 tabular-nums text-emerald-400/90">
|
|
2148
|
+
{agg.tasksByDayDone[d] ?? 0}
|
|
2149
|
+
</td>
|
|
2150
|
+
<td className="px-4 py-2.5 tabular-nums text-amber-400/90">
|
|
2151
|
+
{agg.tasksByDayActive[d] ?? 0}
|
|
2152
|
+
</td>
|
|
2153
|
+
<td className="px-4 py-2.5 tabular-nums text-sky-300/90">
|
|
2154
|
+
{formatMinutesCell(agg.taskMinutesByDay[d])}
|
|
2155
|
+
</td>
|
|
2156
|
+
<td className="px-4 py-2.5 tabular-nums text-zinc-300">
|
|
2157
|
+
{formatMinutesCell(
|
|
2158
|
+
agg.sessionCodingMinutesByDay[d],
|
|
2159
|
+
)}
|
|
2160
|
+
</td>
|
|
2161
|
+
</tr>
|
|
2162
|
+
))}
|
|
2163
|
+
</tbody>
|
|
2164
|
+
</table>
|
|
2165
|
+
</section>
|
|
2166
|
+
|
|
2167
|
+
<section
|
|
2168
|
+
id="report-tag-time"
|
|
2169
|
+
className="mt-10 scroll-mt-28 space-y-8 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
2170
|
+
>
|
|
2171
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2172
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2173
|
+
{t.tagTimeSectionTitle}
|
|
2174
|
+
</h3>
|
|
2175
|
+
<ReportingFilteredBadge
|
|
2176
|
+
active={reportingFiltersActive}
|
|
2177
|
+
label={t.sectionFilteredBadge}
|
|
2178
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2179
|
+
/>
|
|
2180
|
+
<InlineMetricHelpTrigger
|
|
2181
|
+
ariaLabel={t.metricHelpTagTimeSectionAria}
|
|
2182
|
+
body={t.metricHelpTagTimeSectionBody}
|
|
2183
|
+
/>
|
|
2184
|
+
</div>
|
|
2185
|
+
<p className="text-xs text-zinc-500">
|
|
2186
|
+
{t.tagTimeSectionHint}
|
|
2187
|
+
</p>
|
|
2188
|
+
|
|
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
|
+
<div id="report-tag-time-week" className="space-y-4">
|
|
2380
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2381
|
+
<h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-400">
|
|
2382
|
+
{t.tagTimeByWeekTitle}
|
|
2383
|
+
</h4>
|
|
2384
|
+
<InlineMetricHelpTrigger
|
|
2385
|
+
ariaLabel={t.metricHelpTagTimeWeekTableAria}
|
|
2386
|
+
body={t.metricHelpTagTimeWeekTableBody}
|
|
2387
|
+
/>
|
|
2388
|
+
<InlineMetricHelpTrigger
|
|
2389
|
+
ariaLabel={t.metricHelpTagWeekCalendarAria}
|
|
2390
|
+
body={t.metricHelpTagWeekCalendarBody}
|
|
2391
|
+
/>
|
|
2392
|
+
</div>
|
|
2393
|
+
|
|
2394
|
+
<fieldset className="rounded-lg border border-zinc-800/90 bg-zinc-950/30 p-3">
|
|
2395
|
+
<legend className="px-1 text-xs text-zinc-500">
|
|
2396
|
+
<span className="inline-flex flex-wrap items-center gap-1">
|
|
2397
|
+
{t.tagWeekStartsOnLegend}
|
|
2398
|
+
<InlineMetricHelpTrigger
|
|
2399
|
+
ariaLabel={t.metricHelpTagWeekStartsOnAria}
|
|
2400
|
+
body={t.metricHelpTagWeekStartsOnBody}
|
|
2401
|
+
/>
|
|
2402
|
+
</span>
|
|
2403
|
+
</legend>
|
|
2404
|
+
<div
|
|
2405
|
+
className="mt-2 flex flex-wrap gap-2"
|
|
2406
|
+
role="radiogroup"
|
|
2407
|
+
aria-label={t.tagWeekStartsOnLegend}
|
|
2408
|
+
>
|
|
2409
|
+
{(
|
|
2410
|
+
[
|
|
2411
|
+
["monday", t.weekStartsMonday],
|
|
2412
|
+
["sunday", t.weekStartsSunday],
|
|
2413
|
+
["saturday", t.weekStartsSaturday],
|
|
2414
|
+
] as const
|
|
2415
|
+
).map(([value, label]) => (
|
|
2416
|
+
<button
|
|
2417
|
+
key={value}
|
|
2418
|
+
type="button"
|
|
2419
|
+
role="radio"
|
|
2420
|
+
aria-checked={weekStartsOn === value}
|
|
2421
|
+
onClick={() => persistWeekStartsOn(value)}
|
|
2422
|
+
className={`rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
2423
|
+
weekStartsOn === value
|
|
2424
|
+
? "border-violet-500/80 bg-violet-950/50 text-violet-200"
|
|
2425
|
+
: "border-zinc-700 bg-zinc-900/60 text-zinc-400 hover:border-zinc-600 hover:text-zinc-200"
|
|
2426
|
+
}`}
|
|
2427
|
+
>
|
|
2428
|
+
{label}
|
|
2429
|
+
</button>
|
|
2430
|
+
))}
|
|
2431
|
+
</div>
|
|
2432
|
+
</fieldset>
|
|
2433
|
+
|
|
2434
|
+
{tagWeekCalendarRows.length === 0 ? (
|
|
2435
|
+
<p className="text-sm text-zinc-500">—</p>
|
|
2436
|
+
) : tagWeekCalendarRowsVisible.length === 0 ? (
|
|
2437
|
+
<div className="space-y-2 text-sm text-zinc-500">
|
|
2438
|
+
<p>{t.weekNavNoTagDataThisWeek}</p>
|
|
2439
|
+
{archivedExcludedTaskMinutes > 1e-9 ? (
|
|
2440
|
+
<p className="text-amber-200/90">
|
|
2441
|
+
{reportingArchivedExcludedRichText(
|
|
2442
|
+
t.reportingArchivedExcludedAside,
|
|
2443
|
+
archivedExcludedTaskMinutes,
|
|
2444
|
+
)}
|
|
2445
|
+
</p>
|
|
2446
|
+
) : null}
|
|
2447
|
+
</div>
|
|
2448
|
+
) : (
|
|
2449
|
+
<div className="space-y-8">
|
|
2450
|
+
{tagCalendarWeekGroups.map(
|
|
2451
|
+
({ weekStart, rows }) => (
|
|
2452
|
+
<div key={weekStart}>
|
|
2453
|
+
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
2454
|
+
<p className="text-sm font-medium text-zinc-300">
|
|
2455
|
+
{formatWeekRangeLabel(
|
|
2456
|
+
weekStart,
|
|
2457
|
+
reportLocale,
|
|
2458
|
+
)}
|
|
2459
|
+
</p>
|
|
2460
|
+
<InlineMetricHelpTrigger
|
|
2461
|
+
ariaLabel={
|
|
2462
|
+
t.metricHelpTagTimeColWeekAria
|
|
2463
|
+
}
|
|
2464
|
+
body={t.metricHelpTagTimeColWeekBody}
|
|
2465
|
+
/>
|
|
2466
|
+
</div>
|
|
2467
|
+
<div className="overflow-x-auto rounded-lg border border-zinc-800/80">
|
|
2468
|
+
<table className="w-full min-w-[28rem] text-left text-sm">
|
|
2469
|
+
<thead className="border-b border-zinc-800 bg-zinc-950/40 text-xs uppercase text-zinc-500">
|
|
2470
|
+
<tr>
|
|
2471
|
+
<th className="px-2 py-2 pl-3 font-medium">
|
|
2472
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2473
|
+
<span>{t.tagTimeColTag}</span>
|
|
2474
|
+
<InlineMetricHelpTrigger
|
|
2475
|
+
ariaLabel={
|
|
2476
|
+
t.metricHelpTagTimeColTagAria
|
|
2477
|
+
}
|
|
2478
|
+
body={
|
|
2479
|
+
t.metricHelpTagTimeColTagBody
|
|
2480
|
+
}
|
|
2481
|
+
/>
|
|
2482
|
+
</div>
|
|
2483
|
+
</th>
|
|
2484
|
+
{weekdayDateColumnHeaders(
|
|
2485
|
+
weekStart,
|
|
2486
|
+
reportLocale,
|
|
2487
|
+
).map((col) => (
|
|
2488
|
+
<th
|
|
2489
|
+
key={col.dateKey}
|
|
2490
|
+
className="px-1 py-2 text-center font-medium normal-case"
|
|
2491
|
+
title={col.dateKey}
|
|
2492
|
+
>
|
|
2493
|
+
<div className="flex flex-col items-center gap-0.5 leading-tight">
|
|
2494
|
+
<span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500">
|
|
2495
|
+
{col.weekdayShort}
|
|
2496
|
+
</span>
|
|
2497
|
+
<span className="text-[0.7rem] font-medium tabular-nums text-zinc-300">
|
|
2498
|
+
{col.calendarDateShort}
|
|
2499
|
+
</span>
|
|
2500
|
+
</div>
|
|
2501
|
+
</th>
|
|
2502
|
+
))}
|
|
2503
|
+
<th className="px-2 py-2 pr-3 text-center font-medium">
|
|
2504
|
+
<div className="flex min-h-5 items-center justify-center gap-0.5">
|
|
2505
|
+
<span>
|
|
2506
|
+
{t.tagWeekSumColumn}
|
|
2507
|
+
</span>
|
|
2508
|
+
<InlineMetricHelpTrigger
|
|
2509
|
+
align="end"
|
|
2510
|
+
ariaLabel={
|
|
2511
|
+
t.metricHelpTagTimeColMinutesAria
|
|
2512
|
+
}
|
|
2513
|
+
body={
|
|
2514
|
+
t.metricHelpTagTimeColMinutesBody
|
|
2515
|
+
}
|
|
2516
|
+
/>
|
|
2517
|
+
</div>
|
|
2518
|
+
</th>
|
|
2519
|
+
</tr>
|
|
2520
|
+
</thead>
|
|
2521
|
+
<tbody>
|
|
2522
|
+
{buildTagWeekDisplayBlocks(rows).map(
|
|
2523
|
+
(block) => {
|
|
2524
|
+
if (block.kind === "leaf") {
|
|
2525
|
+
const row = block.row;
|
|
2526
|
+
return (
|
|
2527
|
+
<tr
|
|
2528
|
+
key={`${weekStart}\0${row.tagKey}`}
|
|
2529
|
+
className="border-b border-zinc-800/70 last:border-0"
|
|
2530
|
+
>
|
|
2531
|
+
<ReportingTagNameCell
|
|
2532
|
+
tagKey={row.tagKey}
|
|
2533
|
+
displayLabel={
|
|
2534
|
+
row.displayTag ||
|
|
2535
|
+
row.tagKey
|
|
2536
|
+
}
|
|
2537
|
+
untaggedLabel={
|
|
2538
|
+
t.tagTimeUntagged
|
|
2539
|
+
}
|
|
2540
|
+
descriptions={
|
|
2541
|
+
tagDescriptions
|
|
2542
|
+
}
|
|
2543
|
+
className="px-2 py-2 pl-3 text-violet-200/90"
|
|
2544
|
+
/>
|
|
2545
|
+
{row.slots.map(
|
|
2546
|
+
(mins, i) => {
|
|
2547
|
+
const dateKey =
|
|
2548
|
+
addDaysYmd(
|
|
2549
|
+
row.weekStart,
|
|
2550
|
+
i,
|
|
2551
|
+
);
|
|
2552
|
+
return (
|
|
2553
|
+
<td
|
|
2554
|
+
key={dateKey}
|
|
2555
|
+
className="px-1 py-2 text-center tabular-nums text-zinc-200"
|
|
2556
|
+
title={dateKey}
|
|
2557
|
+
>
|
|
2558
|
+
{mins > 0
|
|
2559
|
+
? formatDuration(
|
|
2560
|
+
mins,
|
|
2561
|
+
)
|
|
2562
|
+
: "—"}
|
|
2563
|
+
</td>
|
|
2564
|
+
);
|
|
2565
|
+
},
|
|
2566
|
+
)}
|
|
2567
|
+
<td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
|
|
2568
|
+
{formatDuration(
|
|
2569
|
+
row.total,
|
|
2570
|
+
)}
|
|
2571
|
+
</td>
|
|
2572
|
+
</tr>
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
const rollupKey = `${weekStart}:::${block.projectKeyLower}`;
|
|
2576
|
+
const open =
|
|
2577
|
+
tagWeekRollupOpenKeys.has(
|
|
2578
|
+
rollupKey,
|
|
2579
|
+
);
|
|
2580
|
+
const rollupDesc =
|
|
2581
|
+
reportingProjectDescriptionLine(
|
|
2582
|
+
block.projectKeyLower,
|
|
2583
|
+
projectDescriptions,
|
|
2584
|
+
) ?? "";
|
|
2585
|
+
return (
|
|
2586
|
+
<Fragment key={rollupKey}>
|
|
2587
|
+
<tr className="border-b border-zinc-800/70 bg-zinc-900/40">
|
|
2588
|
+
<td className="px-2 py-2 pl-3 align-top text-violet-200/90">
|
|
2589
|
+
<div className="flex items-start gap-1.5">
|
|
2590
|
+
<button
|
|
2591
|
+
type="button"
|
|
2592
|
+
aria-expanded={
|
|
2593
|
+
open
|
|
2594
|
+
? "true"
|
|
2595
|
+
: "false"
|
|
2596
|
+
}
|
|
2597
|
+
aria-label={
|
|
2598
|
+
t.tagWeekScopedRollupToggleAria
|
|
2599
|
+
}
|
|
2600
|
+
className="mt-0.5 shrink-0 rounded p-0.5 text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
|
2601
|
+
onClick={() =>
|
|
2602
|
+
toggleTagWeekRollup(
|
|
2603
|
+
rollupKey,
|
|
2604
|
+
)
|
|
2605
|
+
}
|
|
2606
|
+
>
|
|
2607
|
+
<ChevronRight
|
|
2608
|
+
className={`size-4 transition-transform duration-200 ${
|
|
2609
|
+
open
|
|
2610
|
+
? "rotate-90"
|
|
2611
|
+
: ""
|
|
2612
|
+
}`}
|
|
2613
|
+
strokeWidth={2}
|
|
2614
|
+
aria-hidden
|
|
2615
|
+
/>
|
|
2616
|
+
</button>
|
|
2617
|
+
<div className="min-w-0">
|
|
2618
|
+
<div className="flex flex-wrap items-baseline gap-x-1.5 font-medium">
|
|
2619
|
+
<span>
|
|
2620
|
+
@
|
|
2621
|
+
{
|
|
2622
|
+
block.displayProject
|
|
2623
|
+
}
|
|
2624
|
+
</span>
|
|
2625
|
+
<span className="text-[0.65rem] font-normal tabular-nums text-zinc-500">
|
|
2626
|
+
(
|
|
2627
|
+
{
|
|
2628
|
+
block.children
|
|
2629
|
+
.length
|
|
2630
|
+
}
|
|
2631
|
+
)
|
|
2632
|
+
</span>
|
|
2633
|
+
</div>
|
|
2634
|
+
{rollupDesc ? (
|
|
2635
|
+
<p className="mt-1 max-w-[min(22rem,55vw)] whitespace-pre-line text-[0.65rem] font-normal leading-snug text-zinc-500">
|
|
2636
|
+
{rollupDesc}
|
|
2637
|
+
</p>
|
|
2638
|
+
) : null}
|
|
2639
|
+
</div>
|
|
2640
|
+
</div>
|
|
2641
|
+
</td>
|
|
2642
|
+
{block.parentSlots.map(
|
|
2643
|
+
(mins, i) => {
|
|
2644
|
+
const dateKey =
|
|
2645
|
+
addDaysYmd(
|
|
2646
|
+
block.weekStart,
|
|
2647
|
+
i,
|
|
2648
|
+
);
|
|
2649
|
+
return (
|
|
2650
|
+
<td
|
|
2651
|
+
key={dateKey}
|
|
2652
|
+
className="px-1 py-2 text-center tabular-nums text-zinc-200"
|
|
2653
|
+
title={dateKey}
|
|
2654
|
+
>
|
|
2655
|
+
{mins > 0
|
|
2656
|
+
? formatDuration(
|
|
2657
|
+
mins,
|
|
2658
|
+
)
|
|
2659
|
+
: "—"}
|
|
2660
|
+
</td>
|
|
2661
|
+
);
|
|
2662
|
+
},
|
|
2663
|
+
)}
|
|
2664
|
+
<td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
|
|
2665
|
+
{formatDuration(
|
|
2666
|
+
block.parentTotal,
|
|
2667
|
+
)}
|
|
2668
|
+
</td>
|
|
2669
|
+
</tr>
|
|
2670
|
+
{open
|
|
2671
|
+
? block.children.map(
|
|
2672
|
+
(child) => (
|
|
2673
|
+
<tr
|
|
2674
|
+
key={`${weekStart}\0${child.tagKey}`}
|
|
2675
|
+
className="border-b border-zinc-800/55 bg-zinc-950/30"
|
|
2676
|
+
>
|
|
2677
|
+
<ReportingTagNameCell
|
|
2678
|
+
tagKey={
|
|
2679
|
+
child.tagKey
|
|
2680
|
+
}
|
|
2681
|
+
displayLabel={
|
|
2682
|
+
child.displayTag ||
|
|
2683
|
+
child.tagKey
|
|
2684
|
+
}
|
|
2685
|
+
untaggedLabel={
|
|
2686
|
+
t.tagTimeUntagged
|
|
2687
|
+
}
|
|
2688
|
+
descriptions={
|
|
2689
|
+
tagDescriptions
|
|
2690
|
+
}
|
|
2691
|
+
className="px-2 py-2 pl-10 text-violet-200/85"
|
|
2692
|
+
/>
|
|
2693
|
+
{child.slots.map(
|
|
2694
|
+
(mins, i) => {
|
|
2695
|
+
const dateKey =
|
|
2696
|
+
addDaysYmd(
|
|
2697
|
+
child.weekStart,
|
|
2698
|
+
i,
|
|
2699
|
+
);
|
|
2700
|
+
return (
|
|
2701
|
+
<td
|
|
2702
|
+
key={
|
|
2703
|
+
dateKey
|
|
2704
|
+
}
|
|
2705
|
+
className="px-1 py-2 text-center tabular-nums text-zinc-300/95"
|
|
2706
|
+
title={
|
|
2707
|
+
dateKey
|
|
2708
|
+
}
|
|
2709
|
+
>
|
|
2710
|
+
{mins > 0
|
|
2711
|
+
? formatDuration(
|
|
2712
|
+
mins,
|
|
2713
|
+
)
|
|
2714
|
+
: "—"}
|
|
2715
|
+
</td>
|
|
2716
|
+
);
|
|
2717
|
+
},
|
|
2718
|
+
)}
|
|
2719
|
+
<td className="px-2 py-2 pr-3 text-center tabular-nums font-medium text-zinc-200">
|
|
2720
|
+
{formatDuration(
|
|
2721
|
+
child.total,
|
|
2722
|
+
)}
|
|
2723
|
+
</td>
|
|
2724
|
+
</tr>
|
|
2725
|
+
),
|
|
2726
|
+
)
|
|
2727
|
+
: null}
|
|
2728
|
+
</Fragment>
|
|
2729
|
+
);
|
|
2730
|
+
},
|
|
2731
|
+
)}
|
|
2732
|
+
</tbody>
|
|
2733
|
+
</table>
|
|
2734
|
+
</div>
|
|
2735
|
+
</div>
|
|
2736
|
+
),
|
|
2737
|
+
)}
|
|
2738
|
+
</div>
|
|
2739
|
+
)}
|
|
2740
|
+
</div>
|
|
2741
|
+
</section>
|
|
2742
|
+
|
|
2743
|
+
<section
|
|
2744
|
+
id="report-projects"
|
|
2745
|
+
className="mt-10 scroll-mt-28 space-y-4 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
|
|
2746
|
+
>
|
|
2747
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2748
|
+
<h3 className="text-sm font-semibold text-zinc-200">
|
|
2749
|
+
{t.projectSectionTitle}
|
|
2750
|
+
</h3>
|
|
2751
|
+
<ReportingFilteredBadge
|
|
2752
|
+
active={reportingFiltersActive}
|
|
2753
|
+
label={t.sectionFilteredBadge}
|
|
2754
|
+
titleText={t.sectionFilteredBadgeTitle}
|
|
2755
|
+
/>
|
|
2756
|
+
<InlineMetricHelpTrigger
|
|
2757
|
+
ariaLabel={t.metricHelpProjectTitleAria}
|
|
2758
|
+
body={t.metricHelpProjectTitleBody}
|
|
2759
|
+
/>
|
|
2760
|
+
</div>
|
|
2761
|
+
<p className="text-xs text-zinc-500">
|
|
2762
|
+
{t.projectTableHint}
|
|
2763
|
+
</p>
|
|
2764
|
+
|
|
2765
|
+
<div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
|
|
2766
|
+
<h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-400">
|
|
2767
|
+
{t.projectWeeklyCalendarTitle}
|
|
2768
|
+
</h4>
|
|
2769
|
+
<InlineMetricHelpTrigger
|
|
2770
|
+
ariaLabel={t.metricHelpProjectCalendarAria}
|
|
2771
|
+
body={t.metricHelpProjectCalendarBody}
|
|
2772
|
+
/>
|
|
2773
|
+
</div>
|
|
2774
|
+
|
|
2775
|
+
{projectWeekCalendarRows.length === 0 ? (
|
|
2776
|
+
<p className="text-sm text-zinc-500">—</p>
|
|
2777
|
+
) : projectWeekCalendarRowsVisible.length === 0 ? (
|
|
2778
|
+
<div className="space-y-2 text-sm text-zinc-500">
|
|
2779
|
+
<p>{t.weekNavNoProjectDataThisWeek}</p>
|
|
2780
|
+
{archivedExcludedTaskMinutes > 1e-9 ? (
|
|
2781
|
+
<p className="text-amber-200/90">
|
|
2782
|
+
{reportingArchivedExcludedRichText(
|
|
2783
|
+
t.reportingArchivedExcludedAside,
|
|
2784
|
+
archivedExcludedTaskMinutes,
|
|
2785
|
+
)}
|
|
2786
|
+
</p>
|
|
2787
|
+
) : null}
|
|
2788
|
+
</div>
|
|
2789
|
+
) : (
|
|
2790
|
+
<div className="space-y-8">
|
|
2791
|
+
{projectCalendarWeekGroups.map(
|
|
2792
|
+
({ weekStart, rows }) => (
|
|
2793
|
+
<div key={weekStart}>
|
|
2794
|
+
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
2795
|
+
<p className="text-sm font-medium text-zinc-300">
|
|
2796
|
+
{formatWeekRangeLabel(
|
|
2797
|
+
weekStart,
|
|
2798
|
+
reportLocale,
|
|
2799
|
+
)}
|
|
2800
|
+
</p>
|
|
2801
|
+
<InlineMetricHelpTrigger
|
|
2802
|
+
ariaLabel={t.metricHelpTagTimeColWeekAria}
|
|
2803
|
+
body={t.metricHelpTagTimeColWeekBody}
|
|
2804
|
+
/>
|
|
2805
|
+
</div>
|
|
2806
|
+
<div className="overflow-x-auto rounded-lg border border-zinc-800/80">
|
|
2807
|
+
<table className="w-full min-w-[28rem] text-left text-sm">
|
|
2808
|
+
<thead className="border-b border-zinc-800 bg-zinc-950/40 text-xs uppercase text-zinc-500">
|
|
2809
|
+
<tr>
|
|
2810
|
+
<th className="px-2 py-2 pl-3 font-medium">
|
|
2811
|
+
<div className="flex min-h-5 items-center gap-0.5">
|
|
2812
|
+
<span>{t.projectColProject}</span>
|
|
2813
|
+
<InlineMetricHelpTrigger
|
|
2814
|
+
ariaLabel={
|
|
2815
|
+
t.metricHelpProjectColProjAria
|
|
2816
|
+
}
|
|
2817
|
+
body={
|
|
2818
|
+
t.metricHelpProjectColProjBody
|
|
2819
|
+
}
|
|
2820
|
+
/>
|
|
2821
|
+
</div>
|
|
2822
|
+
</th>
|
|
2823
|
+
{weekdayDateColumnHeaders(
|
|
2824
|
+
weekStart,
|
|
2825
|
+
reportLocale,
|
|
2826
|
+
).map((col) => (
|
|
2827
|
+
<th
|
|
2828
|
+
key={`proj-${col.dateKey}`}
|
|
2829
|
+
className="px-1 py-2 text-center font-medium normal-case"
|
|
2830
|
+
title={col.dateKey}
|
|
2831
|
+
>
|
|
2832
|
+
<div className="flex flex-col items-center gap-0.5 leading-tight">
|
|
2833
|
+
<span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500">
|
|
2834
|
+
{col.weekdayShort}
|
|
2835
|
+
</span>
|
|
2836
|
+
<span className="text-[0.7rem] font-medium tabular-nums text-zinc-300">
|
|
2837
|
+
{col.calendarDateShort}
|
|
2838
|
+
</span>
|
|
2839
|
+
</div>
|
|
2840
|
+
</th>
|
|
2841
|
+
))}
|
|
2842
|
+
<th className="px-2 py-2 pr-3 text-center font-medium">
|
|
2843
|
+
<div className="flex min-h-5 items-center justify-center gap-0.5">
|
|
2844
|
+
<span>{t.tagWeekSumColumn}</span>
|
|
2845
|
+
<InlineMetricHelpTrigger
|
|
2846
|
+
align="end"
|
|
2847
|
+
ariaLabel={
|
|
2848
|
+
t.metricHelpProjectColTimeAria
|
|
2849
|
+
}
|
|
2850
|
+
body={
|
|
2851
|
+
t.metricHelpProjectColTimeBody
|
|
2852
|
+
}
|
|
2853
|
+
/>
|
|
2854
|
+
</div>
|
|
2855
|
+
</th>
|
|
2856
|
+
</tr>
|
|
2857
|
+
</thead>
|
|
2858
|
+
<tbody>
|
|
2859
|
+
{rows.map((row) => (
|
|
2860
|
+
<tr
|
|
2861
|
+
key={`${weekStart}\0${row.projectKey}`}
|
|
2862
|
+
className="border-b border-zinc-800/70 last:border-0"
|
|
2863
|
+
>
|
|
2864
|
+
<ReportingProjectNameCell
|
|
2865
|
+
projectKey={row.projectKey}
|
|
2866
|
+
displayLabel={
|
|
2867
|
+
row.displayProject ||
|
|
2868
|
+
row.projectKey
|
|
2869
|
+
}
|
|
2870
|
+
unassignedLabel={
|
|
2871
|
+
t.projectUnassigned
|
|
2872
|
+
}
|
|
2873
|
+
descriptions={projectDescriptions}
|
|
2874
|
+
className="px-2 py-2 pl-3 text-sky-200/90"
|
|
2875
|
+
/>
|
|
2876
|
+
{row.slots.map((mins, i) => {
|
|
2877
|
+
const dateKey = addDaysYmd(
|
|
2878
|
+
row.weekStart,
|
|
2879
|
+
i,
|
|
2880
|
+
);
|
|
2881
|
+
return (
|
|
2882
|
+
<td
|
|
2883
|
+
key={dateKey}
|
|
2884
|
+
className="px-1 py-2 text-center tabular-nums text-zinc-200"
|
|
2885
|
+
title={dateKey}
|
|
2886
|
+
>
|
|
2887
|
+
{mins > 0
|
|
2888
|
+
? formatDuration(mins)
|
|
2889
|
+
: "—"}
|
|
2890
|
+
</td>
|
|
2891
|
+
);
|
|
2892
|
+
})}
|
|
2893
|
+
<td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
|
|
2894
|
+
{formatDuration(row.total)}
|
|
2895
|
+
</td>
|
|
2896
|
+
</tr>
|
|
2897
|
+
))}
|
|
2898
|
+
</tbody>
|
|
2899
|
+
</table>
|
|
2900
|
+
</div>
|
|
2901
|
+
</div>
|
|
2902
|
+
),
|
|
2903
|
+
)}
|
|
2904
|
+
</div>
|
|
2905
|
+
)}
|
|
2906
|
+
</section>
|
|
2907
|
+
</>
|
|
2908
|
+
)}
|
|
2909
|
+
</>
|
|
2910
|
+
)}
|
|
2911
|
+
</div>
|
|
2912
|
+
<ReportingPageTocDesktop
|
|
2913
|
+
title={t.tocTitle}
|
|
2914
|
+
ariaLabel={t.tocNavAria}
|
|
2915
|
+
entries={reportingTocEntries}
|
|
2916
|
+
/>
|
|
2917
|
+
</div>
|
|
2918
|
+
)}
|
|
2919
|
+
</div>
|
|
2920
|
+
<ScrollToTopFab ariaLabel={t.scrollToTopAria} />
|
|
2921
|
+
<ReportingTour
|
|
2922
|
+
open={reportingTourOpen}
|
|
2923
|
+
onOpenChange={setReportingTourOpen}
|
|
2924
|
+
dt={dt}
|
|
2925
|
+
hasReportingChartData={hasReportingChartData}
|
|
2926
|
+
/>
|
|
2927
|
+
</div>
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
export default function ReportingPage() {
|
|
2932
|
+
return (
|
|
2933
|
+
<Suspense
|
|
2934
|
+
fallback={
|
|
2935
|
+
<div className="min-h-screen bg-zinc-100 px-6 py-10 text-sm text-zinc-500 dark:bg-zinc-900">
|
|
2936
|
+
Kronosys…
|
|
2937
|
+
</div>
|
|
2938
|
+
}
|
|
2939
|
+
>
|
|
2940
|
+
<ReportingContent />
|
|
2941
|
+
</Suspense>
|
|
2942
|
+
);
|
|
2943
|
+
}
|