@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,1261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Timer, Play, Pause, Ticket, Zap, History } from "lucide-react";
|
|
5
|
+
import { TRACE_ISSUE_SEARCH_CLIENT_TIMEOUT_MS } from "@/lib/gitlabIssueSearch";
|
|
6
|
+
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
7
|
+
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
8
|
+
import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
|
|
9
|
+
import {
|
|
10
|
+
buildStartTaskFromDraft,
|
|
11
|
+
formatProjectDisplay,
|
|
12
|
+
mergeTagsForDisplay,
|
|
13
|
+
parseTaskWithAutoTags,
|
|
14
|
+
removeProjectFromDraft,
|
|
15
|
+
removeSavedTagFromDraft,
|
|
16
|
+
} from "@/lib/taskParsing";
|
|
17
|
+
import { TagPills } from "./TagPills";
|
|
18
|
+
import {
|
|
19
|
+
PROJECT_CHIP_APPLIED_CLASS,
|
|
20
|
+
TASK_FOCUS_LAUNCHER_INPUT_ROW_CLASS,
|
|
21
|
+
} from "./taskFieldStyles";
|
|
22
|
+
import { KronosysDatetimePopoverField } from "./KronosysDatetimePopoverField";
|
|
23
|
+
import { TaskSessionLiveCard } from "./TaskSessionLiveCard";
|
|
24
|
+
import { IssuePickerModal, type RemoteIssue } from "./IssuePickerModal";
|
|
25
|
+
import {
|
|
26
|
+
DashboardAlertModal,
|
|
27
|
+
DashboardConfirmModal,
|
|
28
|
+
DashboardTriActionModal,
|
|
29
|
+
} from "./DashboardSimpleModal";
|
|
30
|
+
import {
|
|
31
|
+
tbVioletIcon,
|
|
32
|
+
tbVioletToggleOff,
|
|
33
|
+
tbVioletToggleOn,
|
|
34
|
+
} from "@/lib/translucentButtonClasses";
|
|
35
|
+
import { InlineMetricHelpTrigger } from "./InlineMetricHelpTrigger";
|
|
36
|
+
import {
|
|
37
|
+
readConcurrentTaskStartPreference,
|
|
38
|
+
writeConcurrentTaskStartPreference,
|
|
39
|
+
type ConcurrentTaskStartPreference,
|
|
40
|
+
} from "@/lib/concurrentTaskStartPreference";
|
|
41
|
+
import { mergeLiveSessionIntoHistory } from "@/lib/sessionListMerge";
|
|
42
|
+
import { formatSessionEndReasonLine } from "@/lib/sessionEndReason";
|
|
43
|
+
import type { SessionListEntry } from "@/components/dashboard/SessionListPanel";
|
|
44
|
+
import { SessionEndReasonEditor } from "@/components/dashboard/SessionEndReasonEditor";
|
|
45
|
+
import { useDashboardToast } from "@/components/dashboard/DashboardToastProvider";
|
|
46
|
+
|
|
47
|
+
type TaskEntryMode = "realtime" | "past";
|
|
48
|
+
|
|
49
|
+
type TaskRow = {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
durationMs: number;
|
|
53
|
+
isDone: boolean;
|
|
54
|
+
startTime?: string;
|
|
55
|
+
endTime?: string;
|
|
56
|
+
tags?: string[];
|
|
57
|
+
project?: string | null;
|
|
58
|
+
subtasks?: Array<{
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
done: boolean;
|
|
62
|
+
durationMs?: number;
|
|
63
|
+
}>;
|
|
64
|
+
manualTaskTimerPaused?: boolean;
|
|
65
|
+
activeSubtaskTimerId?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type SessionShape = {
|
|
69
|
+
sessionId?: string;
|
|
70
|
+
sessionName?: string;
|
|
71
|
+
isPaused?: boolean;
|
|
72
|
+
/** Présent et non vide lorsque la session a été clôturée (instantané d’historique). */
|
|
73
|
+
endAt?: string | null;
|
|
74
|
+
sessionEndReasonKind?: string;
|
|
75
|
+
sessionEndReasonNote?: string;
|
|
76
|
+
activeTasks?: TaskRow[];
|
|
77
|
+
activeTask?: TaskRow | null;
|
|
78
|
+
tasks?: TaskRow[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function runningTasksFromSession(
|
|
82
|
+
session: SessionShape | null | undefined,
|
|
83
|
+
): TaskRow[] {
|
|
84
|
+
if (!session) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const raw =
|
|
88
|
+
Array.isArray(session.activeTasks) && session.activeTasks.length > 0
|
|
89
|
+
? session.activeTasks
|
|
90
|
+
: session.activeTask
|
|
91
|
+
? [session.activeTask]
|
|
92
|
+
: [];
|
|
93
|
+
return raw.filter((t) => t && !t.isDone && !t.manualTaskTimerPaused);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type LiveSessionShape = SessionShape & {
|
|
97
|
+
kronoFocus?: { status?: "idle" | "running" | "paused" };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export function TaskFocusPanel({
|
|
101
|
+
payload,
|
|
102
|
+
lang,
|
|
103
|
+
t,
|
|
104
|
+
refresh,
|
|
105
|
+
urlFocusedSessionId,
|
|
106
|
+
liveSessionId,
|
|
107
|
+
displayTimeZone,
|
|
108
|
+
use24HourClock,
|
|
109
|
+
onResumeActiveSession,
|
|
110
|
+
showKronoFocusInTaskOps = true,
|
|
111
|
+
allowTaskStartTimeEdit = true,
|
|
112
|
+
allowTaskEndTimeEdit = true,
|
|
113
|
+
}: {
|
|
114
|
+
payload: KronosysUpdatePayload;
|
|
115
|
+
lang: Lang;
|
|
116
|
+
t: DashboardStrings;
|
|
117
|
+
refresh: () => Promise<boolean | void>;
|
|
118
|
+
/** Onglet `?session=` : id affiché (live ou archive), prioritaire sur `inspectingSessionId`. */
|
|
119
|
+
urlFocusedSessionId?: string;
|
|
120
|
+
liveSessionId?: string;
|
|
121
|
+
displayTimeZone: string;
|
|
122
|
+
use24HourClock: boolean;
|
|
123
|
+
/** Retour à la session active (réinitialise l’URL si `?session=`). */
|
|
124
|
+
onResumeActiveSession?: () => Promise<void>;
|
|
125
|
+
/** Option `dashboardShowKronoFocusInTaskOps` : boutons Krono Focus dans la colonne tâches. */
|
|
126
|
+
showKronoFocusInTaskOps?: boolean;
|
|
127
|
+
/** Option `dashboardAllowTaskStartTimeEdit` : correction de l'heure de début des tâches. */
|
|
128
|
+
allowTaskStartTimeEdit?: boolean;
|
|
129
|
+
/** Option `dashboardAllowTaskEndTimeEdit` : correction de l'heure de fin des tâches terminées. */
|
|
130
|
+
allowTaskEndTimeEdit?: boolean;
|
|
131
|
+
}) {
|
|
132
|
+
const live = payload.current as SessionShape | undefined;
|
|
133
|
+
const history = useMemo(
|
|
134
|
+
() =>
|
|
135
|
+
mergeLiveSessionIntoHistory(
|
|
136
|
+
(payload.history || []) as SessionListEntry[],
|
|
137
|
+
live,
|
|
138
|
+
) as SessionShape[],
|
|
139
|
+
[payload.history, live],
|
|
140
|
+
);
|
|
141
|
+
const historyArchivedList = (payload.historyArchived || []) as SessionShape[];
|
|
142
|
+
const payloadInspect =
|
|
143
|
+
(payload.inspectingSessionId as string | null | undefined) ?? null;
|
|
144
|
+
const liveId = liveSessionId ?? live?.sessionId;
|
|
145
|
+
const hasLiveSession = typeof liveId === "string" && liveId.length > 0;
|
|
146
|
+
|
|
147
|
+
let archiveColumnId: string | null = null;
|
|
148
|
+
if (urlFocusedSessionId !== undefined) {
|
|
149
|
+
if (urlFocusedSessionId && urlFocusedSessionId !== liveId) {
|
|
150
|
+
archiveColumnId = urlFocusedSessionId;
|
|
151
|
+
}
|
|
152
|
+
} else if (payloadInspect) {
|
|
153
|
+
archiveColumnId = payloadInspect;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const knownTags = (payload.knownTags || []) as string[];
|
|
157
|
+
const knownProjects = (payload.knownProjects || []) as string[];
|
|
158
|
+
const viewingSession = archiveColumnId
|
|
159
|
+
? (history.find((s) => s.sessionId === archiveColumnId) ??
|
|
160
|
+
historyArchivedList.find((s) => s.sessionId === archiveColumnId) ??
|
|
161
|
+
null)
|
|
162
|
+
: null;
|
|
163
|
+
const sessionCurrent = viewingSession ?? live;
|
|
164
|
+
const isInspecting = !!viewingSession;
|
|
165
|
+
const inspectingId = archiveColumnId;
|
|
166
|
+
const isPauseState = !!live?.isPaused;
|
|
167
|
+
const inspectSessionEndReasonLine = viewingSession
|
|
168
|
+
? formatSessionEndReasonLine(
|
|
169
|
+
t,
|
|
170
|
+
viewingSession.sessionEndReasonKind,
|
|
171
|
+
viewingSession.sessionEndReasonNote,
|
|
172
|
+
)
|
|
173
|
+
: null;
|
|
174
|
+
/** Session passée **terminée** : on n’affiche que les entrées marquées terminées (pas de blocs en cours / en pause). */
|
|
175
|
+
const inspectingEndedSession =
|
|
176
|
+
isInspecting &&
|
|
177
|
+
typeof viewingSession?.endAt === "string" &&
|
|
178
|
+
viewingSession.endAt.trim() !== "";
|
|
179
|
+
const inspectingLiveRunning =
|
|
180
|
+
isInspecting &&
|
|
181
|
+
typeof archiveColumnId === "string" &&
|
|
182
|
+
typeof liveId === "string" &&
|
|
183
|
+
archiveColumnId === liveId &&
|
|
184
|
+
hasLiveSession &&
|
|
185
|
+
!inspectingEndedSession;
|
|
186
|
+
const sessionEndReasonEditSid =
|
|
187
|
+
typeof viewingSession?.sessionId === "string"
|
|
188
|
+
? viewingSession.sessionId.trim()
|
|
189
|
+
: "";
|
|
190
|
+
const canEditInspectSessionEndReason =
|
|
191
|
+
isInspecting && !inspectingLiveRunning && sessionEndReasonEditSid !== "";
|
|
192
|
+
|
|
193
|
+
const taskSessionForBuckets = viewingSession ?? live;
|
|
194
|
+
/** Tâches live au minuteur (hors archive). */
|
|
195
|
+
const runningTasks = !isInspecting ? runningTasksFromSession(live) : [];
|
|
196
|
+
const runningTaskIds = useMemo(
|
|
197
|
+
() => new Set(runningTasks.map((t) => String(t.id))),
|
|
198
|
+
[runningTasks],
|
|
199
|
+
);
|
|
200
|
+
/**
|
|
201
|
+
* `tasks` + pile `activeTasks` / `activeTask` (dédoublonné par id, la pile l’emporte).
|
|
202
|
+
* Sinon une tâche uniquement sur la pile — après pause concurrente — n’apparaît ni « en cours » ni « en pause ».
|
|
203
|
+
*/
|
|
204
|
+
const mergedTasksForBuckets = useMemo(() => {
|
|
205
|
+
const sess = taskSessionForBuckets;
|
|
206
|
+
if (!sess) {
|
|
207
|
+
return [] as TaskRow[];
|
|
208
|
+
}
|
|
209
|
+
const map = new Map<string, TaskRow>();
|
|
210
|
+
const base = Array.isArray(sess.tasks) ? (sess.tasks as TaskRow[]) : [];
|
|
211
|
+
for (const t of base) {
|
|
212
|
+
if (t?.id) {
|
|
213
|
+
map.set(String(t.id), t);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const stack: TaskRow[] =
|
|
217
|
+
Array.isArray(sess.activeTasks) && sess.activeTasks.length > 0
|
|
218
|
+
? [...(sess.activeTasks as TaskRow[])]
|
|
219
|
+
: sess.activeTask
|
|
220
|
+
? [sess.activeTask as TaskRow]
|
|
221
|
+
: [];
|
|
222
|
+
for (const t of stack) {
|
|
223
|
+
if (t?.id) {
|
|
224
|
+
map.set(String(t.id), t);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return [...map.values()];
|
|
228
|
+
}, [taskSessionForBuckets]);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Bandeau TRACKING : uniquement si la collecte n’est pas en pause session
|
|
232
|
+
* (sinon le minuteur de suivi n’est pas actif, mais l’interpolation locale faisait défiler l’horloge).
|
|
233
|
+
*/
|
|
234
|
+
const trackingActive =
|
|
235
|
+
!isInspecting && runningTasks.length > 0 && !isPauseState;
|
|
236
|
+
|
|
237
|
+
const { pausedTasks, completedTasks } = useMemo(() => {
|
|
238
|
+
const paused: TaskRow[] = [];
|
|
239
|
+
const completed: TaskRow[] = [];
|
|
240
|
+
for (const t of mergedTasksForBuckets) {
|
|
241
|
+
if (t.isDone) {
|
|
242
|
+
completed.push(t);
|
|
243
|
+
} else if (!inspectingEndedSession && !runningTaskIds.has(String(t.id))) {
|
|
244
|
+
paused.push(t);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { pausedTasks: paused, completedTasks: completed };
|
|
248
|
+
}, [mergedTasksForBuckets, inspectingEndedSession, runningTaskIds]);
|
|
249
|
+
const pausedTasksDisplay = [...pausedTasks].reverse();
|
|
250
|
+
const completedTasksDisplay = [...completedTasks].reverse();
|
|
251
|
+
const showPausedTaskSection =
|
|
252
|
+
!inspectingEndedSession && pausedTasksDisplay.length > 0;
|
|
253
|
+
const showCompletedTaskSection = completedTasksDisplay.length > 0;
|
|
254
|
+
const showTaskListBuckets =
|
|
255
|
+
(isInspecting || hasLiveSession) &&
|
|
256
|
+
(showPausedTaskSection || showCompletedTaskSection);
|
|
257
|
+
/** Pastille « total » seulement quand les deux colonnes sont visibles (évite le doublon avec un seul bloc). */
|
|
258
|
+
const showSessionTaskCountBadge =
|
|
259
|
+
!inspectingEndedSession &&
|
|
260
|
+
showPausedTaskSection &&
|
|
261
|
+
showCompletedTaskSection;
|
|
262
|
+
|
|
263
|
+
const kronoFocusStatus = (live as LiveSessionShape | undefined)?.kronoFocus
|
|
264
|
+
?.status;
|
|
265
|
+
const kronoFocusIsRunningOrPaused =
|
|
266
|
+
kronoFocusStatus === "running" || kronoFocusStatus === "paused";
|
|
267
|
+
|
|
268
|
+
const glCfg = payload.cfg as Record<string, unknown> | undefined;
|
|
269
|
+
/** Jeton configuré : afficher l’import ; l’API refuse la recherche sans jeton valide (et message si connexion non testée). */
|
|
270
|
+
const showRemoteIssueImport =
|
|
271
|
+
glCfg?.gitlabTokenStored === true || glCfg?.gitlabTokenFromEnv === true;
|
|
272
|
+
|
|
273
|
+
const [taskInput, setTaskInput] = useState("");
|
|
274
|
+
const [taskEntryMode, setTaskEntryMode] = useState<TaskEntryMode>("realtime");
|
|
275
|
+
const [pastStartLocal, setPastStartLocal] = useState("");
|
|
276
|
+
const [pastEndLocal, setPastEndLocal] = useState("");
|
|
277
|
+
const [startKronoFocusWithTask, setStartKronoFocusWithTask] = useState(false);
|
|
278
|
+
const [issuePickerOpen, setIssuePickerOpen] = useState(false);
|
|
279
|
+
const [alertMessage, setAlertMessage] = useState<string | null>(null);
|
|
280
|
+
const [kronoFocusResetConfirmOpen, setKronoFocusResetConfirmOpen] =
|
|
281
|
+
useState(false);
|
|
282
|
+
const [deleteTaskConfirmId, setDeleteTaskConfirmId] = useState<string | null>(
|
|
283
|
+
null,
|
|
284
|
+
);
|
|
285
|
+
const [concurrentStartModalOpen, setConcurrentStartModalOpen] =
|
|
286
|
+
useState(false);
|
|
287
|
+
const [rememberConcurrentStart, setRememberConcurrentStart] = useState(false);
|
|
288
|
+
const rememberConcurrentRef = useRef(false);
|
|
289
|
+
const pendingLiveStartRef = useRef<{
|
|
290
|
+
name: string;
|
|
291
|
+
tags: string[];
|
|
292
|
+
project?: string;
|
|
293
|
+
startKronoFocus: boolean;
|
|
294
|
+
} | null>(null);
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
rememberConcurrentRef.current = rememberConcurrentStart;
|
|
298
|
+
}, [rememberConcurrentStart]);
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!showKronoFocusInTaskOps) {
|
|
302
|
+
setStartKronoFocusWithTask(false);
|
|
303
|
+
}
|
|
304
|
+
}, [showKronoFocusInTaskOps]);
|
|
305
|
+
|
|
306
|
+
const post = useCallback(
|
|
307
|
+
async (body: Record<string, unknown>) => {
|
|
308
|
+
await postKronosysAction(body);
|
|
309
|
+
await refresh();
|
|
310
|
+
},
|
|
311
|
+
[refresh],
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const { pushToast } = useDashboardToast();
|
|
315
|
+
|
|
316
|
+
const startKronoFocusFromTask = useCallback(async () => {
|
|
317
|
+
const timer = (live as LiveSessionShape | undefined)?.kronoFocus;
|
|
318
|
+
const st = timer?.status;
|
|
319
|
+
if (st === "running" || st === "paused") {
|
|
320
|
+
setKronoFocusResetConfirmOpen(true);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
await post({ type: "startKronoFocus", linkToActiveTask: true });
|
|
324
|
+
}, [live, post]);
|
|
325
|
+
|
|
326
|
+
const executeKronoFocusResetAndStart = useCallback(async () => {
|
|
327
|
+
setKronoFocusResetConfirmOpen(false);
|
|
328
|
+
await post({ type: "resetKronoFocus" });
|
|
329
|
+
await post({ type: "startKronoFocus", linkToActiveTask: true });
|
|
330
|
+
}, [post]);
|
|
331
|
+
|
|
332
|
+
const titleParsed = parseTaskWithAutoTags(taskInput);
|
|
333
|
+
const mergedDraftTags = mergeTagsForDisplay(taskInput, []);
|
|
334
|
+
const mergedDraftProject = titleParsed.project;
|
|
335
|
+
|
|
336
|
+
const handleRemoveDraftTag = useCallback((tag: string) => {
|
|
337
|
+
setTaskInput((prev) => {
|
|
338
|
+
const regex = new RegExp(`(^|\\s)#${tag}(?=\\s|$)`, "gi");
|
|
339
|
+
return prev.replace(regex, " ").replace(/\s+/g, " ").trim();
|
|
340
|
+
});
|
|
341
|
+
}, []);
|
|
342
|
+
|
|
343
|
+
const handleRemoveDraftProject = useCallback(() => {
|
|
344
|
+
setTaskInput((prev) => {
|
|
345
|
+
const regex = /(^|\s)@[\w.-]+(?=\s|$)/gi;
|
|
346
|
+
return prev.replace(regex, " ").replace(/\s+/g, " ").trim();
|
|
347
|
+
});
|
|
348
|
+
}, []);
|
|
349
|
+
|
|
350
|
+
const confirmDeleteTask = useCallback((taskId: string) => {
|
|
351
|
+
setDeleteTaskConfirmId(taskId);
|
|
352
|
+
}, []);
|
|
353
|
+
|
|
354
|
+
const clearTaskEntryForm = useCallback(() => {
|
|
355
|
+
setTaskInput("");
|
|
356
|
+
setStartKronoFocusWithTask(false);
|
|
357
|
+
}, []);
|
|
358
|
+
|
|
359
|
+
const resolveConcurrentAndStartLive = useCallback(
|
|
360
|
+
async (mode: ConcurrentTaskStartPreference, persistPreference: boolean) => {
|
|
361
|
+
const draft = pendingLiveStartRef.current;
|
|
362
|
+
if (!draft || !live) {
|
|
363
|
+
setConcurrentStartModalOpen(false);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (persistPreference) {
|
|
367
|
+
writeConcurrentTaskStartPreference(mode);
|
|
368
|
+
}
|
|
369
|
+
const running = runningTasksFromSession(live);
|
|
370
|
+
try {
|
|
371
|
+
if (mode !== "parallel") {
|
|
372
|
+
for (const t of running) {
|
|
373
|
+
if (mode === "pause") {
|
|
374
|
+
await postKronosysAction({
|
|
375
|
+
type: "setTaskTimerPaused",
|
|
376
|
+
taskId: t.id,
|
|
377
|
+
paused: true,
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
await postKronosysAction({
|
|
381
|
+
type: "finishTask",
|
|
382
|
+
taskId: t.id,
|
|
383
|
+
shouldCommit: false,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
await post({
|
|
389
|
+
type: "startTask",
|
|
390
|
+
name: draft.name,
|
|
391
|
+
startKronoFocus: draft.startKronoFocus,
|
|
392
|
+
tags: draft.tags,
|
|
393
|
+
...(draft.project ? { project: draft.project } : {}),
|
|
394
|
+
});
|
|
395
|
+
} catch (error) {
|
|
396
|
+
pushToast(
|
|
397
|
+
typeof error === "string"
|
|
398
|
+
? error
|
|
399
|
+
: error instanceof Error
|
|
400
|
+
? error.message
|
|
401
|
+
: "Failed to resolve concurrent tasks",
|
|
402
|
+
);
|
|
403
|
+
pendingLiveStartRef.current = null;
|
|
404
|
+
setConcurrentStartModalOpen(false);
|
|
405
|
+
setRememberConcurrentStart(false);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
pendingLiveStartRef.current = null;
|
|
409
|
+
setConcurrentStartModalOpen(false);
|
|
410
|
+
setRememberConcurrentStart(false);
|
|
411
|
+
clearTaskEntryForm();
|
|
412
|
+
},
|
|
413
|
+
[live, post, clearTaskEntryForm, pushToast],
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const submitStartTask = useCallback(async () => {
|
|
417
|
+
const hasDraft = taskInput.trim().length > 0;
|
|
418
|
+
if (!hasDraft) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const payloadStart = buildStartTaskFromDraft(taskInput, [], undefined);
|
|
422
|
+
if (
|
|
423
|
+
!payloadStart.name.trim() &&
|
|
424
|
+
payloadStart.tags.length === 0 &&
|
|
425
|
+
!payloadStart.project
|
|
426
|
+
) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const body = {
|
|
430
|
+
name: payloadStart.name,
|
|
431
|
+
tags: payloadStart.tags,
|
|
432
|
+
...(payloadStart.project ? { project: payloadStart.project } : {}),
|
|
433
|
+
startKronoFocus: showKronoFocusInTaskOps && startKronoFocusWithTask,
|
|
434
|
+
};
|
|
435
|
+
if (!isInspecting && !hasLiveSession) {
|
|
436
|
+
pushToast(t.taskStartAutoSessionToast);
|
|
437
|
+
await post({ type: "newSession", sessionScope: undefined });
|
|
438
|
+
await post({
|
|
439
|
+
type: "startTask",
|
|
440
|
+
name: body.name,
|
|
441
|
+
startKronoFocus: body.startKronoFocus,
|
|
442
|
+
tags: body.tags,
|
|
443
|
+
...(body.project ? { project: body.project } : {}),
|
|
444
|
+
});
|
|
445
|
+
clearTaskEntryForm();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!isInspecting && hasLiveSession && live) {
|
|
449
|
+
const running = runningTasksFromSession(live);
|
|
450
|
+
if (running.length > 0) {
|
|
451
|
+
const pref = readConcurrentTaskStartPreference();
|
|
452
|
+
pendingLiveStartRef.current = body;
|
|
453
|
+
if (pref === "pause") {
|
|
454
|
+
void resolveConcurrentAndStartLive("pause", false);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (pref === "finish") {
|
|
458
|
+
void resolveConcurrentAndStartLive("finish", false);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (pref === "parallel") {
|
|
462
|
+
void resolveConcurrentAndStartLive("parallel", false);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
setRememberConcurrentStart(false);
|
|
466
|
+
setConcurrentStartModalOpen(true);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
await post({
|
|
471
|
+
type: "startTask",
|
|
472
|
+
name: body.name,
|
|
473
|
+
startKronoFocus: body.startKronoFocus,
|
|
474
|
+
tags: body.tags,
|
|
475
|
+
...(body.project ? { project: body.project } : {}),
|
|
476
|
+
});
|
|
477
|
+
clearTaskEntryForm();
|
|
478
|
+
}, [
|
|
479
|
+
clearTaskEntryForm,
|
|
480
|
+
hasLiveSession,
|
|
481
|
+
isInspecting,
|
|
482
|
+
live,
|
|
483
|
+
post,
|
|
484
|
+
pushToast,
|
|
485
|
+
resolveConcurrentAndStartLive,
|
|
486
|
+
showKronoFocusInTaskOps,
|
|
487
|
+
startKronoFocusWithTask,
|
|
488
|
+
t.taskStartAutoSessionToast,
|
|
489
|
+
taskInput,
|
|
490
|
+
]);
|
|
491
|
+
|
|
492
|
+
const cancelConcurrentStartModal = useCallback(() => {
|
|
493
|
+
pendingLiveStartRef.current = null;
|
|
494
|
+
setRememberConcurrentStart(false);
|
|
495
|
+
setConcurrentStartModalOpen(false);
|
|
496
|
+
}, []);
|
|
497
|
+
|
|
498
|
+
const retroTargetSessionId = inspectingId ?? live?.sessionId;
|
|
499
|
+
|
|
500
|
+
const submitAddHistoricalTask = useCallback(() => {
|
|
501
|
+
if (!retroTargetSessionId) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const nameRaw = taskInput;
|
|
505
|
+
const hasDraft = nameRaw.trim().length > 0;
|
|
506
|
+
if (!hasDraft) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const startMs = pastStartLocal.trim()
|
|
510
|
+
? new Date(pastStartLocal).getTime()
|
|
511
|
+
: NaN;
|
|
512
|
+
const endMs = pastEndLocal.trim() ? new Date(pastEndLocal).getTime() : NaN;
|
|
513
|
+
if (
|
|
514
|
+
!Number.isFinite(startMs) ||
|
|
515
|
+
!Number.isFinite(endMs) ||
|
|
516
|
+
endMs <= startMs
|
|
517
|
+
) {
|
|
518
|
+
setAlertMessage(t.archiveTaskDatetimeRangeInvalid);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const durationMs = Math.round(endMs - startMs);
|
|
522
|
+
const payloadStart = buildStartTaskFromDraft(nameRaw, [], undefined);
|
|
523
|
+
if (
|
|
524
|
+
!payloadStart.name.trim() &&
|
|
525
|
+
payloadStart.tags.length === 0 &&
|
|
526
|
+
!payloadStart.project
|
|
527
|
+
) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
void post({
|
|
531
|
+
type: "addHistoricalTask",
|
|
532
|
+
sessionId: retroTargetSessionId,
|
|
533
|
+
name: payloadStart.name,
|
|
534
|
+
tags: payloadStart.tags,
|
|
535
|
+
durationMs,
|
|
536
|
+
startTime: new Date(startMs).toISOString(),
|
|
537
|
+
endTime: new Date(endMs).toISOString(),
|
|
538
|
+
...(payloadStart.project ? { project: payloadStart.project } : {}),
|
|
539
|
+
});
|
|
540
|
+
setTaskInput("");
|
|
541
|
+
setPastStartLocal("");
|
|
542
|
+
setPastEndLocal("");
|
|
543
|
+
}, [
|
|
544
|
+
pastEndLocal,
|
|
545
|
+
pastStartLocal,
|
|
546
|
+
post,
|
|
547
|
+
retroTargetSessionId,
|
|
548
|
+
t.archiveTaskDatetimeRangeInvalid,
|
|
549
|
+
t.archiveTaskDatetimeRangeInvalid,
|
|
550
|
+
taskInput,
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
const pastRangeValid =
|
|
554
|
+
pastStartLocal.trim() !== "" &&
|
|
555
|
+
pastEndLocal.trim() !== "" &&
|
|
556
|
+
Number.isFinite(new Date(pastStartLocal).getTime()) &&
|
|
557
|
+
Number.isFinite(new Date(pastEndLocal).getTime()) &&
|
|
558
|
+
new Date(pastEndLocal).getTime() > new Date(pastStartLocal).getTime();
|
|
559
|
+
|
|
560
|
+
const canSubmitPast = !!taskInput.trim() && pastRangeValid;
|
|
561
|
+
|
|
562
|
+
/** Début + fin sur une seule ligne (scroll horizontal si besoin), contenu centré dans la zone. */
|
|
563
|
+
const pastDateTimeRow = (
|
|
564
|
+
<div className="flex min-w-0 max-w-full flex-nowrap items-center justify-center gap-x-2 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:thin] sm:gap-x-3 [&::-webkit-scrollbar]:h-1">
|
|
565
|
+
<label className="flex shrink-0 items-center gap-1.5">
|
|
566
|
+
<span className="whitespace-nowrap text-[0.6rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
567
|
+
{t.archiveTaskStartLabel}
|
|
568
|
+
</span>
|
|
569
|
+
<KronosysDatetimePopoverField
|
|
570
|
+
value={pastStartLocal}
|
|
571
|
+
onChange={setPastStartLocal}
|
|
572
|
+
aria-label={t.archiveTaskStartLabel}
|
|
573
|
+
lang={lang}
|
|
574
|
+
t={t}
|
|
575
|
+
/>
|
|
576
|
+
</label>
|
|
577
|
+
<label className="flex shrink-0 items-center gap-1.5">
|
|
578
|
+
<span className="whitespace-nowrap text-[0.6rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
579
|
+
{t.archiveTaskEndLabel}
|
|
580
|
+
</span>
|
|
581
|
+
<KronosysDatetimePopoverField
|
|
582
|
+
value={pastEndLocal}
|
|
583
|
+
onChange={setPastEndLocal}
|
|
584
|
+
aria-label={t.archiveTaskEndLabel}
|
|
585
|
+
lang={lang}
|
|
586
|
+
t={t}
|
|
587
|
+
/>
|
|
588
|
+
</label>
|
|
589
|
+
</div>
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const pastDatetimeRowInspecting = (
|
|
593
|
+
<div className="flex w-full min-w-0 justify-center">
|
|
594
|
+
<div className="kronosys-past-datetime-blink-twice min-w-0 max-w-full">
|
|
595
|
+
{pastDateTimeRow}
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const taskFormTagSection =
|
|
601
|
+
mergedDraftTags.length > 0 ? (
|
|
602
|
+
<div className="-mx-0.5 flex w-full min-w-0 flex-wrap items-start gap-x-2 gap-y-1.5 px-0.5 pb-0.5">
|
|
603
|
+
<TagPills
|
|
604
|
+
variant="applied"
|
|
605
|
+
className="flex min-w-0 max-w-full flex-wrap items-baseline gap-x-1 gap-y-0.5"
|
|
606
|
+
differentiateProjectScopedTags
|
|
607
|
+
tags={mergedDraftTags}
|
|
608
|
+
onRemove={handleRemoveDraftTag}
|
|
609
|
+
defaultTagBucketLabel={t.taskTagDefaultBucket}
|
|
610
|
+
/>
|
|
611
|
+
</div>
|
|
612
|
+
) : null;
|
|
613
|
+
|
|
614
|
+
const resumeLiveSession = async () => {
|
|
615
|
+
if (onResumeActiveSession) {
|
|
616
|
+
await onResumeActiveSession();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
await post({ type: "inspectSession", sessionId: null });
|
|
620
|
+
await post({ type: "setPaused", paused: false });
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const historyBannerTitle = isPauseState
|
|
624
|
+
? t.pausedNote
|
|
625
|
+
: t.viewingHistoryBannerTitle;
|
|
626
|
+
const viewingSessionLabel =
|
|
627
|
+
sessionCurrent?.sessionName?.trim() || sessionCurrent?.sessionId || "—";
|
|
628
|
+
|
|
629
|
+
const fetchGitlabIssues = useCallback(
|
|
630
|
+
async (query: string) => {
|
|
631
|
+
const glCfg = payload.cfg as Record<string, unknown> | undefined;
|
|
632
|
+
const base =
|
|
633
|
+
typeof glCfg?.gitlabApiBaseUrl === "string"
|
|
634
|
+
? glCfg.gitlabApiBaseUrl.trim()
|
|
635
|
+
: "";
|
|
636
|
+
try {
|
|
637
|
+
const res = await postKronosysAction(
|
|
638
|
+
{
|
|
639
|
+
type: "fetchRemoteIssues",
|
|
640
|
+
lang,
|
|
641
|
+
search: query,
|
|
642
|
+
gitlabApiBaseUrl: base,
|
|
643
|
+
},
|
|
644
|
+
{ signal: AbortSignal.timeout(TRACE_ISSUE_SEARCH_CLIENT_TIMEOUT_MS) },
|
|
645
|
+
);
|
|
646
|
+
const err = res.result?.remoteIssuesError;
|
|
647
|
+
if (err) {
|
|
648
|
+
return {
|
|
649
|
+
issues: [] as RemoteIssue[],
|
|
650
|
+
error: typeof err === "string" ? err : String(err),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const list = res.result?.remoteIssues;
|
|
654
|
+
return { issues: Array.isArray(list) ? (list as RemoteIssue[]) : [] };
|
|
655
|
+
} catch (e) {
|
|
656
|
+
const aborted =
|
|
657
|
+
e instanceof Error &&
|
|
658
|
+
(e.name === "AbortError" || e.name === "TimeoutError");
|
|
659
|
+
if (aborted) {
|
|
660
|
+
return {
|
|
661
|
+
issues: [] as RemoteIssue[],
|
|
662
|
+
error: t.issuePickerDashboardRequestTimeout,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
throw e;
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
[lang, payload.cfg, t],
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
const onOpenGitlabIssuePicker = useCallback(() => {
|
|
672
|
+
setIssuePickerOpen(true);
|
|
673
|
+
}, []);
|
|
674
|
+
|
|
675
|
+
const showTaskModeToggle = !!retroTargetSessionId;
|
|
676
|
+
const effectiveTaskEntryMode: TaskEntryMode = showTaskModeToggle
|
|
677
|
+
? taskEntryMode
|
|
678
|
+
: "realtime";
|
|
679
|
+
|
|
680
|
+
return (
|
|
681
|
+
<section id="kronosys-task-focus" className="scroll-mt-24">
|
|
682
|
+
{isInspecting && (
|
|
683
|
+
<section
|
|
684
|
+
className="mb-4 flex flex-col gap-3 rounded-lg border border-amber-800/55 bg-[#120d0a] px-4 py-3 sm:flex-row sm:items-center sm:justify-between"
|
|
685
|
+
aria-label={t.historyInspectBannerAria}
|
|
686
|
+
>
|
|
687
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
688
|
+
<Pause
|
|
689
|
+
className="mt-0.5 shrink-0 text-amber-400"
|
|
690
|
+
size={18}
|
|
691
|
+
aria-hidden
|
|
692
|
+
/>
|
|
693
|
+
<div className="min-w-0">
|
|
694
|
+
<strong className="block text-sm text-amber-200">
|
|
695
|
+
{historyBannerTitle}
|
|
696
|
+
</strong>
|
|
697
|
+
<div className="mt-1 break-all text-xs font-normal text-amber-300/75">
|
|
698
|
+
{t.inspectingLabel} {viewingSessionLabel}
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
<button
|
|
703
|
+
type="button"
|
|
704
|
+
className="shrink-0 rounded-lg border border-amber-800/60 bg-amber-950/50 px-3 py-1.5 text-sm font-medium text-amber-200 hover:bg-amber-950/70"
|
|
705
|
+
onClick={() => void resumeLiveSession()}
|
|
706
|
+
>
|
|
707
|
+
{t.resumeBtn}
|
|
708
|
+
</button>
|
|
709
|
+
</section>
|
|
710
|
+
)}
|
|
711
|
+
|
|
712
|
+
{isInspecting &&
|
|
713
|
+
(inspectSessionEndReasonLine || canEditInspectSessionEndReason) ? (
|
|
714
|
+
<div className="mb-4 rounded-lg border border-zinc-200 bg-zinc-50/90 px-4 py-2.5 text-left text-sm text-zinc-700 dark:border-zinc-700/80 dark:bg-zinc-900/40 dark:text-zinc-300">
|
|
715
|
+
<div className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
716
|
+
{t.selectedSessionEndReasonTitle}
|
|
717
|
+
</div>
|
|
718
|
+
{inspectSessionEndReasonLine ? (
|
|
719
|
+
<p className="mt-1 whitespace-pre-wrap leading-snug">
|
|
720
|
+
{inspectSessionEndReasonLine}
|
|
721
|
+
</p>
|
|
722
|
+
) : null}
|
|
723
|
+
{canEditInspectSessionEndReason ? (
|
|
724
|
+
<div
|
|
725
|
+
className={
|
|
726
|
+
inspectSessionEndReasonLine
|
|
727
|
+
? "mt-3 border-t border-zinc-200 pt-3 dark:border-zinc-600/80"
|
|
728
|
+
: "mt-2"
|
|
729
|
+
}
|
|
730
|
+
>
|
|
731
|
+
<SessionEndReasonEditor
|
|
732
|
+
t={t}
|
|
733
|
+
radioGroupName="kronosys-session-end-reason-edit-tasks"
|
|
734
|
+
sessionId={sessionEndReasonEditSid}
|
|
735
|
+
initialKind={viewingSession?.sessionEndReasonKind}
|
|
736
|
+
initialNote={viewingSession?.sessionEndReasonNote}
|
|
737
|
+
post={post}
|
|
738
|
+
/>
|
|
739
|
+
</div>
|
|
740
|
+
) : null}
|
|
741
|
+
</div>
|
|
742
|
+
) : null}
|
|
743
|
+
|
|
744
|
+
<div className="space-y-6">
|
|
745
|
+
<>
|
|
746
|
+
{!inspectingEndedSession ? (
|
|
747
|
+
<>
|
|
748
|
+
<div className="mb-5 grid min-h-8 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-2">
|
|
749
|
+
<span className="shrink-0 text-[0.7rem] font-semibold uppercase tracking-wide text-zinc-500">
|
|
750
|
+
{t.taskTrackerTitle}
|
|
751
|
+
</span>
|
|
752
|
+
<div className="flex min-w-0 justify-center justify-self-stretch px-0.5 sm:px-1">
|
|
753
|
+
{!isInspecting &&
|
|
754
|
+
showTaskModeToggle &&
|
|
755
|
+
effectiveTaskEntryMode === "past" ? (
|
|
756
|
+
<div className="kronosys-past-datetime-blink-twice min-w-0 max-w-full">
|
|
757
|
+
{pastDateTimeRow}
|
|
758
|
+
</div>
|
|
759
|
+
) : null}
|
|
760
|
+
</div>
|
|
761
|
+
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 sm:gap-3">
|
|
762
|
+
{!isInspecting && trackingActive ? (
|
|
763
|
+
<div
|
|
764
|
+
className="flex items-center gap-2 text-sm font-bold text-emerald-400"
|
|
765
|
+
aria-live="polite"
|
|
766
|
+
>
|
|
767
|
+
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-current" />
|
|
768
|
+
<span className="whitespace-nowrap">
|
|
769
|
+
{lang === "fr" ? "SUIVI" : "TRACKING"}
|
|
770
|
+
{runningTasks.length > 1 ? (
|
|
771
|
+
<span className="ml-1.5 font-mono text-xs font-semibold tabular-nums opacity-90">
|
|
772
|
+
({runningTasks.length})
|
|
773
|
+
</span>
|
|
774
|
+
) : null}
|
|
775
|
+
</span>
|
|
776
|
+
</div>
|
|
777
|
+
) : null}
|
|
778
|
+
{!isInspecting &&
|
|
779
|
+
showTaskModeToggle &&
|
|
780
|
+
effectiveTaskEntryMode === "past" ? (
|
|
781
|
+
<InlineMetricHelpTrigger
|
|
782
|
+
ariaLabel={t.archiveAddTaskIntroHelpAria}
|
|
783
|
+
body={t.archiveAddTaskIntro}
|
|
784
|
+
panelClassName="w-[min(calc(100vw-2rem),22rem)]"
|
|
785
|
+
/>
|
|
786
|
+
) : null}
|
|
787
|
+
{!isInspecting && showRemoteIssueImport ? (
|
|
788
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
789
|
+
<button
|
|
790
|
+
type="button"
|
|
791
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-zinc-300 text-zinc-500 hover:bg-zinc-200 hover:text-zinc-800 disabled:opacity-50 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
|
792
|
+
title={t.importGitIssue}
|
|
793
|
+
aria-label={t.importGitIssue}
|
|
794
|
+
onClick={() => onOpenGitlabIssuePicker()}
|
|
795
|
+
>
|
|
796
|
+
<Ticket size={15} />
|
|
797
|
+
</button>
|
|
798
|
+
</div>
|
|
799
|
+
) : null}
|
|
800
|
+
</div>
|
|
801
|
+
</div>
|
|
802
|
+
|
|
803
|
+
{isInspecting && inspectingId ? (
|
|
804
|
+
<div className="min-w-0 space-y-4">
|
|
805
|
+
<div className="flex justify-end">
|
|
806
|
+
<InlineMetricHelpTrigger
|
|
807
|
+
ariaLabel={t.archiveAddTaskIntroHelpAria}
|
|
808
|
+
body={t.archiveAddTaskIntro}
|
|
809
|
+
panelClassName="w-[min(calc(100vw-2rem),22rem)]"
|
|
810
|
+
/>
|
|
811
|
+
</div>
|
|
812
|
+
{pastDatetimeRowInspecting}
|
|
813
|
+
{mergedDraftProject?.trim() ? (
|
|
814
|
+
<div className="flex min-w-0 justify-start">
|
|
815
|
+
<span
|
|
816
|
+
className={`${PROJECT_CHIP_APPLIED_CLASS} max-w-full truncate`}
|
|
817
|
+
title={formatProjectDisplay(mergedDraftProject)}
|
|
818
|
+
>
|
|
819
|
+
{formatProjectDisplay(mergedDraftProject)}
|
|
820
|
+
</span>
|
|
821
|
+
</div>
|
|
822
|
+
) : null}
|
|
823
|
+
<div className="flex min-h-10 min-w-0 flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end">
|
|
824
|
+
<input
|
|
825
|
+
className={`${TASK_FOCUS_LAUNCHER_INPUT_ROW_CLASS} min-w-0 text-center sm:min-w-[12rem]`}
|
|
826
|
+
placeholder={t.taskPlaceholderPast}
|
|
827
|
+
value={taskInput}
|
|
828
|
+
onChange={(e) => setTaskInput(e.target.value)}
|
|
829
|
+
onKeyDown={(e) => {
|
|
830
|
+
if (e.key === "Enter" && canSubmitPast) {
|
|
831
|
+
e.preventDefault();
|
|
832
|
+
void submitAddHistoricalTask();
|
|
833
|
+
}
|
|
834
|
+
}}
|
|
835
|
+
aria-label={t.taskPlaceholderPast}
|
|
836
|
+
/>
|
|
837
|
+
<div className="flex h-10 shrink-0 items-center gap-1">
|
|
838
|
+
{showRemoteIssueImport ? (
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-zinc-300 text-zinc-500 hover:bg-zinc-200 hover:text-zinc-800 disabled:opacity-50 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
|
842
|
+
title={t.importGitIssue}
|
|
843
|
+
aria-label={t.importGitIssue}
|
|
844
|
+
onClick={() => onOpenGitlabIssuePicker()}
|
|
845
|
+
>
|
|
846
|
+
<Ticket size={15} />
|
|
847
|
+
</button>
|
|
848
|
+
) : null}
|
|
849
|
+
<button
|
|
850
|
+
type="button"
|
|
851
|
+
className={tbVioletIcon}
|
|
852
|
+
disabled={!canSubmitPast}
|
|
853
|
+
title={t.archiveAddTaskBtn}
|
|
854
|
+
aria-label={t.archiveAddTaskBtn}
|
|
855
|
+
onClick={() => void submitAddHistoricalTask()}
|
|
856
|
+
>
|
|
857
|
+
<Play
|
|
858
|
+
size={22}
|
|
859
|
+
className="ml-0.5"
|
|
860
|
+
fill="currentColor"
|
|
861
|
+
aria-hidden
|
|
862
|
+
/>
|
|
863
|
+
</button>
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
{taskFormTagSection}
|
|
867
|
+
</div>
|
|
868
|
+
) : (
|
|
869
|
+
<div className="min-w-0 space-y-4">
|
|
870
|
+
{mergedDraftProject?.trim() ? (
|
|
871
|
+
<div className="flex min-w-0 justify-start">
|
|
872
|
+
<span
|
|
873
|
+
className={`${PROJECT_CHIP_APPLIED_CLASS} inline-flex items-center gap-1 max-w-full truncate pr-1`}
|
|
874
|
+
title={formatProjectDisplay(mergedDraftProject)}
|
|
875
|
+
>
|
|
876
|
+
<span className="truncate">
|
|
877
|
+
{formatProjectDisplay(mergedDraftProject)}
|
|
878
|
+
</span>
|
|
879
|
+
<button
|
|
880
|
+
type="button"
|
|
881
|
+
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full hover:bg-black/10 dark:hover:bg-white/10"
|
|
882
|
+
onClick={handleRemoveDraftProject}
|
|
883
|
+
>
|
|
884
|
+
<span className="text-[0.65rem] font-bold leading-none">
|
|
885
|
+
×
|
|
886
|
+
</span>
|
|
887
|
+
</button>
|
|
888
|
+
</span>
|
|
889
|
+
</div>
|
|
890
|
+
) : null}
|
|
891
|
+
<div className="flex min-h-10 min-w-0 items-center gap-2">
|
|
892
|
+
{showTaskModeToggle ? (
|
|
893
|
+
<div
|
|
894
|
+
role="group"
|
|
895
|
+
aria-label={t.taskEntryModeGroupAria}
|
|
896
|
+
className="flex h-10 shrink-0 items-center gap-1"
|
|
897
|
+
>
|
|
898
|
+
<button
|
|
899
|
+
type="button"
|
|
900
|
+
className={
|
|
901
|
+
effectiveTaskEntryMode === "realtime"
|
|
902
|
+
? tbVioletToggleOn
|
|
903
|
+
: tbVioletToggleOff
|
|
904
|
+
}
|
|
905
|
+
aria-pressed={effectiveTaskEntryMode === "realtime"}
|
|
906
|
+
title={t.taskEntryModeRealtime}
|
|
907
|
+
aria-label={t.taskEntryModeRealtime}
|
|
908
|
+
onClick={() => setTaskEntryMode("realtime")}
|
|
909
|
+
>
|
|
910
|
+
<Zap size={18} strokeWidth={2.25} aria-hidden />
|
|
911
|
+
</button>
|
|
912
|
+
<button
|
|
913
|
+
type="button"
|
|
914
|
+
className={
|
|
915
|
+
effectiveTaskEntryMode === "past"
|
|
916
|
+
? tbVioletToggleOn
|
|
917
|
+
: tbVioletToggleOff
|
|
918
|
+
}
|
|
919
|
+
aria-pressed={effectiveTaskEntryMode === "past"}
|
|
920
|
+
title={t.taskEntryModePast}
|
|
921
|
+
aria-label={t.taskEntryModePast}
|
|
922
|
+
onClick={() => setTaskEntryMode("past")}
|
|
923
|
+
>
|
|
924
|
+
<History size={18} strokeWidth={2} aria-hidden />
|
|
925
|
+
</button>
|
|
926
|
+
</div>
|
|
927
|
+
) : null}
|
|
928
|
+
<input
|
|
929
|
+
className={`${TASK_FOCUS_LAUNCHER_INPUT_ROW_CLASS} min-w-0 flex-1 text-center`}
|
|
930
|
+
placeholder={
|
|
931
|
+
effectiveTaskEntryMode === "past"
|
|
932
|
+
? t.taskPlaceholderPast
|
|
933
|
+
: t.taskPlaceholder
|
|
934
|
+
}
|
|
935
|
+
value={taskInput}
|
|
936
|
+
onChange={(e) => setTaskInput(e.target.value)}
|
|
937
|
+
onKeyDown={(e) => {
|
|
938
|
+
if (e.key !== "Enter") {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (
|
|
942
|
+
effectiveTaskEntryMode === "realtime" &&
|
|
943
|
+
taskInput.trim()
|
|
944
|
+
) {
|
|
945
|
+
e.preventDefault();
|
|
946
|
+
void submitStartTask();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (
|
|
950
|
+
effectiveTaskEntryMode === "past" &&
|
|
951
|
+
canSubmitPast &&
|
|
952
|
+
retroTargetSessionId
|
|
953
|
+
) {
|
|
954
|
+
e.preventDefault();
|
|
955
|
+
void submitAddHistoricalTask();
|
|
956
|
+
}
|
|
957
|
+
}}
|
|
958
|
+
aria-label={
|
|
959
|
+
effectiveTaskEntryMode === "past"
|
|
960
|
+
? t.taskPlaceholderPast
|
|
961
|
+
: t.taskPlaceholder
|
|
962
|
+
}
|
|
963
|
+
/>
|
|
964
|
+
<div className="flex h-10 shrink-0 items-center gap-1">
|
|
965
|
+
{effectiveTaskEntryMode === "realtime" ? (
|
|
966
|
+
<>
|
|
967
|
+
{showKronoFocusInTaskOps ? (
|
|
968
|
+
<button
|
|
969
|
+
type="button"
|
|
970
|
+
role="switch"
|
|
971
|
+
aria-checked={
|
|
972
|
+
startKronoFocusWithTask ? "true" : "false"
|
|
973
|
+
}
|
|
974
|
+
aria-label={
|
|
975
|
+
startKronoFocusWithTask
|
|
976
|
+
? t.startTaskWithKronoFocusToggleAriaOn
|
|
977
|
+
: t.startTaskWithKronoFocusToggleAriaOff
|
|
978
|
+
}
|
|
979
|
+
title={t.startTaskWithKronoFocus}
|
|
980
|
+
onClick={() =>
|
|
981
|
+
setStartKronoFocusWithTask((v) => !v)
|
|
982
|
+
}
|
|
983
|
+
className={`inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg transition-colors [&_svg]:shrink-0 ${
|
|
984
|
+
startKronoFocusWithTask
|
|
985
|
+
? "border-2 border-violet-500/65 bg-violet-500/12 text-violet-900 shadow-none hover:border-violet-500/80 hover:bg-violet-500/18 dark:border-violet-400/55 dark:bg-violet-600/22 dark:text-violet-100 dark:hover:border-violet-400/70 dark:hover:bg-violet-600/30"
|
|
986
|
+
: "border border-zinc-300 bg-white/80 text-zinc-500 dark:border-zinc-700 dark:bg-zinc-950/50 dark:text-zinc-400"
|
|
987
|
+
} hover:bg-zinc-100/90 dark:hover:bg-zinc-800/40`}
|
|
988
|
+
>
|
|
989
|
+
<Timer size={20} strokeWidth={2} aria-hidden />
|
|
990
|
+
</button>
|
|
991
|
+
) : null}
|
|
992
|
+
<button
|
|
993
|
+
type="button"
|
|
994
|
+
className={tbVioletIcon}
|
|
995
|
+
disabled={!taskInput.trim()}
|
|
996
|
+
title={t.startTaskBtn}
|
|
997
|
+
aria-label={t.startTaskBtn}
|
|
998
|
+
onClick={() => void submitStartTask()}
|
|
999
|
+
>
|
|
1000
|
+
<Play
|
|
1001
|
+
size={22}
|
|
1002
|
+
className="ml-0.5"
|
|
1003
|
+
fill="currentColor"
|
|
1004
|
+
aria-hidden
|
|
1005
|
+
/>
|
|
1006
|
+
</button>
|
|
1007
|
+
</>
|
|
1008
|
+
) : (
|
|
1009
|
+
<button
|
|
1010
|
+
type="button"
|
|
1011
|
+
className={tbVioletIcon}
|
|
1012
|
+
disabled={!canSubmitPast || !retroTargetSessionId}
|
|
1013
|
+
title={t.archiveAddTaskBtn}
|
|
1014
|
+
aria-label={t.archiveAddTaskBtn}
|
|
1015
|
+
onClick={() => void submitAddHistoricalTask()}
|
|
1016
|
+
>
|
|
1017
|
+
<Play
|
|
1018
|
+
size={22}
|
|
1019
|
+
className="ml-0.5"
|
|
1020
|
+
fill="currentColor"
|
|
1021
|
+
aria-hidden
|
|
1022
|
+
/>
|
|
1023
|
+
</button>
|
|
1024
|
+
)}
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
{taskFormTagSection}
|
|
1028
|
+
</div>
|
|
1029
|
+
)}
|
|
1030
|
+
</>
|
|
1031
|
+
) : null}
|
|
1032
|
+
|
|
1033
|
+
{!isInspecting && runningTasks.length > 0 ? (
|
|
1034
|
+
<div className="min-w-0 pt-2">
|
|
1035
|
+
<div className="mb-3 flex items-baseline justify-between gap-3">
|
|
1036
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
|
1037
|
+
{t.tasksRunningHeading}
|
|
1038
|
+
</h3>
|
|
1039
|
+
<span className="tabular-nums text-[0.7rem] font-medium text-emerald-600 dark:text-emerald-400/90">
|
|
1040
|
+
{runningTasks.length}
|
|
1041
|
+
</span>
|
|
1042
|
+
</div>
|
|
1043
|
+
<div className="divide-y divide-zinc-200/80 dark:divide-zinc-700/80">
|
|
1044
|
+
{runningTasks.map((task) => (
|
|
1045
|
+
<TaskSessionLiveCard
|
|
1046
|
+
key={task.id}
|
|
1047
|
+
variant="plain"
|
|
1048
|
+
task={task}
|
|
1049
|
+
lang={lang}
|
|
1050
|
+
displayTimeZone={displayTimeZone}
|
|
1051
|
+
use24HourClock={use24HourClock}
|
|
1052
|
+
isInspecting={isInspecting}
|
|
1053
|
+
inspectingId={inspectingId}
|
|
1054
|
+
knownTags={knownTags}
|
|
1055
|
+
knownProjects={knownProjects}
|
|
1056
|
+
post={post}
|
|
1057
|
+
t={t}
|
|
1058
|
+
confirmDeleteTask={confirmDeleteTask}
|
|
1059
|
+
kronoFocusIsRunningOrPaused={kronoFocusIsRunningOrPaused}
|
|
1060
|
+
showKronoFocusTaskActions={showKronoFocusInTaskOps}
|
|
1061
|
+
startKronoFocusFromTask={() =>
|
|
1062
|
+
void startKronoFocusFromTask()
|
|
1063
|
+
}
|
|
1064
|
+
pausePlayMode="pause"
|
|
1065
|
+
onPausePlay={() =>
|
|
1066
|
+
void post({
|
|
1067
|
+
type: "setTaskTimerPaused",
|
|
1068
|
+
taskId: task.id,
|
|
1069
|
+
paused: true,
|
|
1070
|
+
})
|
|
1071
|
+
}
|
|
1072
|
+
anchorId={`kronosys-active-task-${task.id}`}
|
|
1073
|
+
allowTaskStartTimeEdit={allowTaskStartTimeEdit}
|
|
1074
|
+
allowTaskEndTimeEdit={allowTaskEndTimeEdit}
|
|
1075
|
+
/>
|
|
1076
|
+
))}
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
) : null}
|
|
1080
|
+
</>
|
|
1081
|
+
</div>
|
|
1082
|
+
|
|
1083
|
+
{showTaskListBuckets ? (
|
|
1084
|
+
<div className="mt-8 space-y-10">
|
|
1085
|
+
{showSessionTaskCountBadge ? (
|
|
1086
|
+
<div className="flex justify-end">
|
|
1087
|
+
<span className="rounded-full border border-zinc-600 px-2 py-0.5 text-xs text-zinc-400">
|
|
1088
|
+
{mergedTasksForBuckets.length}
|
|
1089
|
+
</span>
|
|
1090
|
+
</div>
|
|
1091
|
+
) : null}
|
|
1092
|
+
|
|
1093
|
+
{showPausedTaskSection ? (
|
|
1094
|
+
<div className="space-y-2">
|
|
1095
|
+
<div className="flex items-center justify-between gap-2">
|
|
1096
|
+
<h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
|
1097
|
+
{t.tasksPausedHeading}
|
|
1098
|
+
</h4>
|
|
1099
|
+
<span className="rounded-full border border-zinc-600/80 px-2 py-0.5 text-[0.65rem] text-zinc-500">
|
|
1100
|
+
{pausedTasksDisplay.length}
|
|
1101
|
+
</span>
|
|
1102
|
+
</div>
|
|
1103
|
+
<div className="space-y-3">
|
|
1104
|
+
{pausedTasksDisplay.map((task) => (
|
|
1105
|
+
<TaskSessionLiveCard
|
|
1106
|
+
key={task.id}
|
|
1107
|
+
task={task}
|
|
1108
|
+
lang={lang}
|
|
1109
|
+
displayTimeZone={displayTimeZone}
|
|
1110
|
+
use24HourClock={use24HourClock}
|
|
1111
|
+
isInspecting={isInspecting}
|
|
1112
|
+
inspectingId={inspectingId}
|
|
1113
|
+
knownTags={knownTags}
|
|
1114
|
+
knownProjects={knownProjects}
|
|
1115
|
+
post={post}
|
|
1116
|
+
t={t}
|
|
1117
|
+
confirmDeleteTask={confirmDeleteTask}
|
|
1118
|
+
kronoFocusIsRunningOrPaused={kronoFocusIsRunningOrPaused}
|
|
1119
|
+
showKronoFocusTaskActions={showKronoFocusInTaskOps}
|
|
1120
|
+
startKronoFocusFromTask={() =>
|
|
1121
|
+
void startKronoFocusFromTask()
|
|
1122
|
+
}
|
|
1123
|
+
pausePlayMode="resume"
|
|
1124
|
+
onPausePlay={() =>
|
|
1125
|
+
void post({ type: "resumePausedTask", taskId: task.id })
|
|
1126
|
+
}
|
|
1127
|
+
anchorId={`kronosys-task-${task.id}`}
|
|
1128
|
+
allowTaskStartTimeEdit={allowTaskStartTimeEdit}
|
|
1129
|
+
allowTaskEndTimeEdit={allowTaskEndTimeEdit}
|
|
1130
|
+
/>
|
|
1131
|
+
))}
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
) : null}
|
|
1135
|
+
|
|
1136
|
+
{showCompletedTaskSection ? (
|
|
1137
|
+
<div className="space-y-2">
|
|
1138
|
+
<div className="flex items-center justify-between gap-2">
|
|
1139
|
+
<h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
|
1140
|
+
{t.tasksCompletedHeading}
|
|
1141
|
+
</h4>
|
|
1142
|
+
<span className="rounded-full border border-zinc-600/80 px-2 py-0.5 text-[0.65rem] text-zinc-500">
|
|
1143
|
+
{completedTasksDisplay.length}
|
|
1144
|
+
</span>
|
|
1145
|
+
</div>
|
|
1146
|
+
<div className="space-y-3">
|
|
1147
|
+
{completedTasksDisplay.map((task) => (
|
|
1148
|
+
<TaskSessionLiveCard
|
|
1149
|
+
key={task.id}
|
|
1150
|
+
task={task}
|
|
1151
|
+
lang={lang}
|
|
1152
|
+
displayTimeZone={displayTimeZone}
|
|
1153
|
+
use24HourClock={use24HourClock}
|
|
1154
|
+
isInspecting={isInspecting}
|
|
1155
|
+
inspectingId={inspectingId}
|
|
1156
|
+
knownTags={knownTags}
|
|
1157
|
+
knownProjects={knownProjects}
|
|
1158
|
+
post={post}
|
|
1159
|
+
t={t}
|
|
1160
|
+
confirmDeleteTask={confirmDeleteTask}
|
|
1161
|
+
kronoFocusIsRunningOrPaused={kronoFocusIsRunningOrPaused}
|
|
1162
|
+
showKronoFocusTaskActions={showKronoFocusInTaskOps}
|
|
1163
|
+
startKronoFocusFromTask={() =>
|
|
1164
|
+
void startKronoFocusFromTask()
|
|
1165
|
+
}
|
|
1166
|
+
pausePlayMode={null}
|
|
1167
|
+
onPausePlay={() => {}}
|
|
1168
|
+
anchorId={`kronosys-task-${task.id}`}
|
|
1169
|
+
allowTaskStartTimeEdit={allowTaskStartTimeEdit}
|
|
1170
|
+
allowTaskEndTimeEdit={allowTaskEndTimeEdit}
|
|
1171
|
+
/>
|
|
1172
|
+
))}
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
) : null}
|
|
1176
|
+
</div>
|
|
1177
|
+
) : null}
|
|
1178
|
+
|
|
1179
|
+
{issuePickerOpen ? (
|
|
1180
|
+
<IssuePickerModal
|
|
1181
|
+
t={t}
|
|
1182
|
+
fetchIssues={fetchGitlabIssues}
|
|
1183
|
+
onClose={() => setIssuePickerOpen(false)}
|
|
1184
|
+
onSelect={(issue) => {
|
|
1185
|
+
const title = String(issue.title ?? "").trimEnd();
|
|
1186
|
+
const suffix = `#${issue.number}`;
|
|
1187
|
+
setTaskInput(title ? `${title}${suffix}` : suffix);
|
|
1188
|
+
setIssuePickerOpen(false);
|
|
1189
|
+
}}
|
|
1190
|
+
/>
|
|
1191
|
+
) : null}
|
|
1192
|
+
<DashboardAlertModal
|
|
1193
|
+
open={alertMessage !== null}
|
|
1194
|
+
message={alertMessage ?? ""}
|
|
1195
|
+
okLabel={t.dialogOkBtn}
|
|
1196
|
+
onClose={() => setAlertMessage(null)}
|
|
1197
|
+
/>
|
|
1198
|
+
<DashboardConfirmModal
|
|
1199
|
+
open={kronoFocusResetConfirmOpen}
|
|
1200
|
+
message={t.startKronoFocusFromTaskConfirm}
|
|
1201
|
+
cancelLabel={t.dialogCancelBtn}
|
|
1202
|
+
confirmLabel={t.dialogConfirmBtn}
|
|
1203
|
+
onCancel={() => setKronoFocusResetConfirmOpen(false)}
|
|
1204
|
+
onConfirm={executeKronoFocusResetAndStart}
|
|
1205
|
+
/>
|
|
1206
|
+
<DashboardConfirmModal
|
|
1207
|
+
open={deleteTaskConfirmId !== null}
|
|
1208
|
+
message={t.taskDeleteConfirm}
|
|
1209
|
+
cancelLabel={t.dialogCancelBtn}
|
|
1210
|
+
confirmLabel={t.dialogConfirmBtn}
|
|
1211
|
+
confirmVariant="danger"
|
|
1212
|
+
onCancel={() => setDeleteTaskConfirmId(null)}
|
|
1213
|
+
onConfirm={() => {
|
|
1214
|
+
const id = deleteTaskConfirmId;
|
|
1215
|
+
setDeleteTaskConfirmId(null);
|
|
1216
|
+
if (id) {
|
|
1217
|
+
void post({
|
|
1218
|
+
type: "deleteTask",
|
|
1219
|
+
taskId: id,
|
|
1220
|
+
sessionId: inspectingId,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}}
|
|
1224
|
+
/>
|
|
1225
|
+
<DashboardTriActionModal
|
|
1226
|
+
open={concurrentStartModalOpen}
|
|
1227
|
+
title={t.taskConcurrentTrackingConflictTitle}
|
|
1228
|
+
message={t.taskConcurrentTrackingConflictMessage}
|
|
1229
|
+
dismissLabel={t.dialogCancelBtn}
|
|
1230
|
+
tertiaryLabel={t.taskConcurrentTrackingConflictParallelBtn}
|
|
1231
|
+
secondaryLabel={t.taskConcurrentTrackingConflictPauseBtn}
|
|
1232
|
+
primaryLabel={t.taskConcurrentTrackingConflictFinishBtn}
|
|
1233
|
+
primaryVariant="danger"
|
|
1234
|
+
dismissCheckbox={{
|
|
1235
|
+
label: t.taskConcurrentTrackingDontShowAgain,
|
|
1236
|
+
checked: rememberConcurrentStart,
|
|
1237
|
+
onChange: setRememberConcurrentStart,
|
|
1238
|
+
}}
|
|
1239
|
+
onDismiss={cancelConcurrentStartModal}
|
|
1240
|
+
onTertiary={() =>
|
|
1241
|
+
void resolveConcurrentAndStartLive(
|
|
1242
|
+
"parallel",
|
|
1243
|
+
rememberConcurrentRef.current,
|
|
1244
|
+
)
|
|
1245
|
+
}
|
|
1246
|
+
onSecondary={() =>
|
|
1247
|
+
void resolveConcurrentAndStartLive(
|
|
1248
|
+
"pause",
|
|
1249
|
+
rememberConcurrentRef.current,
|
|
1250
|
+
)
|
|
1251
|
+
}
|
|
1252
|
+
onPrimary={() =>
|
|
1253
|
+
void resolveConcurrentAndStartLive(
|
|
1254
|
+
"finish",
|
|
1255
|
+
rememberConcurrentRef.current,
|
|
1256
|
+
)
|
|
1257
|
+
}
|
|
1258
|
+
/>
|
|
1259
|
+
</section>
|
|
1260
|
+
);
|
|
1261
|
+
}
|