@nightkatana/kronosys-app 1.0.0-beta.2 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/app/api/action/route.ts +39 -3
- package/app/api/action-logs/route.ts +24 -0
- package/app/api/backup/route.ts +1 -1
- package/app/api/restore/route.ts +145 -0
- package/app/changelog/page.tsx +71 -4
- package/app/globals.css +127 -0
- package/app/guide/page.tsx +61 -15
- package/app/implementation/page.tsx +700 -0
- package/app/layout.tsx +14 -3
- package/app/licenses/page.tsx +99 -37
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +784 -229
- package/app/reporting/page.tsx +1266 -474
- package/app/settings/page.tsx +252 -18
- package/bin/kronosys.mjs +140 -15
- package/components/KronosysPayloadProvider.tsx +2 -0
- package/components/RouteTransition.tsx +18 -0
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
- package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +323 -48
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +211 -20
- package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
- package/components/dashboard/ReportingTour.tsx +87 -21
- package/components/dashboard/SavedProjectPicker.tsx +16 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
- package/components/dashboard/SessionListPanel.tsx +327 -44
- package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +1081 -478
- package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
- package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
- package/components/dashboard/taskFieldStyles.ts +20 -4
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +494 -84
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +177 -6
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +1188 -0
- package/lib/kronosysApi.ts +6 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +517 -75
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +37 -3
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +182 -21
- package/lib/settingsCopy.ts +178 -4
- package/lib/taskParsing.ts +360 -103
- package/lib/taskTemplateDraft.ts +135 -0
- package/lib/taskTimelineGantt.ts +265 -0
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +121 -47
- package/next.config.ts +7 -0
- package/package.json +12 -24
- package/server/actionDispatch.ts +1000 -77
- package/server/actionTaskSession.ts +337 -24
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/defaultCfg.ts +5 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +38 -2
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +66 -3
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/tsconfig.json +2 -1
- package/components/dashboard/IssuePickerModal.tsx +0 -168
- package/components/dashboard/ThemeToggle.test.tsx +0 -26
- package/lib/backupCsvExport.test.ts +0 -149
- package/lib/dashboardQuickSearchQuery.test.ts +0 -63
- package/lib/dataDir.test.ts +0 -87
- package/lib/formatIsoShort.test.ts +0 -46
- package/lib/kronoFocusRhythm.test.ts +0 -130
- package/lib/kronoFocusTimerUrgency.test.ts +0 -74
- package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
- package/lib/reportingAggregate.test.ts +0 -325
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -141
- package/lib/reportingWeekLayout.test.ts +0 -239
- package/lib/sessionAssiduity.test.ts +0 -25
- package/lib/sessionEndWarnings.test.ts +0 -200
- package/lib/sessionListMerge.test.ts +0 -101
- package/lib/sessionTaskSidebarStats.test.ts +0 -24
- package/lib/taskParsing.test.ts +0 -153
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -723
- package/server/actionTaskSession.test.ts +0 -713
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -65
- package/server/payloadStore.test.ts +0 -78
- package/server/sessionWallHydrate.test.ts +0 -46
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type MutableRefObject,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Timer, Play, Pause, Zap, History } from "lucide-react";
|
|
6
12
|
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
7
13
|
import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
|
|
8
14
|
import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
|
|
@@ -10,18 +16,23 @@ import {
|
|
|
10
16
|
buildStartTaskFromDraft,
|
|
11
17
|
formatProjectDisplay,
|
|
12
18
|
mergeTagsForDisplay,
|
|
19
|
+
normalizeProjectKey,
|
|
20
|
+
normalizeTagKey,
|
|
13
21
|
parseTaskWithAutoTags,
|
|
14
|
-
|
|
15
|
-
removeSavedTagFromDraft,
|
|
22
|
+
taskTitleForDisplay,
|
|
16
23
|
} from "@/lib/taskParsing";
|
|
24
|
+
import { isTaskDisplayPlanned } from "@/lib/temporalDisplayPlanned";
|
|
17
25
|
import { TagPills } from "./TagPills";
|
|
18
26
|
import {
|
|
19
27
|
PROJECT_CHIP_APPLIED_CLASS,
|
|
28
|
+
PROJECT_CHIP_APPLIED_PERSONAL_CLASS,
|
|
20
29
|
TASK_FOCUS_LAUNCHER_INPUT_ROW_CLASS,
|
|
21
30
|
} from "./taskFieldStyles";
|
|
22
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
KronosysDatetimePopoverField,
|
|
33
|
+
formatDatetimeLocalValue,
|
|
34
|
+
} from "./KronosysDatetimePopoverField";
|
|
23
35
|
import { TaskSessionLiveCard } from "./TaskSessionLiveCard";
|
|
24
|
-
import { IssuePickerModal, type RemoteIssue } from "./IssuePickerModal";
|
|
25
36
|
import {
|
|
26
37
|
DashboardAlertModal,
|
|
27
38
|
DashboardConfirmModal,
|
|
@@ -38,23 +49,60 @@ import {
|
|
|
38
49
|
writeConcurrentTaskStartPreference,
|
|
39
50
|
type ConcurrentTaskStartPreference,
|
|
40
51
|
} from "@/lib/concurrentTaskStartPreference";
|
|
52
|
+
import {
|
|
53
|
+
isPlannedBoundaryConflictModalOpen,
|
|
54
|
+
setConcurrentTaskStartConflictModalOpen,
|
|
55
|
+
} from "@/lib/kronosysDashboardModalGates";
|
|
41
56
|
import { mergeLiveSessionIntoHistory } from "@/lib/sessionListMerge";
|
|
42
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
buildTaskTemplateSignature,
|
|
59
|
+
formatTaskTemplateDatalistLabel,
|
|
60
|
+
formatTaskTemplateDraftLine,
|
|
61
|
+
parseTaskTemplatesFromPayload,
|
|
62
|
+
} from "@/lib/taskTemplateDraft";
|
|
43
63
|
import type { SessionListEntry } from "@/components/dashboard/SessionListPanel";
|
|
44
|
-
import { SessionEndReasonEditor } from "@/components/dashboard/SessionEndReasonEditor";
|
|
45
64
|
import { useDashboardToast } from "@/components/dashboard/DashboardToastProvider";
|
|
46
|
-
|
|
47
65
|
type TaskEntryMode = "realtime" | "past";
|
|
66
|
+
const TASK_DELETE_ANIM_MS = 170;
|
|
67
|
+
const SAVE_TEMPLATE_MARKER_RE = /(^|\s)##(?=\s|$)/g;
|
|
68
|
+
|
|
69
|
+
function parseDraftTemplateMarker(raw: string): {
|
|
70
|
+
normalizedDraft: string;
|
|
71
|
+
saveAsTemplate: boolean;
|
|
72
|
+
} {
|
|
73
|
+
const hasMarker = SAVE_TEMPLATE_MARKER_RE.test(raw);
|
|
74
|
+
SAVE_TEMPLATE_MARKER_RE.lastIndex = 0;
|
|
75
|
+
if (!hasMarker) {
|
|
76
|
+
return { normalizedDraft: raw.trim(), saveAsTemplate: false };
|
|
77
|
+
}
|
|
78
|
+
const cleaned = raw
|
|
79
|
+
.replace(SAVE_TEMPLATE_MARKER_RE, " ")
|
|
80
|
+
.replace(/\s+/g, " ")
|
|
81
|
+
.trim();
|
|
82
|
+
if (cleaned.length > 0) {
|
|
83
|
+
return { normalizedDraft: cleaned, saveAsTemplate: true };
|
|
84
|
+
}
|
|
85
|
+
// Cas explicite demandé: "##" seul déclenche aussi la sauvegarde template.
|
|
86
|
+
return { normalizedDraft: raw.trim(), saveAsTemplate: true };
|
|
87
|
+
}
|
|
48
88
|
|
|
49
89
|
type TaskRow = {
|
|
50
90
|
id: string;
|
|
51
91
|
name: string;
|
|
92
|
+
note?: string;
|
|
52
93
|
durationMs: number;
|
|
53
94
|
isDone: boolean;
|
|
54
95
|
startTime?: string;
|
|
55
96
|
endTime?: string;
|
|
97
|
+
scheduledEndAt?: string;
|
|
98
|
+
taskTimerLaps?: Array<{
|
|
99
|
+
startTime?: string;
|
|
100
|
+
endTime?: string;
|
|
101
|
+
durationMs?: number;
|
|
102
|
+
}>;
|
|
56
103
|
tags?: string[];
|
|
57
104
|
project?: string | null;
|
|
105
|
+
personalProject?: boolean;
|
|
58
106
|
subtasks?: Array<{
|
|
59
107
|
id: string;
|
|
60
108
|
title: string;
|
|
@@ -63,6 +111,9 @@ type TaskRow = {
|
|
|
63
111
|
}>;
|
|
64
112
|
manualTaskTimerPaused?: boolean;
|
|
65
113
|
activeSubtaskTimerId?: string;
|
|
114
|
+
subtaskTimerStartedAt?: string | number;
|
|
115
|
+
mainTimerSegmentStartedAt?: string | null;
|
|
116
|
+
taskCurrentLapStartedAt?: string | number | null;
|
|
66
117
|
};
|
|
67
118
|
|
|
68
119
|
type SessionShape = {
|
|
@@ -78,6 +129,33 @@ type SessionShape = {
|
|
|
78
129
|
tasks?: TaskRow[];
|
|
79
130
|
};
|
|
80
131
|
|
|
132
|
+
/** Aligné sur l’arrondi d’affichage des ajustements de durée (TaskSessionLiveCard). */
|
|
133
|
+
function pastEntrySecondsToManualFieldParts(totalSeconds: number): {
|
|
134
|
+
h: string;
|
|
135
|
+
m: string;
|
|
136
|
+
s: string;
|
|
137
|
+
} {
|
|
138
|
+
let ts = Math.max(0, Math.floor(totalSeconds));
|
|
139
|
+
if (ts >= 3600) {
|
|
140
|
+
ts = Math.floor(ts / 60) * 60;
|
|
141
|
+
}
|
|
142
|
+
const h = Math.floor(ts / 3600);
|
|
143
|
+
const m = Math.floor((ts % 3600) / 60);
|
|
144
|
+
const s = ts % 60;
|
|
145
|
+
return { h: String(h), m: String(m), s: String(s) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parsePastManualDurationMs(
|
|
149
|
+
hours: string,
|
|
150
|
+
minutes: string,
|
|
151
|
+
seconds: string,
|
|
152
|
+
): number {
|
|
153
|
+
const h = Math.max(0, Math.floor(Number(hours) || 0));
|
|
154
|
+
const m = Math.max(0, Math.floor(Number(minutes) || 0));
|
|
155
|
+
const s = Math.max(0, Math.floor(Number(seconds) || 0));
|
|
156
|
+
return (h * 3600 + m * 60 + s) * 1000;
|
|
157
|
+
}
|
|
158
|
+
|
|
81
159
|
function runningTasksFromSession(
|
|
82
160
|
session: SessionShape | null | undefined,
|
|
83
161
|
): TaskRow[] {
|
|
@@ -88,8 +166,8 @@ function runningTasksFromSession(
|
|
|
88
166
|
Array.isArray(session.activeTasks) && session.activeTasks.length > 0
|
|
89
167
|
? session.activeTasks
|
|
90
168
|
: session.activeTask
|
|
91
|
-
|
|
92
|
-
|
|
169
|
+
? [session.activeTask]
|
|
170
|
+
: [];
|
|
93
171
|
return raw.filter((t) => t && !t.isDone && !t.manualTaskTimerPaused);
|
|
94
172
|
}
|
|
95
173
|
|
|
@@ -110,6 +188,7 @@ export function TaskFocusPanel({
|
|
|
110
188
|
showKronoFocusInTaskOps = true,
|
|
111
189
|
allowTaskStartTimeEdit = true,
|
|
112
190
|
allowTaskEndTimeEdit = true,
|
|
191
|
+
taskLauncherApplyDraftRef,
|
|
113
192
|
}: {
|
|
114
193
|
payload: KronosysUpdatePayload;
|
|
115
194
|
lang: Lang;
|
|
@@ -128,6 +207,8 @@ export function TaskFocusPanel({
|
|
|
128
207
|
allowTaskStartTimeEdit?: boolean;
|
|
129
208
|
/** Option `dashboardAllowTaskEndTimeEdit` : correction de l'heure de fin des tâches terminées. */
|
|
130
209
|
allowTaskEndTimeEdit?: boolean;
|
|
210
|
+
/** Remplissage du champ nouvelle tâche depuis la recherche rapide (modèles). */
|
|
211
|
+
taskLauncherApplyDraftRef?: MutableRefObject<((text: string) => void) | null>;
|
|
131
212
|
}) {
|
|
132
213
|
const live = payload.current as SessionShape | undefined;
|
|
133
214
|
const history = useMemo(
|
|
@@ -155,48 +236,51 @@ export function TaskFocusPanel({
|
|
|
155
236
|
|
|
156
237
|
const knownTags = (payload.knownTags || []) as string[];
|
|
157
238
|
const knownProjects = (payload.knownProjects || []) as string[];
|
|
239
|
+
const knownPersonalProjects = (
|
|
240
|
+
((payload as Record<string, unknown>).knownPersonalProjects || []) as string[]
|
|
241
|
+
).filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
158
242
|
const viewingSession = archiveColumnId
|
|
159
|
-
?
|
|
243
|
+
? history.find((s) => s.sessionId === archiveColumnId) ??
|
|
160
244
|
historyArchivedList.find((s) => s.sessionId === archiveColumnId) ??
|
|
161
|
-
null
|
|
245
|
+
null
|
|
162
246
|
: null;
|
|
163
247
|
const sessionCurrent = viewingSession ?? live;
|
|
164
248
|
const isInspecting = !!viewingSession;
|
|
165
249
|
const inspectingId = archiveColumnId;
|
|
166
250
|
const isPauseState = !!live?.isPaused;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
251
|
+
/** Pause globale : bandeau mural distinct (icône navigation). */
|
|
252
|
+
const globalPauseContextActive = Boolean(
|
|
253
|
+
live &&
|
|
254
|
+
typeof live === "object" &&
|
|
255
|
+
"globalPauseContext" in live &&
|
|
256
|
+
(live as { globalPauseContext?: unknown }).globalPauseContext,
|
|
257
|
+
);
|
|
174
258
|
/** Session passée **terminée** : on n’affiche que les entrées marquées terminées (pas de blocs en cours / en pause). */
|
|
175
259
|
const inspectingEndedSession =
|
|
176
260
|
isInspecting &&
|
|
177
261
|
typeof viewingSession?.endAt === "string" &&
|
|
178
262
|
viewingSession.endAt.trim() !== "";
|
|
179
|
-
|
|
180
|
-
|
|
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 !== "";
|
|
263
|
+
|
|
264
|
+
const [bucketNowMs, setBucketNowMs] = useState(() => Date.now());
|
|
192
265
|
|
|
193
266
|
const taskSessionForBuckets = viewingSession ?? live;
|
|
194
|
-
/** Tâches live
|
|
195
|
-
const
|
|
267
|
+
/** Tâches live sur la pile (minuteur non en pause manuelle), avant filtre « planifié ». */
|
|
268
|
+
const runningTasksRaw = !isInspecting ? runningTasksFromSession(live) : [];
|
|
269
|
+
const runningTasks = useMemo(
|
|
270
|
+
() => runningTasksRaw.filter((t) => !isTaskDisplayPlanned(t, bucketNowMs)),
|
|
271
|
+
[runningTasksRaw, bucketNowMs],
|
|
272
|
+
);
|
|
196
273
|
const runningTaskIds = useMemo(
|
|
197
274
|
() => new Set(runningTasks.map((t) => String(t.id))),
|
|
198
275
|
[runningTasks],
|
|
199
276
|
);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
const id = globalThis.setInterval(() => {
|
|
280
|
+
setBucketNowMs(Date.now());
|
|
281
|
+
}, 1000);
|
|
282
|
+
return () => globalThis.clearInterval(id);
|
|
283
|
+
}, []);
|
|
200
284
|
/**
|
|
201
285
|
* `tasks` + pile `activeTasks` / `activeTask` (dédoublonné par id, la pile l’emporte).
|
|
202
286
|
* Sinon une tâche uniquement sur la pile — après pause concurrente — n’apparaît ni « en cours » ni « en pause ».
|
|
@@ -217,8 +301,8 @@ export function TaskFocusPanel({
|
|
|
217
301
|
Array.isArray(sess.activeTasks) && sess.activeTasks.length > 0
|
|
218
302
|
? [...(sess.activeTasks as TaskRow[])]
|
|
219
303
|
: sess.activeTask
|
|
220
|
-
|
|
221
|
-
|
|
304
|
+
? [sess.activeTask as TaskRow]
|
|
305
|
+
: [];
|
|
222
306
|
for (const t of stack) {
|
|
223
307
|
if (t?.id) {
|
|
224
308
|
map.set(String(t.id), t);
|
|
@@ -234,69 +318,190 @@ export function TaskFocusPanel({
|
|
|
234
318
|
const trackingActive =
|
|
235
319
|
!isInspecting && runningTasks.length > 0 && !isPauseState;
|
|
236
320
|
|
|
237
|
-
const { pausedTasks, completedTasks } = useMemo(() => {
|
|
321
|
+
const { plannedTasks, pausedTasks, completedTasks } = useMemo(() => {
|
|
322
|
+
const planned: TaskRow[] = [];
|
|
238
323
|
const paused: TaskRow[] = [];
|
|
239
324
|
const completed: TaskRow[] = [];
|
|
240
325
|
for (const t of mergedTasksForBuckets) {
|
|
241
|
-
if (t
|
|
326
|
+
if (isTaskDisplayPlanned(t, bucketNowMs)) {
|
|
327
|
+
planned.push(t);
|
|
328
|
+
} else if (t.isDone) {
|
|
242
329
|
completed.push(t);
|
|
243
330
|
} else if (!inspectingEndedSession && !runningTaskIds.has(String(t.id))) {
|
|
244
331
|
paused.push(t);
|
|
245
332
|
}
|
|
246
333
|
}
|
|
247
|
-
return {
|
|
248
|
-
|
|
334
|
+
return {
|
|
335
|
+
plannedTasks: planned,
|
|
336
|
+
pausedTasks: paused,
|
|
337
|
+
completedTasks: completed,
|
|
338
|
+
};
|
|
339
|
+
}, [
|
|
340
|
+
mergedTasksForBuckets,
|
|
341
|
+
inspectingEndedSession,
|
|
342
|
+
runningTaskIds,
|
|
343
|
+
bucketNowMs,
|
|
344
|
+
]);
|
|
249
345
|
const pausedTasksDisplay = [...pausedTasks].reverse();
|
|
346
|
+
const plannedTasksDisplay = [...plannedTasks].reverse();
|
|
250
347
|
const completedTasksDisplay = [...completedTasks].reverse();
|
|
348
|
+
const showPlannedTaskSection = plannedTasksDisplay.length > 0;
|
|
251
349
|
const showPausedTaskSection =
|
|
252
350
|
!inspectingEndedSession && pausedTasksDisplay.length > 0;
|
|
253
351
|
const showCompletedTaskSection = completedTasksDisplay.length > 0;
|
|
254
352
|
const showTaskListBuckets =
|
|
255
353
|
(isInspecting || hasLiveSession) &&
|
|
256
|
-
(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
354
|
+
(showPlannedTaskSection ||
|
|
355
|
+
showPausedTaskSection ||
|
|
356
|
+
showCompletedTaskSection);
|
|
357
|
+
const bucketSectionsCount =
|
|
358
|
+
Number(showPlannedTaskSection) +
|
|
359
|
+
Number(showPausedTaskSection) +
|
|
360
|
+
Number(showCompletedTaskSection);
|
|
361
|
+
/** Pastille « total » lorsqu’au moins deux blocs de tâches sont visibles (évite le doublon avec un seul bloc). */
|
|
362
|
+
const showSessionTaskCountBadge = bucketSectionsCount >= 2;
|
|
363
|
+
const canAddHistoricalTaskToInspectingSession =
|
|
364
|
+
isInspecting && !!inspectingId;
|
|
365
|
+
const showTaskEntryControls =
|
|
366
|
+
!inspectingEndedSession || canAddHistoricalTaskToInspectingSession;
|
|
262
367
|
|
|
263
368
|
const kronoFocusStatus = (live as LiveSessionShape | undefined)?.kronoFocus
|
|
264
369
|
?.status;
|
|
265
370
|
const kronoFocusIsRunningOrPaused =
|
|
266
371
|
kronoFocusStatus === "running" || kronoFocusStatus === "paused";
|
|
267
372
|
|
|
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
373
|
const [taskInput, setTaskInput] = useState("");
|
|
374
|
+
const [taskNoteInput, setTaskNoteInput] = useState("");
|
|
375
|
+
const [debouncedTemplateQuery, setDebouncedTemplateQuery] = useState("");
|
|
274
376
|
const [taskEntryMode, setTaskEntryMode] = useState<TaskEntryMode>("realtime");
|
|
275
377
|
const [pastStartLocal, setPastStartLocal] = useState("");
|
|
276
378
|
const [pastEndLocal, setPastEndLocal] = useState("");
|
|
379
|
+
const [pastManualHours, setPastManualHours] = useState("0");
|
|
380
|
+
const [pastManualMinutes, setPastManualMinutes] = useState("0");
|
|
381
|
+
const [pastManualSeconds, setPastManualSeconds] = useState("0");
|
|
382
|
+
const pendingPastDurationMsRef = useRef<number | null>(null);
|
|
277
383
|
const [startKronoFocusWithTask, setStartKronoFocusWithTask] = useState(false);
|
|
278
|
-
const [issuePickerOpen, setIssuePickerOpen] = useState(false);
|
|
279
384
|
const [alertMessage, setAlertMessage] = useState<string | null>(null);
|
|
280
385
|
const [kronoFocusResetConfirmOpen, setKronoFocusResetConfirmOpen] =
|
|
281
386
|
useState(false);
|
|
282
387
|
const [deleteTaskConfirmId, setDeleteTaskConfirmId] = useState<string | null>(
|
|
283
388
|
null,
|
|
284
389
|
);
|
|
390
|
+
const [deletingTaskIds, setDeletingTaskIds] = useState<Set<string>>(
|
|
391
|
+
() => new Set(),
|
|
392
|
+
);
|
|
285
393
|
const [concurrentStartModalOpen, setConcurrentStartModalOpen] =
|
|
286
394
|
useState(false);
|
|
287
395
|
const [rememberConcurrentStart, setRememberConcurrentStart] = useState(false);
|
|
396
|
+
const [taskListVisibleSessionId, setTaskListVisibleSessionId] = useState<
|
|
397
|
+
string | null
|
|
398
|
+
>(null);
|
|
399
|
+
const [taskListEnterSessionId, setTaskListEnterSessionId] = useState<
|
|
400
|
+
string | null
|
|
401
|
+
>(null);
|
|
288
402
|
const rememberConcurrentRef = useRef(false);
|
|
289
403
|
const pendingLiveStartRef = useRef<{
|
|
290
404
|
name: string;
|
|
291
405
|
tags: string[];
|
|
292
406
|
project?: string;
|
|
407
|
+
personalProject?: boolean;
|
|
408
|
+
note: string;
|
|
293
409
|
startKronoFocus: boolean;
|
|
410
|
+
saveAsTemplate: boolean;
|
|
294
411
|
} | null>(null);
|
|
295
412
|
|
|
296
413
|
useEffect(() => {
|
|
297
414
|
rememberConcurrentRef.current = rememberConcurrentStart;
|
|
298
415
|
}, [rememberConcurrentStart]);
|
|
299
416
|
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
setConcurrentTaskStartConflictModalOpen(concurrentStartModalOpen);
|
|
419
|
+
return () => setConcurrentTaskStartConflictModalOpen(false);
|
|
420
|
+
}, [concurrentStartModalOpen]);
|
|
421
|
+
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
const targetSessionId =
|
|
424
|
+
typeof sessionCurrent?.sessionId === "string" &&
|
|
425
|
+
sessionCurrent.sessionId.trim() !== ""
|
|
426
|
+
? sessionCurrent.sessionId.trim()
|
|
427
|
+
: null;
|
|
428
|
+
if (!targetSessionId) {
|
|
429
|
+
setTaskListVisibleSessionId(null);
|
|
430
|
+
setTaskListEnterSessionId(null);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
setTaskListVisibleSessionId(targetSessionId);
|
|
434
|
+
setTaskListEnterSessionId(null);
|
|
435
|
+
let raf1 = 0;
|
|
436
|
+
let raf2 = 0;
|
|
437
|
+
raf1 = requestAnimationFrame(() => {
|
|
438
|
+
raf2 = requestAnimationFrame(() =>
|
|
439
|
+
setTaskListEnterSessionId(targetSessionId),
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
return () => {
|
|
443
|
+
cancelAnimationFrame(raf1);
|
|
444
|
+
cancelAnimationFrame(raf2);
|
|
445
|
+
};
|
|
446
|
+
}, [sessionCurrent?.sessionId]);
|
|
447
|
+
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (!taskLauncherApplyDraftRef) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
taskLauncherApplyDraftRef.current = (text: string) => {
|
|
453
|
+
setTaskInput(text);
|
|
454
|
+
globalThis.requestAnimationFrame(() => {
|
|
455
|
+
document.getElementById("kronosys-task-launcher-input")?.focus();
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
return () => {
|
|
459
|
+
taskLauncherApplyDraftRef.current = null;
|
|
460
|
+
};
|
|
461
|
+
}, [taskLauncherApplyDraftRef]);
|
|
462
|
+
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
const t = globalThis.setTimeout(() => {
|
|
465
|
+
setDebouncedTemplateQuery(taskInput.trim().toLowerCase());
|
|
466
|
+
}, 220);
|
|
467
|
+
return () => globalThis.clearTimeout(t);
|
|
468
|
+
}, [taskInput]);
|
|
469
|
+
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
const startMs = pastStartLocal.trim()
|
|
472
|
+
? new Date(pastStartLocal).getTime()
|
|
473
|
+
: NaN;
|
|
474
|
+
const endMs = pastEndLocal.trim() ? new Date(pastEndLocal).getTime() : NaN;
|
|
475
|
+
const pending = pendingPastDurationMsRef.current;
|
|
476
|
+
if (
|
|
477
|
+
pending !== null &&
|
|
478
|
+
Number.isFinite(startMs) &&
|
|
479
|
+
Number.isFinite(endMs) &&
|
|
480
|
+
endMs > startMs
|
|
481
|
+
) {
|
|
482
|
+
pendingPastDurationMsRef.current = null;
|
|
483
|
+
const spanMs = endMs - startMs;
|
|
484
|
+
const capped = Math.min(Math.max(0, pending), spanMs);
|
|
485
|
+
const parts = pastEntrySecondsToManualFieldParts(capped / 1000);
|
|
486
|
+
setPastManualHours(parts.h);
|
|
487
|
+
setPastManualMinutes(parts.m);
|
|
488
|
+
setPastManualSeconds(parts.s);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (
|
|
492
|
+
!Number.isFinite(startMs) ||
|
|
493
|
+
!Number.isFinite(endMs) ||
|
|
494
|
+
endMs <= startMs
|
|
495
|
+
) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const spanSec = Math.floor((endMs - startMs) / 1000);
|
|
499
|
+
const parts = pastEntrySecondsToManualFieldParts(spanSec);
|
|
500
|
+
setPastManualHours(parts.h);
|
|
501
|
+
setPastManualMinutes(parts.m);
|
|
502
|
+
setPastManualSeconds(parts.s);
|
|
503
|
+
}, [pastStartLocal, pastEndLocal]);
|
|
504
|
+
|
|
300
505
|
useEffect(() => {
|
|
301
506
|
if (!showKronoFocusInTaskOps) {
|
|
302
507
|
setStartKronoFocusWithTask(false);
|
|
@@ -332,6 +537,113 @@ export function TaskFocusPanel({
|
|
|
332
537
|
const titleParsed = parseTaskWithAutoTags(taskInput);
|
|
333
538
|
const mergedDraftTags = mergeTagsForDisplay(taskInput, []);
|
|
334
539
|
const mergedDraftProject = titleParsed.project;
|
|
540
|
+
const mergedDraftPersonalProject = titleParsed.personalProject;
|
|
541
|
+
const taskTemplates = useMemo(
|
|
542
|
+
() =>
|
|
543
|
+
parseTaskTemplatesFromPayload(
|
|
544
|
+
(payload as Record<string, unknown>).taskTemplates,
|
|
545
|
+
),
|
|
546
|
+
[payload],
|
|
547
|
+
);
|
|
548
|
+
const filteredTaskTemplates = useMemo(() => {
|
|
549
|
+
const q = debouncedTemplateQuery;
|
|
550
|
+
const rows =
|
|
551
|
+
q.length === 0
|
|
552
|
+
? taskTemplates
|
|
553
|
+
: taskTemplates.filter((tpl) => {
|
|
554
|
+
const bag = [tpl.name, ...tpl.tags, tpl.project ?? ""]
|
|
555
|
+
.join(" ")
|
|
556
|
+
.toLowerCase();
|
|
557
|
+
return bag.includes(q);
|
|
558
|
+
});
|
|
559
|
+
return rows.slice(0, 20);
|
|
560
|
+
}, [debouncedTemplateQuery, taskTemplates]);
|
|
561
|
+
const taskTemplateSignatureSet = useMemo(() => {
|
|
562
|
+
const signatures = new Set<string>();
|
|
563
|
+
for (const tpl of taskTemplates) {
|
|
564
|
+
signatures.add(
|
|
565
|
+
buildTaskTemplateSignature({
|
|
566
|
+
name: tpl.name,
|
|
567
|
+
tags: tpl.tags,
|
|
568
|
+
project: tpl.project,
|
|
569
|
+
personalProject: tpl.personalProject === true,
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
return signatures;
|
|
574
|
+
}, [taskTemplates]);
|
|
575
|
+
const isTaskAlreadyTemplate = useCallback(
|
|
576
|
+
(task: TaskRow) =>
|
|
577
|
+
taskTemplateSignatureSet.has(
|
|
578
|
+
buildTaskTemplateSignature({
|
|
579
|
+
name: task.name,
|
|
580
|
+
tags: mergeTagsForDisplay(task.name, task.tags ?? []),
|
|
581
|
+
project:
|
|
582
|
+
typeof task.project === "string" && task.project.trim()
|
|
583
|
+
? task.project
|
|
584
|
+
: null,
|
|
585
|
+
personalProject: task.personalProject === true,
|
|
586
|
+
}),
|
|
587
|
+
),
|
|
588
|
+
[taskTemplateSignatureSet],
|
|
589
|
+
);
|
|
590
|
+
const saveTaskAsTemplate = useCallback(
|
|
591
|
+
async (task: TaskRow) => {
|
|
592
|
+
const title = taskTitleForDisplay(task.name).trim();
|
|
593
|
+
const tagParts = (task.tags ?? []).map(
|
|
594
|
+
(tag) => `#${normalizeTagKey(tag)}`,
|
|
595
|
+
);
|
|
596
|
+
const project =
|
|
597
|
+
typeof task.project === "string" && task.project.trim()
|
|
598
|
+
? formatProjectDisplay(task.project, {
|
|
599
|
+
personal: task.personalProject === true,
|
|
600
|
+
})
|
|
601
|
+
: "";
|
|
602
|
+
const draftString = [title, ...tagParts, project]
|
|
603
|
+
.filter(Boolean)
|
|
604
|
+
.join(" ")
|
|
605
|
+
.trim();
|
|
606
|
+
const draft = buildStartTaskFromDraft(draftString, [], undefined);
|
|
607
|
+
if (!draft.name.trim() && draft.tags.length === 0 && !draft.project) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
await post({
|
|
611
|
+
type: "saveTaskTemplate",
|
|
612
|
+
name: draft.name,
|
|
613
|
+
tags: draft.tags,
|
|
614
|
+
...(draft.project ? { project: draft.project } : {}),
|
|
615
|
+
...(draft.project
|
|
616
|
+
? { personalProject: draft.personalProject === true }
|
|
617
|
+
: {}),
|
|
618
|
+
});
|
|
619
|
+
pushToast(t.taskTemplateSavedToast);
|
|
620
|
+
},
|
|
621
|
+
[post, pushToast, t.taskTemplateSavedToast],
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const saveDraftAsTemplate = useCallback(
|
|
625
|
+
async (draft: {
|
|
626
|
+
name: string;
|
|
627
|
+
tags: string[];
|
|
628
|
+
project?: string;
|
|
629
|
+
personalProject?: boolean;
|
|
630
|
+
}) => {
|
|
631
|
+
if (!draft.name.trim() && draft.tags.length === 0 && !draft.project) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
await post({
|
|
635
|
+
type: "saveTaskTemplate",
|
|
636
|
+
name: draft.name,
|
|
637
|
+
tags: draft.tags,
|
|
638
|
+
...(draft.project ? { project: draft.project } : {}),
|
|
639
|
+
...(draft.project
|
|
640
|
+
? { personalProject: draft.personalProject === true }
|
|
641
|
+
: {}),
|
|
642
|
+
});
|
|
643
|
+
pushToast(t.taskTemplateSavedToast);
|
|
644
|
+
},
|
|
645
|
+
[post, pushToast, t.taskTemplateSavedToast],
|
|
646
|
+
);
|
|
335
647
|
|
|
336
648
|
const handleRemoveDraftTag = useCallback((tag: string) => {
|
|
337
649
|
setTaskInput((prev) => {
|
|
@@ -342,20 +654,90 @@ export function TaskFocusPanel({
|
|
|
342
654
|
|
|
343
655
|
const handleRemoveDraftProject = useCallback(() => {
|
|
344
656
|
setTaskInput((prev) => {
|
|
345
|
-
|
|
346
|
-
|
|
657
|
+
let s = prev.replace(/(^|\s)@[^\s@#]+(?:#[^\s#]+)?(?=\s|$)/gi, " ");
|
|
658
|
+
s = s.replace(/(^|\s)![^\s!#]+(?:#[^\s#]+)?(?=\s|$)/gi, " ");
|
|
659
|
+
return s.replace(/\s+/g, " ").trim();
|
|
347
660
|
});
|
|
348
661
|
}, []);
|
|
349
662
|
|
|
350
|
-
const confirmDeleteTask = useCallback(
|
|
351
|
-
|
|
352
|
-
|
|
663
|
+
const confirmDeleteTask = useCallback(
|
|
664
|
+
(taskId: string) => {
|
|
665
|
+
if (deletingTaskIds.has(taskId)) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
setDeleteTaskConfirmId(taskId);
|
|
669
|
+
},
|
|
670
|
+
[deletingTaskIds],
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
const visibleTaskIds = new Set(
|
|
675
|
+
mergedTasksForBuckets.map((task) => String(task.id)),
|
|
676
|
+
);
|
|
677
|
+
setDeletingTaskIds((prev) => {
|
|
678
|
+
if (prev.size === 0) {
|
|
679
|
+
return prev;
|
|
680
|
+
}
|
|
681
|
+
const next = new Set<string>();
|
|
682
|
+
for (const id of prev) {
|
|
683
|
+
if (visibleTaskIds.has(id)) {
|
|
684
|
+
next.add(id);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return next.size === prev.size ? prev : next;
|
|
688
|
+
});
|
|
689
|
+
}, [mergedTasksForBuckets]);
|
|
353
690
|
|
|
354
691
|
const clearTaskEntryForm = useCallback(() => {
|
|
355
692
|
setTaskInput("");
|
|
693
|
+
setTaskNoteInput("");
|
|
356
694
|
setStartKronoFocusWithTask(false);
|
|
357
695
|
}, []);
|
|
358
696
|
|
|
697
|
+
const duplicateTaskToDraft = useCallback(
|
|
698
|
+
(task: TaskRow) => {
|
|
699
|
+
const targetSessionId = inspectingId ?? live?.sessionId;
|
|
700
|
+
const title = taskTitleForDisplay(task.name).trim();
|
|
701
|
+
const tags = (task.tags ?? []).map((tag) => `#${normalizeTagKey(tag)}`);
|
|
702
|
+
const project =
|
|
703
|
+
typeof task.project === "string" && task.project.trim()
|
|
704
|
+
? formatProjectDisplay(task.project, {
|
|
705
|
+
personal: task.personalProject === true,
|
|
706
|
+
})
|
|
707
|
+
: "";
|
|
708
|
+
const nextInput = [title, ...tags, project]
|
|
709
|
+
.filter(Boolean)
|
|
710
|
+
.join(" ")
|
|
711
|
+
.trim();
|
|
712
|
+
setTaskInput(nextInput);
|
|
713
|
+
setTaskNoteInput(typeof task.note === "string" ? task.note : "");
|
|
714
|
+
if (
|
|
715
|
+
typeof task.startTime === "string" &&
|
|
716
|
+
task.startTime.trim() &&
|
|
717
|
+
typeof task.endTime === "string" &&
|
|
718
|
+
task.endTime.trim() &&
|
|
719
|
+
targetSessionId
|
|
720
|
+
) {
|
|
721
|
+
const start = new Date(task.startTime);
|
|
722
|
+
const end = new Date(task.endTime);
|
|
723
|
+
if (!Number.isNaN(start.getTime()) && !Number.isNaN(end.getTime())) {
|
|
724
|
+
const spanMs = Math.max(0, end.getTime() - start.getTime());
|
|
725
|
+
const durRaw =
|
|
726
|
+
typeof task.durationMs === "number" &&
|
|
727
|
+
Number.isFinite(task.durationMs)
|
|
728
|
+
? task.durationMs
|
|
729
|
+
: spanMs;
|
|
730
|
+
pendingPastDurationMsRef.current =
|
|
731
|
+
spanMs > 0 ? Math.min(Math.max(0, durRaw), spanMs) : spanMs;
|
|
732
|
+
setTaskEntryMode("past");
|
|
733
|
+
setPastStartLocal(formatDatetimeLocalValue(start));
|
|
734
|
+
setPastEndLocal(formatDatetimeLocalValue(end));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
[inspectingId, live?.sessionId],
|
|
739
|
+
);
|
|
740
|
+
|
|
359
741
|
const resolveConcurrentAndStartLive = useCallback(
|
|
360
742
|
async (mode: ConcurrentTaskStartPreference, persistPreference: boolean) => {
|
|
361
743
|
const draft = pendingLiveStartRef.current;
|
|
@@ -380,7 +762,6 @@ export function TaskFocusPanel({
|
|
|
380
762
|
await postKronosysAction({
|
|
381
763
|
type: "finishTask",
|
|
382
764
|
taskId: t.id,
|
|
383
|
-
shouldCommit: false,
|
|
384
765
|
});
|
|
385
766
|
}
|
|
386
767
|
}
|
|
@@ -390,15 +771,20 @@ export function TaskFocusPanel({
|
|
|
390
771
|
name: draft.name,
|
|
391
772
|
startKronoFocus: draft.startKronoFocus,
|
|
392
773
|
tags: draft.tags,
|
|
774
|
+
note: draft.note,
|
|
393
775
|
...(draft.project ? { project: draft.project } : {}),
|
|
776
|
+
...(draft.personalProject ? { personalProject: true } : {}),
|
|
394
777
|
});
|
|
778
|
+
if (draft.saveAsTemplate) {
|
|
779
|
+
await saveDraftAsTemplate(draft);
|
|
780
|
+
}
|
|
395
781
|
} catch (error) {
|
|
396
782
|
pushToast(
|
|
397
783
|
typeof error === "string"
|
|
398
784
|
? error
|
|
399
785
|
: error instanceof Error
|
|
400
|
-
|
|
401
|
-
|
|
786
|
+
? error.message
|
|
787
|
+
: "Failed to resolve concurrent tasks",
|
|
402
788
|
);
|
|
403
789
|
pendingLiveStartRef.current = null;
|
|
404
790
|
setConcurrentStartModalOpen(false);
|
|
@@ -410,15 +796,20 @@ export function TaskFocusPanel({
|
|
|
410
796
|
setRememberConcurrentStart(false);
|
|
411
797
|
clearTaskEntryForm();
|
|
412
798
|
},
|
|
413
|
-
[live, post, clearTaskEntryForm, pushToast],
|
|
799
|
+
[live, post, clearTaskEntryForm, pushToast, saveDraftAsTemplate],
|
|
414
800
|
);
|
|
415
801
|
|
|
416
802
|
const submitStartTask = useCallback(async () => {
|
|
417
|
-
const
|
|
803
|
+
const draftControl = parseDraftTemplateMarker(taskInput);
|
|
804
|
+
const hasDraft = draftControl.normalizedDraft.length > 0;
|
|
418
805
|
if (!hasDraft) {
|
|
419
806
|
return;
|
|
420
807
|
}
|
|
421
|
-
const payloadStart = buildStartTaskFromDraft(
|
|
808
|
+
const payloadStart = buildStartTaskFromDraft(
|
|
809
|
+
draftControl.normalizedDraft,
|
|
810
|
+
[],
|
|
811
|
+
undefined,
|
|
812
|
+
);
|
|
422
813
|
if (
|
|
423
814
|
!payloadStart.name.trim() &&
|
|
424
815
|
payloadStart.tags.length === 0 &&
|
|
@@ -430,7 +821,10 @@ export function TaskFocusPanel({
|
|
|
430
821
|
name: payloadStart.name,
|
|
431
822
|
tags: payloadStart.tags,
|
|
432
823
|
...(payloadStart.project ? { project: payloadStart.project } : {}),
|
|
824
|
+
...(payloadStart.personalProject ? { personalProject: true } : {}),
|
|
433
825
|
startKronoFocus: showKronoFocusInTaskOps && startKronoFocusWithTask,
|
|
826
|
+
saveAsTemplate: draftControl.saveAsTemplate,
|
|
827
|
+
note: taskNoteInput,
|
|
434
828
|
};
|
|
435
829
|
if (!isInspecting && !hasLiveSession) {
|
|
436
830
|
pushToast(t.taskStartAutoSessionToast);
|
|
@@ -440,14 +834,23 @@ export function TaskFocusPanel({
|
|
|
440
834
|
name: body.name,
|
|
441
835
|
startKronoFocus: body.startKronoFocus,
|
|
442
836
|
tags: body.tags,
|
|
837
|
+
note: body.note,
|
|
443
838
|
...(body.project ? { project: body.project } : {}),
|
|
839
|
+
...(body.personalProject ? { personalProject: true } : {}),
|
|
444
840
|
});
|
|
841
|
+
if (body.saveAsTemplate) {
|
|
842
|
+
await saveDraftAsTemplate(body);
|
|
843
|
+
}
|
|
445
844
|
clearTaskEntryForm();
|
|
446
845
|
return;
|
|
447
846
|
}
|
|
448
847
|
if (!isInspecting && hasLiveSession && live) {
|
|
449
848
|
const running = runningTasksFromSession(live);
|
|
450
849
|
if (running.length > 0) {
|
|
850
|
+
if (isPlannedBoundaryConflictModalOpen()) {
|
|
851
|
+
pushToast(t.taskStartBlockedByPlannedBoundaryConflict);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
451
854
|
const pref = readConcurrentTaskStartPreference();
|
|
452
855
|
pendingLiveStartRef.current = body;
|
|
453
856
|
if (pref === "pause") {
|
|
@@ -472,8 +875,13 @@ export function TaskFocusPanel({
|
|
|
472
875
|
name: body.name,
|
|
473
876
|
startKronoFocus: body.startKronoFocus,
|
|
474
877
|
tags: body.tags,
|
|
878
|
+
note: body.note,
|
|
475
879
|
...(body.project ? { project: body.project } : {}),
|
|
880
|
+
...(body.personalProject ? { personalProject: true } : {}),
|
|
476
881
|
});
|
|
882
|
+
if (body.saveAsTemplate) {
|
|
883
|
+
await saveDraftAsTemplate(body);
|
|
884
|
+
}
|
|
477
885
|
clearTaskEntryForm();
|
|
478
886
|
}, [
|
|
479
887
|
clearTaskEntryForm,
|
|
@@ -483,9 +891,11 @@ export function TaskFocusPanel({
|
|
|
483
891
|
post,
|
|
484
892
|
pushToast,
|
|
485
893
|
resolveConcurrentAndStartLive,
|
|
894
|
+
saveDraftAsTemplate,
|
|
486
895
|
showKronoFocusInTaskOps,
|
|
487
896
|
startKronoFocusWithTask,
|
|
488
897
|
t.taskStartAutoSessionToast,
|
|
898
|
+
t.taskStartBlockedByPlannedBoundaryConflict,
|
|
489
899
|
taskInput,
|
|
490
900
|
]);
|
|
491
901
|
|
|
@@ -518,7 +928,16 @@ export function TaskFocusPanel({
|
|
|
518
928
|
setAlertMessage(t.archiveTaskDatetimeRangeInvalid);
|
|
519
929
|
return;
|
|
520
930
|
}
|
|
521
|
-
const
|
|
931
|
+
const spanMs = Math.round(endMs - startMs);
|
|
932
|
+
const durationMs = parsePastManualDurationMs(
|
|
933
|
+
pastManualHours,
|
|
934
|
+
pastManualMinutes,
|
|
935
|
+
pastManualSeconds,
|
|
936
|
+
);
|
|
937
|
+
if (durationMs <= 0 || durationMs > spanMs) {
|
|
938
|
+
setAlertMessage(t.archiveTaskManualDurationInvalid);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
522
941
|
const payloadStart = buildStartTaskFromDraft(nameRaw, [], undefined);
|
|
523
942
|
if (
|
|
524
943
|
!payloadStart.name.trim() &&
|
|
@@ -532,22 +951,32 @@ export function TaskFocusPanel({
|
|
|
532
951
|
sessionId: retroTargetSessionId,
|
|
533
952
|
name: payloadStart.name,
|
|
534
953
|
tags: payloadStart.tags,
|
|
954
|
+
note: taskNoteInput,
|
|
535
955
|
durationMs,
|
|
536
956
|
startTime: new Date(startMs).toISOString(),
|
|
537
957
|
endTime: new Date(endMs).toISOString(),
|
|
538
958
|
...(payloadStart.project ? { project: payloadStart.project } : {}),
|
|
959
|
+
...(payloadStart.personalProject ? { personalProject: true } : {}),
|
|
539
960
|
});
|
|
540
961
|
setTaskInput("");
|
|
962
|
+
setTaskNoteInput("");
|
|
541
963
|
setPastStartLocal("");
|
|
542
964
|
setPastEndLocal("");
|
|
965
|
+
setPastManualHours("0");
|
|
966
|
+
setPastManualMinutes("0");
|
|
967
|
+
setPastManualSeconds("0");
|
|
543
968
|
}, [
|
|
544
969
|
pastEndLocal,
|
|
970
|
+
pastManualHours,
|
|
971
|
+
pastManualMinutes,
|
|
972
|
+
pastManualSeconds,
|
|
545
973
|
pastStartLocal,
|
|
546
974
|
post,
|
|
547
975
|
retroTargetSessionId,
|
|
548
976
|
t.archiveTaskDatetimeRangeInvalid,
|
|
549
|
-
t.
|
|
977
|
+
t.archiveTaskManualDurationInvalid,
|
|
550
978
|
taskInput,
|
|
979
|
+
taskNoteInput,
|
|
551
980
|
]);
|
|
552
981
|
|
|
553
982
|
const pastRangeValid =
|
|
@@ -557,7 +986,22 @@ export function TaskFocusPanel({
|
|
|
557
986
|
Number.isFinite(new Date(pastEndLocal).getTime()) &&
|
|
558
987
|
new Date(pastEndLocal).getTime() > new Date(pastStartLocal).getTime();
|
|
559
988
|
|
|
560
|
-
const
|
|
989
|
+
const pastManualDurationMs = parsePastManualDurationMs(
|
|
990
|
+
pastManualHours,
|
|
991
|
+
pastManualMinutes,
|
|
992
|
+
pastManualSeconds,
|
|
993
|
+
);
|
|
994
|
+
const spanMsPast = pastRangeValid
|
|
995
|
+
? Math.round(
|
|
996
|
+
new Date(pastEndLocal).getTime() - new Date(pastStartLocal).getTime(),
|
|
997
|
+
)
|
|
998
|
+
: 0;
|
|
999
|
+
const pastManualDurationValid =
|
|
1000
|
+
pastRangeValid &&
|
|
1001
|
+
pastManualDurationMs > 0 &&
|
|
1002
|
+
pastManualDurationMs <= spanMsPast;
|
|
1003
|
+
|
|
1004
|
+
const canSubmitPast = !!taskInput.trim() && pastManualDurationValid;
|
|
561
1005
|
|
|
562
1006
|
/** Début + fin sur une seule ligne (scroll horizontal si besoin), contenu centré dans la zone. */
|
|
563
1007
|
const pastDateTimeRow = (
|
|
@@ -583,17 +1027,63 @@ export function TaskFocusPanel({
|
|
|
583
1027
|
onChange={setPastEndLocal}
|
|
584
1028
|
aria-label={t.archiveTaskEndLabel}
|
|
585
1029
|
lang={lang}
|
|
1030
|
+
defaultTimeMode="next-half-hour"
|
|
586
1031
|
t={t}
|
|
587
1032
|
/>
|
|
588
1033
|
</label>
|
|
589
1034
|
</div>
|
|
590
1035
|
);
|
|
591
1036
|
|
|
1037
|
+
const pastTimerDurationRow = (
|
|
1038
|
+
<div className="mx-auto w-max max-w-full space-y-1">
|
|
1039
|
+
<div className="text-center text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
1040
|
+
{t.archiveTaskTimerDurationLabel}
|
|
1041
|
+
</div>
|
|
1042
|
+
<div className="flex flex-wrap items-end justify-center gap-x-2 gap-y-1">
|
|
1043
|
+
<label className="flex w-[3.25rem] shrink-0 flex-col text-xs">
|
|
1044
|
+
{t.taskTimingAdjustHours}
|
|
1045
|
+
<input
|
|
1046
|
+
type="number"
|
|
1047
|
+
min={0}
|
|
1048
|
+
value={pastManualHours}
|
|
1049
|
+
onChange={(e) => setPastManualHours(e.target.value)}
|
|
1050
|
+
className="mt-1 w-full min-w-0 rounded border border-zinc-300 px-1.5 py-1 text-center tabular-nums dark:border-zinc-600 dark:bg-zinc-900"
|
|
1051
|
+
/>
|
|
1052
|
+
</label>
|
|
1053
|
+
<label className="flex w-[3.25rem] shrink-0 flex-col text-xs">
|
|
1054
|
+
{t.taskTimingAdjustMinutes}
|
|
1055
|
+
<input
|
|
1056
|
+
type="number"
|
|
1057
|
+
min={0}
|
|
1058
|
+
max={59}
|
|
1059
|
+
value={pastManualMinutes}
|
|
1060
|
+
onChange={(e) => setPastManualMinutes(e.target.value)}
|
|
1061
|
+
className="mt-1 w-full min-w-0 rounded border border-zinc-300 px-1.5 py-1 text-center tabular-nums dark:border-zinc-600 dark:bg-zinc-900"
|
|
1062
|
+
/>
|
|
1063
|
+
</label>
|
|
1064
|
+
<label className="flex w-[3.25rem] shrink-0 flex-col text-xs">
|
|
1065
|
+
{t.taskTimingAdjustSeconds}
|
|
1066
|
+
<input
|
|
1067
|
+
type="number"
|
|
1068
|
+
min={0}
|
|
1069
|
+
max={59}
|
|
1070
|
+
value={pastManualSeconds}
|
|
1071
|
+
onChange={(e) => setPastManualSeconds(e.target.value)}
|
|
1072
|
+
className="mt-1 w-full min-w-0 rounded border border-zinc-300 px-1.5 py-1 text-center tabular-nums dark:border-zinc-600 dark:bg-zinc-900"
|
|
1073
|
+
/>
|
|
1074
|
+
</label>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
);
|
|
1078
|
+
|
|
592
1079
|
const pastDatetimeRowInspecting = (
|
|
593
|
-
<div className="flex w-full min-w-0
|
|
1080
|
+
<div className="flex w-full min-w-0 flex-col items-center gap-3">
|
|
594
1081
|
<div className="kronosys-past-datetime-blink-twice min-w-0 max-w-full">
|
|
595
1082
|
{pastDateTimeRow}
|
|
596
1083
|
</div>
|
|
1084
|
+
<div className="kronosys-past-datetime-blink-twice w-full max-w-full">
|
|
1085
|
+
{pastTimerDurationRow}
|
|
1086
|
+
</div>
|
|
597
1087
|
</div>
|
|
598
1088
|
);
|
|
599
1089
|
|
|
@@ -620,58 +1110,16 @@ export function TaskFocusPanel({
|
|
|
620
1110
|
await post({ type: "setPaused", paused: false });
|
|
621
1111
|
};
|
|
622
1112
|
|
|
1113
|
+
const resumeLiveSessionWall = async () => {
|
|
1114
|
+
await post({ type: "setPaused", paused: false });
|
|
1115
|
+
};
|
|
1116
|
+
|
|
623
1117
|
const historyBannerTitle = isPauseState
|
|
624
1118
|
? t.pausedNote
|
|
625
1119
|
: t.viewingHistoryBannerTitle;
|
|
626
1120
|
const viewingSessionLabel =
|
|
627
1121
|
sessionCurrent?.sessionName?.trim() || sessionCurrent?.sessionId || "—";
|
|
628
1122
|
|
|
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
1123
|
const showTaskModeToggle = !!retroTargetSessionId;
|
|
676
1124
|
const effectiveTaskEntryMode: TaskEntryMode = showTaskModeToggle
|
|
677
1125
|
? taskEntryMode
|
|
@@ -709,310 +1157,302 @@ export function TaskFocusPanel({
|
|
|
709
1157
|
</section>
|
|
710
1158
|
)}
|
|
711
1159
|
|
|
712
|
-
{isInspecting &&
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
{
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
radioGroupName="kronosys-session-end-reason-edit-tasks"
|
|
734
|
-
sessionId={sessionEndReasonEditSid}
|
|
735
|
-
initialKind={viewingSession?.sessionEndReasonKind}
|
|
736
|
-
initialNote={viewingSession?.sessionEndReasonNote}
|
|
737
|
-
post={post}
|
|
738
|
-
/>
|
|
1160
|
+
{!isInspecting &&
|
|
1161
|
+
hasLiveSession &&
|
|
1162
|
+
isPauseState &&
|
|
1163
|
+
!globalPauseContextActive ? (
|
|
1164
|
+
<section
|
|
1165
|
+
className="relative z-[41] 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"
|
|
1166
|
+
aria-label={t.liveSessionWallPausedBannerAria}
|
|
1167
|
+
>
|
|
1168
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
1169
|
+
<Pause
|
|
1170
|
+
className="mt-0.5 shrink-0 text-amber-400"
|
|
1171
|
+
size={18}
|
|
1172
|
+
aria-hidden
|
|
1173
|
+
/>
|
|
1174
|
+
<div className="min-w-0">
|
|
1175
|
+
<strong className="block text-sm text-amber-200">
|
|
1176
|
+
{t.liveSessionWallPausedTitle}
|
|
1177
|
+
</strong>
|
|
1178
|
+
<p className="mt-1 text-xs font-normal leading-snug text-amber-300/85">
|
|
1179
|
+
{t.liveSessionWallPausedDetail}
|
|
1180
|
+
</p>
|
|
739
1181
|
</div>
|
|
740
|
-
|
|
741
|
-
|
|
1182
|
+
</div>
|
|
1183
|
+
<button
|
|
1184
|
+
type="button"
|
|
1185
|
+
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"
|
|
1186
|
+
onClick={() => void resumeLiveSessionWall()}
|
|
1187
|
+
>
|
|
1188
|
+
{t.liveSessionResumeWallBtn}
|
|
1189
|
+
</button>
|
|
1190
|
+
</section>
|
|
742
1191
|
) : null}
|
|
743
1192
|
|
|
744
1193
|
<div className="space-y-6">
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
<
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1194
|
+
{showTaskEntryControls ? (
|
|
1195
|
+
<>
|
|
1196
|
+
<div className="mb-5 grid min-h-8 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-2">
|
|
1197
|
+
<span className="shrink-0 text-[0.7rem] font-semibold uppercase tracking-wide text-zinc-500">
|
|
1198
|
+
{t.taskTrackerTitle}
|
|
1199
|
+
</span>
|
|
1200
|
+
<div className="flex min-w-0 justify-center justify-self-stretch px-0.5 sm:px-1">
|
|
1201
|
+
{!isInspecting &&
|
|
1202
|
+
showTaskModeToggle &&
|
|
1203
|
+
effectiveTaskEntryMode === "past" ? (
|
|
1204
|
+
<div className="flex min-w-0 max-w-full flex-col items-center gap-2">
|
|
756
1205
|
<div className="kronosys-past-datetime-blink-twice min-w-0 max-w-full">
|
|
757
1206
|
{pastDateTimeRow}
|
|
758
1207
|
</div>
|
|
759
|
-
|
|
1208
|
+
<div className="kronosys-past-datetime-blink-twice w-full max-w-full">
|
|
1209
|
+
{pastTimerDurationRow}
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
) : null}
|
|
1213
|
+
</div>
|
|
1214
|
+
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 sm:gap-3">
|
|
1215
|
+
{!isInspecting && trackingActive ? (
|
|
1216
|
+
<div
|
|
1217
|
+
className="flex items-center gap-2 text-sm font-bold text-emerald-400"
|
|
1218
|
+
aria-live="polite"
|
|
1219
|
+
>
|
|
1220
|
+
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-current" />
|
|
1221
|
+
<span className="whitespace-nowrap">
|
|
1222
|
+
{lang === "fr" ? "SUIVI" : "TRACKING"}
|
|
1223
|
+
{runningTasks.length > 1 ? (
|
|
1224
|
+
<span className="ml-1.5 font-mono text-xs font-semibold tabular-nums opacity-90">
|
|
1225
|
+
({runningTasks.length})
|
|
1226
|
+
</span>
|
|
1227
|
+
) : null}
|
|
1228
|
+
</span>
|
|
1229
|
+
</div>
|
|
1230
|
+
) : null}
|
|
1231
|
+
{!isInspecting &&
|
|
1232
|
+
showTaskModeToggle &&
|
|
1233
|
+
effectiveTaskEntryMode === "past" ? (
|
|
1234
|
+
<InlineMetricHelpTrigger
|
|
1235
|
+
ariaLabel={t.archiveAddTaskIntroHelpAria}
|
|
1236
|
+
body={t.archiveAddTaskIntro}
|
|
1237
|
+
panelClassName="w-[min(calc(100vw-2rem),22rem)]"
|
|
1238
|
+
/>
|
|
1239
|
+
) : null}
|
|
1240
|
+
</div>
|
|
1241
|
+
</div>
|
|
1242
|
+
|
|
1243
|
+
{isInspecting && inspectingId ? (
|
|
1244
|
+
<div className="min-w-0 space-y-4">
|
|
1245
|
+
<div className="flex justify-end">
|
|
1246
|
+
<InlineMetricHelpTrigger
|
|
1247
|
+
ariaLabel={t.archiveAddTaskIntroHelpAria}
|
|
1248
|
+
body={t.archiveAddTaskIntro}
|
|
1249
|
+
panelClassName="w-[min(calc(100vw-2rem),22rem)]"
|
|
1250
|
+
/>
|
|
760
1251
|
</div>
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1252
|
+
{pastDatetimeRowInspecting}
|
|
1253
|
+
{mergedDraftProject?.trim() ? (
|
|
1254
|
+
<div className="flex min-w-0 justify-start">
|
|
1255
|
+
<span
|
|
1256
|
+
className={`${
|
|
1257
|
+
mergedDraftPersonalProject === true
|
|
1258
|
+
? PROJECT_CHIP_APPLIED_PERSONAL_CLASS
|
|
1259
|
+
: PROJECT_CHIP_APPLIED_CLASS
|
|
1260
|
+
} max-w-full truncate`}
|
|
1261
|
+
title={formatProjectDisplay(mergedDraftProject, {
|
|
1262
|
+
personal: mergedDraftPersonalProject === true,
|
|
1263
|
+
})}
|
|
766
1264
|
>
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1265
|
+
{formatProjectDisplay(mergedDraftProject, {
|
|
1266
|
+
personal: mergedDraftPersonalProject === true,
|
|
1267
|
+
})}
|
|
1268
|
+
</span>
|
|
1269
|
+
</div>
|
|
1270
|
+
) : null}
|
|
1271
|
+
<div className="flex min-h-10 min-w-0 flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end">
|
|
1272
|
+
<input
|
|
1273
|
+
id="kronosys-task-launcher-input"
|
|
1274
|
+
className={`${TASK_FOCUS_LAUNCHER_INPUT_ROW_CLASS} min-w-0 text-center sm:min-w-[12rem]`}
|
|
1275
|
+
placeholder={t.taskPlaceholderPast}
|
|
1276
|
+
value={taskInput}
|
|
1277
|
+
list="kronosys-task-templates"
|
|
1278
|
+
onChange={(e) => setTaskInput(e.target.value)}
|
|
1279
|
+
onKeyDown={(e) => {
|
|
1280
|
+
if (e.key === "Enter" && canSubmitPast) {
|
|
1281
|
+
e.preventDefault();
|
|
1282
|
+
void submitAddHistoricalTask();
|
|
1283
|
+
}
|
|
1284
|
+
}}
|
|
1285
|
+
aria-label={t.taskPlaceholderPast}
|
|
1286
|
+
/>
|
|
1287
|
+
<div className="flex h-10 shrink-0 items-center gap-1">
|
|
1288
|
+
<button
|
|
1289
|
+
type="button"
|
|
1290
|
+
className={tbVioletIcon}
|
|
1291
|
+
disabled={!canSubmitPast}
|
|
1292
|
+
title={t.archiveAddTaskBtn}
|
|
1293
|
+
aria-label={t.archiveAddTaskBtn}
|
|
1294
|
+
onClick={() => void submitAddHistoricalTask()}
|
|
1295
|
+
>
|
|
1296
|
+
<Play
|
|
1297
|
+
size={22}
|
|
1298
|
+
className="ml-0.5"
|
|
1299
|
+
fill="currentColor"
|
|
1300
|
+
aria-hidden
|
|
1301
|
+
/>
|
|
1302
|
+
</button>
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
<textarea
|
|
1306
|
+
className="w-full rounded-lg border border-zinc-300 bg-white/90 px-3 py-2 text-sm text-zinc-800 outline-none transition focus:border-violet-500 dark:border-zinc-700 dark:bg-zinc-900/70 dark:text-zinc-100"
|
|
1307
|
+
placeholder={t.taskNotePlaceholder}
|
|
1308
|
+
value={taskNoteInput}
|
|
1309
|
+
onChange={(e) => setTaskNoteInput(e.target.value)}
|
|
1310
|
+
rows={3}
|
|
1311
|
+
aria-label={t.taskNoteLabel}
|
|
1312
|
+
/>
|
|
1313
|
+
{taskFormTagSection}
|
|
1314
|
+
</div>
|
|
1315
|
+
) : (
|
|
1316
|
+
<div className="min-w-0 space-y-4">
|
|
1317
|
+
{mergedDraftProject?.trim() ? (
|
|
1318
|
+
<div className="flex min-w-0 justify-start">
|
|
1319
|
+
<span
|
|
1320
|
+
className={`${
|
|
1321
|
+
mergedDraftPersonalProject === true
|
|
1322
|
+
? PROJECT_CHIP_APPLIED_PERSONAL_CLASS
|
|
1323
|
+
: PROJECT_CHIP_APPLIED_CLASS
|
|
1324
|
+
} inline-flex items-center gap-1 max-w-full truncate pr-1`}
|
|
1325
|
+
title={formatProjectDisplay(mergedDraftProject, {
|
|
1326
|
+
personal: mergedDraftPersonalProject === true,
|
|
1327
|
+
})}
|
|
1328
|
+
>
|
|
1329
|
+
<span className="truncate">
|
|
1330
|
+
{formatProjectDisplay(mergedDraftProject, {
|
|
1331
|
+
personal: mergedDraftPersonalProject === true,
|
|
1332
|
+
})}
|
|
775
1333
|
</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
1334
|
<button
|
|
790
1335
|
type="button"
|
|
791
|
-
className="
|
|
792
|
-
|
|
793
|
-
aria-label={t.importGitIssue}
|
|
794
|
-
onClick={() => onOpenGitlabIssuePicker()}
|
|
1336
|
+
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full hover:bg-black/10 dark:hover:bg-white/10"
|
|
1337
|
+
onClick={handleRemoveDraftProject}
|
|
795
1338
|
>
|
|
796
|
-
<
|
|
1339
|
+
<span className="text-[0.65rem] font-bold leading-none">
|
|
1340
|
+
×
|
|
1341
|
+
</span>
|
|
797
1342
|
</button>
|
|
798
|
-
</
|
|
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
|
-
/>
|
|
1343
|
+
</span>
|
|
811
1344
|
</div>
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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}
|
|
1345
|
+
) : null}
|
|
1346
|
+
<div className="flex min-h-10 min-w-0 items-center gap-2">
|
|
1347
|
+
{showTaskModeToggle ? (
|
|
1348
|
+
<div
|
|
1349
|
+
role="group"
|
|
1350
|
+
aria-label={t.taskEntryModeGroupAria}
|
|
1351
|
+
className="flex h-10 shrink-0 items-center gap-1"
|
|
1352
|
+
>
|
|
849
1353
|
<button
|
|
850
1354
|
type="button"
|
|
851
|
-
className={
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1355
|
+
className={
|
|
1356
|
+
effectiveTaskEntryMode === "realtime"
|
|
1357
|
+
? tbVioletToggleOn
|
|
1358
|
+
: tbVioletToggleOff
|
|
1359
|
+
}
|
|
1360
|
+
aria-pressed={effectiveTaskEntryMode === "realtime"}
|
|
1361
|
+
title={t.taskEntryModeRealtime}
|
|
1362
|
+
aria-label={t.taskEntryModeRealtime}
|
|
1363
|
+
onClick={() => setTaskEntryMode("realtime")}
|
|
856
1364
|
>
|
|
857
|
-
<
|
|
858
|
-
size={22}
|
|
859
|
-
className="ml-0.5"
|
|
860
|
-
fill="currentColor"
|
|
861
|
-
aria-hidden
|
|
862
|
-
/>
|
|
1365
|
+
<Zap size={18} strokeWidth={2.25} aria-hidden />
|
|
863
1366
|
</button>
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1367
|
+
<button
|
|
1368
|
+
type="button"
|
|
1369
|
+
className={
|
|
1370
|
+
effectiveTaskEntryMode === "past"
|
|
1371
|
+
? tbVioletToggleOn
|
|
1372
|
+
: tbVioletToggleOff
|
|
1373
|
+
}
|
|
1374
|
+
aria-pressed={effectiveTaskEntryMode === "past"}
|
|
1375
|
+
title={t.taskEntryModePast}
|
|
1376
|
+
aria-label={t.taskEntryModePast}
|
|
1377
|
+
onClick={() => setTaskEntryMode("past")}
|
|
875
1378
|
>
|
|
876
|
-
<
|
|
877
|
-
|
|
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>
|
|
1379
|
+
<History size={18} strokeWidth={2} aria-hidden />
|
|
1380
|
+
</button>
|
|
889
1381
|
</div>
|
|
890
1382
|
) : null}
|
|
891
|
-
<
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
|
1383
|
+
<input
|
|
1384
|
+
id="kronosys-task-launcher-input"
|
|
1385
|
+
className={`${TASK_FOCUS_LAUNCHER_INPUT_ROW_CLASS} min-w-0 flex-1 text-center`}
|
|
1386
|
+
placeholder={
|
|
1387
|
+
effectiveTaskEntryMode === "past"
|
|
1388
|
+
? t.taskPlaceholderPast
|
|
1389
|
+
: t.taskPlaceholder
|
|
1390
|
+
}
|
|
1391
|
+
value={taskInput}
|
|
1392
|
+
list="kronosys-task-templates"
|
|
1393
|
+
onChange={(e) => setTaskInput(e.target.value)}
|
|
1394
|
+
onKeyDown={(e) => {
|
|
1395
|
+
if (e.key !== "Enter") {
|
|
1396
|
+
return;
|
|
934
1397
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
|
1398
|
+
if (
|
|
1399
|
+
effectiveTaskEntryMode === "realtime" &&
|
|
1400
|
+
taskInput.trim()
|
|
1401
|
+
) {
|
|
1402
|
+
e.preventDefault();
|
|
1403
|
+
void submitStartTask();
|
|
1404
|
+
return;
|
|
962
1405
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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}
|
|
1406
|
+
if (
|
|
1407
|
+
effectiveTaskEntryMode === "past" &&
|
|
1408
|
+
canSubmitPast &&
|
|
1409
|
+
retroTargetSessionId
|
|
1410
|
+
) {
|
|
1411
|
+
e.preventDefault();
|
|
1412
|
+
void submitAddHistoricalTask();
|
|
1413
|
+
}
|
|
1414
|
+
}}
|
|
1415
|
+
aria-label={
|
|
1416
|
+
effectiveTaskEntryMode === "past"
|
|
1417
|
+
? t.taskPlaceholderPast
|
|
1418
|
+
: t.taskPlaceholder
|
|
1419
|
+
}
|
|
1420
|
+
/>
|
|
1421
|
+
<div className="flex h-10 shrink-0 items-center gap-1">
|
|
1422
|
+
{effectiveTaskEntryMode === "realtime" ? (
|
|
1423
|
+
<>
|
|
1424
|
+
{showKronoFocusInTaskOps ? (
|
|
992
1425
|
<button
|
|
993
1426
|
type="button"
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1427
|
+
role="switch"
|
|
1428
|
+
aria-checked={
|
|
1429
|
+
startKronoFocusWithTask ? "true" : "false"
|
|
1430
|
+
}
|
|
1431
|
+
aria-label={
|
|
1432
|
+
startKronoFocusWithTask
|
|
1433
|
+
? t.startTaskWithKronoFocusToggleAriaOn
|
|
1434
|
+
: t.startTaskWithKronoFocusToggleAriaOff
|
|
1435
|
+
}
|
|
1436
|
+
title={t.startTaskWithKronoFocus}
|
|
1437
|
+
onClick={() =>
|
|
1438
|
+
setStartKronoFocusWithTask((v) => !v)
|
|
1439
|
+
}
|
|
1440
|
+
className={`inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg transition-colors [&_svg]:shrink-0 ${
|
|
1441
|
+
startKronoFocusWithTask
|
|
1442
|
+
? "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"
|
|
1443
|
+
: "border border-zinc-300 bg-white/80 text-zinc-500 dark:border-zinc-700 dark:bg-zinc-950/50 dark:text-zinc-400"
|
|
1444
|
+
} hover:bg-zinc-100/90 dark:hover:bg-zinc-800/40`}
|
|
999
1445
|
>
|
|
1000
|
-
<
|
|
1001
|
-
size={22}
|
|
1002
|
-
className="ml-0.5"
|
|
1003
|
-
fill="currentColor"
|
|
1004
|
-
aria-hidden
|
|
1005
|
-
/>
|
|
1446
|
+
<Timer size={20} strokeWidth={2} aria-hidden />
|
|
1006
1447
|
</button>
|
|
1007
|
-
|
|
1008
|
-
) : (
|
|
1448
|
+
) : null}
|
|
1009
1449
|
<button
|
|
1010
1450
|
type="button"
|
|
1011
1451
|
className={tbVioletIcon}
|
|
1012
|
-
disabled={!
|
|
1013
|
-
title={t.
|
|
1014
|
-
aria-label={t.
|
|
1015
|
-
onClick={() => void
|
|
1452
|
+
disabled={!taskInput.trim()}
|
|
1453
|
+
title={t.startTaskBtn}
|
|
1454
|
+
aria-label={t.startTaskBtn}
|
|
1455
|
+
onClick={() => void submitStartTask()}
|
|
1016
1456
|
>
|
|
1017
1457
|
<Play
|
|
1018
1458
|
size={22}
|
|
@@ -1021,30 +1461,78 @@ export function TaskFocusPanel({
|
|
|
1021
1461
|
aria-hidden
|
|
1022
1462
|
/>
|
|
1023
1463
|
</button>
|
|
1024
|
-
|
|
1025
|
-
|
|
1464
|
+
</>
|
|
1465
|
+
) : (
|
|
1466
|
+
<button
|
|
1467
|
+
type="button"
|
|
1468
|
+
className={tbVioletIcon}
|
|
1469
|
+
disabled={!canSubmitPast || !retroTargetSessionId}
|
|
1470
|
+
title={t.archiveAddTaskBtn}
|
|
1471
|
+
aria-label={t.archiveAddTaskBtn}
|
|
1472
|
+
onClick={() => void submitAddHistoricalTask()}
|
|
1473
|
+
>
|
|
1474
|
+
<Play
|
|
1475
|
+
size={22}
|
|
1476
|
+
className="ml-0.5"
|
|
1477
|
+
fill="currentColor"
|
|
1478
|
+
aria-hidden
|
|
1479
|
+
/>
|
|
1480
|
+
</button>
|
|
1481
|
+
)}
|
|
1026
1482
|
</div>
|
|
1027
|
-
{taskFormTagSection}
|
|
1028
1483
|
</div>
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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>
|
|
1484
|
+
<textarea
|
|
1485
|
+
className="w-full rounded-lg border border-zinc-300 bg-white/90 px-3 py-2 text-sm text-zinc-800 outline-none transition focus:border-violet-500 dark:border-zinc-700 dark:bg-zinc-900/70 dark:text-zinc-100"
|
|
1486
|
+
placeholder={t.taskNotePlaceholder}
|
|
1487
|
+
value={taskNoteInput}
|
|
1488
|
+
onChange={(e) => setTaskNoteInput(e.target.value)}
|
|
1489
|
+
rows={3}
|
|
1490
|
+
aria-label={t.taskNoteLabel}
|
|
1491
|
+
/>
|
|
1492
|
+
{taskFormTagSection}
|
|
1042
1493
|
</div>
|
|
1043
|
-
|
|
1044
|
-
|
|
1494
|
+
)}
|
|
1495
|
+
<datalist id="kronosys-task-templates">
|
|
1496
|
+
{filteredTaskTemplates.map((tpl) => (
|
|
1497
|
+
<option key={tpl.id} value={formatTaskTemplateDraftLine(tpl)}>
|
|
1498
|
+
{formatTaskTemplateDatalistLabel(
|
|
1499
|
+
tpl,
|
|
1500
|
+
t.taskTemplateDatalistPrefix,
|
|
1501
|
+
)}
|
|
1502
|
+
</option>
|
|
1503
|
+
))}
|
|
1504
|
+
</datalist>
|
|
1505
|
+
</>
|
|
1506
|
+
) : null}
|
|
1507
|
+
|
|
1508
|
+
{!isInspecting && runningTasks.length > 0 ? (
|
|
1509
|
+
<div
|
|
1510
|
+
className={`min-w-0 overflow-hidden pt-2 transition-[opacity,transform,max-height] duration-300 ease-out motion-reduce:transition-none ${
|
|
1511
|
+
taskListVisibleSessionId !== null &&
|
|
1512
|
+
taskListEnterSessionId === taskListVisibleSessionId
|
|
1513
|
+
? "max-h-[1000rem] translate-y-0 opacity-100"
|
|
1514
|
+
: "max-h-0 translate-y-1 opacity-0"
|
|
1515
|
+
}`}
|
|
1516
|
+
>
|
|
1517
|
+
<div className="mb-3 flex items-baseline justify-between gap-3">
|
|
1518
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
|
1519
|
+
{t.tasksRunningHeading}
|
|
1520
|
+
</h3>
|
|
1521
|
+
<span className="tabular-nums text-[0.7rem] font-medium text-emerald-600 dark:text-emerald-400/90">
|
|
1522
|
+
{runningTasks.length}
|
|
1523
|
+
</span>
|
|
1524
|
+
</div>
|
|
1525
|
+
<div className="space-y-3">
|
|
1526
|
+
{runningTasks.map((task) => (
|
|
1527
|
+
<div
|
|
1528
|
+
key={task.id}
|
|
1529
|
+
className={`transition-all duration-150 ease-out motion-reduce:transition-none ${
|
|
1530
|
+
deletingTaskIds.has(String(task.id))
|
|
1531
|
+
? "pointer-events-none translate-y-1 scale-[0.985] opacity-0"
|
|
1532
|
+
: "translate-y-0 scale-100 opacity-100"
|
|
1533
|
+
}`}
|
|
1534
|
+
>
|
|
1045
1535
|
<TaskSessionLiveCard
|
|
1046
|
-
key={task.id}
|
|
1047
|
-
variant="plain"
|
|
1048
1536
|
task={task}
|
|
1049
1537
|
lang={lang}
|
|
1050
1538
|
displayTimeZone={displayTimeZone}
|
|
@@ -1061,6 +1549,9 @@ export function TaskFocusPanel({
|
|
|
1061
1549
|
startKronoFocusFromTask={() =>
|
|
1062
1550
|
void startKronoFocusFromTask()
|
|
1063
1551
|
}
|
|
1552
|
+
onDuplicateTask={duplicateTaskToDraft}
|
|
1553
|
+
onSaveAsTemplate={(task) => void saveTaskAsTemplate(task)}
|
|
1554
|
+
isAlreadyTemplate={isTaskAlreadyTemplate(task)}
|
|
1064
1555
|
pausePlayMode="pause"
|
|
1065
1556
|
onPausePlay={() =>
|
|
1066
1557
|
void post({
|
|
@@ -1073,15 +1564,22 @@ export function TaskFocusPanel({
|
|
|
1073
1564
|
allowTaskStartTimeEdit={allowTaskStartTimeEdit}
|
|
1074
1565
|
allowTaskEndTimeEdit={allowTaskEndTimeEdit}
|
|
1075
1566
|
/>
|
|
1076
|
-
|
|
1077
|
-
|
|
1567
|
+
</div>
|
|
1568
|
+
))}
|
|
1078
1569
|
</div>
|
|
1079
|
-
|
|
1080
|
-
|
|
1570
|
+
</div>
|
|
1571
|
+
) : null}
|
|
1081
1572
|
</div>
|
|
1082
1573
|
|
|
1083
1574
|
{showTaskListBuckets ? (
|
|
1084
|
-
<div
|
|
1575
|
+
<div
|
|
1576
|
+
className={`mt-8 space-y-10 overflow-hidden transition-[opacity,transform,max-height] duration-300 ease-out motion-reduce:transition-none ${
|
|
1577
|
+
taskListVisibleSessionId !== null &&
|
|
1578
|
+
taskListEnterSessionId === taskListVisibleSessionId
|
|
1579
|
+
? "max-h-[1000rem] translate-y-0 opacity-100"
|
|
1580
|
+
: "max-h-0 translate-y-1 opacity-0"
|
|
1581
|
+
}`}
|
|
1582
|
+
>
|
|
1085
1583
|
{showSessionTaskCountBadge ? (
|
|
1086
1584
|
<div className="flex justify-end">
|
|
1087
1585
|
<span className="rounded-full border border-zinc-600 px-2 py-0.5 text-xs text-zinc-400">
|
|
@@ -1090,6 +1588,86 @@ export function TaskFocusPanel({
|
|
|
1090
1588
|
</div>
|
|
1091
1589
|
) : null}
|
|
1092
1590
|
|
|
1591
|
+
{showPlannedTaskSection ? (
|
|
1592
|
+
<div className="space-y-2">
|
|
1593
|
+
<div className="flex items-center justify-between gap-2">
|
|
1594
|
+
<h4 className="text-xs font-semibold uppercase tracking-wide text-sky-700 dark:text-sky-300/90">
|
|
1595
|
+
{t.tasksPlannedHeading}
|
|
1596
|
+
</h4>
|
|
1597
|
+
<span className="rounded-full border border-sky-600/50 bg-sky-50/80 px-2 py-0.5 text-[0.65rem] text-sky-900 dark:border-sky-500/45 dark:bg-sky-950/40 dark:text-sky-200">
|
|
1598
|
+
{plannedTasksDisplay.length}
|
|
1599
|
+
</span>
|
|
1600
|
+
</div>
|
|
1601
|
+
<div className="space-y-3">
|
|
1602
|
+
{plannedTasksDisplay.map((task) => {
|
|
1603
|
+
const stackRow = runningTasksRaw.find(
|
|
1604
|
+
(r) => String(r.id) === String(task.id),
|
|
1605
|
+
);
|
|
1606
|
+
const pauseMode: "pause" | "resume" | null = task.isDone
|
|
1607
|
+
? null
|
|
1608
|
+
: stackRow && !stackRow.manualTaskTimerPaused
|
|
1609
|
+
? "pause"
|
|
1610
|
+
: "resume";
|
|
1611
|
+
return (
|
|
1612
|
+
<div
|
|
1613
|
+
key={task.id}
|
|
1614
|
+
className={`transition-all duration-150 ease-out motion-reduce:transition-none ${
|
|
1615
|
+
deletingTaskIds.has(String(task.id))
|
|
1616
|
+
? "pointer-events-none translate-y-1 scale-[0.985] opacity-0"
|
|
1617
|
+
: "translate-y-0 scale-100 opacity-100"
|
|
1618
|
+
}`}
|
|
1619
|
+
>
|
|
1620
|
+
<TaskSessionLiveCard
|
|
1621
|
+
task={task}
|
|
1622
|
+
lang={lang}
|
|
1623
|
+
displayTimeZone={displayTimeZone}
|
|
1624
|
+
use24HourClock={use24HourClock}
|
|
1625
|
+
isInspecting={isInspecting}
|
|
1626
|
+
inspectingId={inspectingId}
|
|
1627
|
+
knownTags={knownTags}
|
|
1628
|
+
knownProjects={knownProjects}
|
|
1629
|
+
post={post}
|
|
1630
|
+
t={t}
|
|
1631
|
+
confirmDeleteTask={confirmDeleteTask}
|
|
1632
|
+
kronoFocusIsRunningOrPaused={
|
|
1633
|
+
kronoFocusIsRunningOrPaused
|
|
1634
|
+
}
|
|
1635
|
+
showKronoFocusTaskActions={showKronoFocusInTaskOps}
|
|
1636
|
+
startKronoFocusFromTask={() =>
|
|
1637
|
+
void startKronoFocusFromTask()
|
|
1638
|
+
}
|
|
1639
|
+
onDuplicateTask={duplicateTaskToDraft}
|
|
1640
|
+
onSaveAsTemplate={(task) =>
|
|
1641
|
+
void saveTaskAsTemplate(task)
|
|
1642
|
+
}
|
|
1643
|
+
isAlreadyTemplate={isTaskAlreadyTemplate(task)}
|
|
1644
|
+
pausePlayMode={pauseMode}
|
|
1645
|
+
temporalPlanned
|
|
1646
|
+
onPausePlay={() => {
|
|
1647
|
+
if (pauseMode === "pause") {
|
|
1648
|
+
void post({
|
|
1649
|
+
type: "setTaskTimerPaused",
|
|
1650
|
+
taskId: task.id,
|
|
1651
|
+
paused: true,
|
|
1652
|
+
});
|
|
1653
|
+
} else if (pauseMode === "resume") {
|
|
1654
|
+
void post({
|
|
1655
|
+
type: "resumePausedTask",
|
|
1656
|
+
taskId: task.id,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}}
|
|
1660
|
+
anchorId={`kronosys-task-${task.id}`}
|
|
1661
|
+
allowTaskStartTimeEdit={allowTaskStartTimeEdit}
|
|
1662
|
+
allowTaskEndTimeEdit={allowTaskEndTimeEdit}
|
|
1663
|
+
/>
|
|
1664
|
+
</div>
|
|
1665
|
+
);
|
|
1666
|
+
})}
|
|
1667
|
+
</div>
|
|
1668
|
+
</div>
|
|
1669
|
+
) : null}
|
|
1670
|
+
|
|
1093
1671
|
{showPausedTaskSection ? (
|
|
1094
1672
|
<div className="space-y-2">
|
|
1095
1673
|
<div className="flex items-center justify-between gap-2">
|
|
@@ -1102,32 +1680,43 @@ export function TaskFocusPanel({
|
|
|
1102
1680
|
</div>
|
|
1103
1681
|
<div className="space-y-3">
|
|
1104
1682
|
{pausedTasksDisplay.map((task) => (
|
|
1105
|
-
<
|
|
1683
|
+
<div
|
|
1106
1684
|
key={task.id}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1685
|
+
className={`transition-all duration-150 ease-out motion-reduce:transition-none ${
|
|
1686
|
+
deletingTaskIds.has(String(task.id))
|
|
1687
|
+
? "pointer-events-none translate-y-1 scale-[0.985] opacity-0"
|
|
1688
|
+
: "translate-y-0 scale-100 opacity-100"
|
|
1689
|
+
}`}
|
|
1690
|
+
>
|
|
1691
|
+
<TaskSessionLiveCard
|
|
1692
|
+
task={task}
|
|
1693
|
+
lang={lang}
|
|
1694
|
+
displayTimeZone={displayTimeZone}
|
|
1695
|
+
use24HourClock={use24HourClock}
|
|
1696
|
+
isInspecting={isInspecting}
|
|
1697
|
+
inspectingId={inspectingId}
|
|
1698
|
+
knownTags={knownTags}
|
|
1699
|
+
knownProjects={knownProjects}
|
|
1700
|
+
post={post}
|
|
1701
|
+
t={t}
|
|
1702
|
+
confirmDeleteTask={confirmDeleteTask}
|
|
1703
|
+
kronoFocusIsRunningOrPaused={kronoFocusIsRunningOrPaused}
|
|
1704
|
+
showKronoFocusTaskActions={showKronoFocusInTaskOps}
|
|
1705
|
+
startKronoFocusFromTask={() =>
|
|
1706
|
+
void startKronoFocusFromTask()
|
|
1707
|
+
}
|
|
1708
|
+
onDuplicateTask={duplicateTaskToDraft}
|
|
1709
|
+
onSaveAsTemplate={(task) => void saveTaskAsTemplate(task)}
|
|
1710
|
+
isAlreadyTemplate={isTaskAlreadyTemplate(task)}
|
|
1711
|
+
pausePlayMode="resume"
|
|
1712
|
+
onPausePlay={() =>
|
|
1713
|
+
void post({ type: "resumePausedTask", taskId: task.id })
|
|
1714
|
+
}
|
|
1715
|
+
anchorId={`kronosys-task-${task.id}`}
|
|
1716
|
+
allowTaskStartTimeEdit={allowTaskStartTimeEdit}
|
|
1717
|
+
allowTaskEndTimeEdit={allowTaskEndTimeEdit}
|
|
1718
|
+
/>
|
|
1719
|
+
</div>
|
|
1131
1720
|
))}
|
|
1132
1721
|
</div>
|
|
1133
1722
|
</div>
|
|
@@ -1145,30 +1734,41 @@ export function TaskFocusPanel({
|
|
|
1145
1734
|
</div>
|
|
1146
1735
|
<div className="space-y-3">
|
|
1147
1736
|
{completedTasksDisplay.map((task) => (
|
|
1148
|
-
<
|
|
1737
|
+
<div
|
|
1149
1738
|
key={task.id}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1739
|
+
className={`transition-all duration-150 ease-out motion-reduce:transition-none ${
|
|
1740
|
+
deletingTaskIds.has(String(task.id))
|
|
1741
|
+
? "pointer-events-none translate-y-1 scale-[0.985] opacity-0"
|
|
1742
|
+
: "translate-y-0 scale-100 opacity-100"
|
|
1743
|
+
}`}
|
|
1744
|
+
>
|
|
1745
|
+
<TaskSessionLiveCard
|
|
1746
|
+
task={task}
|
|
1747
|
+
lang={lang}
|
|
1748
|
+
displayTimeZone={displayTimeZone}
|
|
1749
|
+
use24HourClock={use24HourClock}
|
|
1750
|
+
isInspecting={isInspecting}
|
|
1751
|
+
inspectingId={inspectingId}
|
|
1752
|
+
knownTags={knownTags}
|
|
1753
|
+
knownProjects={knownProjects}
|
|
1754
|
+
post={post}
|
|
1755
|
+
t={t}
|
|
1756
|
+
confirmDeleteTask={confirmDeleteTask}
|
|
1757
|
+
kronoFocusIsRunningOrPaused={kronoFocusIsRunningOrPaused}
|
|
1758
|
+
showKronoFocusTaskActions={showKronoFocusInTaskOps}
|
|
1759
|
+
startKronoFocusFromTask={() =>
|
|
1760
|
+
void startKronoFocusFromTask()
|
|
1761
|
+
}
|
|
1762
|
+
onDuplicateTask={duplicateTaskToDraft}
|
|
1763
|
+
onSaveAsTemplate={(task) => void saveTaskAsTemplate(task)}
|
|
1764
|
+
isAlreadyTemplate={isTaskAlreadyTemplate(task)}
|
|
1765
|
+
pausePlayMode={null}
|
|
1766
|
+
onPausePlay={() => {}}
|
|
1767
|
+
anchorId={`kronosys-task-${task.id}`}
|
|
1768
|
+
allowTaskStartTimeEdit={allowTaskStartTimeEdit}
|
|
1769
|
+
allowTaskEndTimeEdit={allowTaskEndTimeEdit}
|
|
1770
|
+
/>
|
|
1771
|
+
</div>
|
|
1172
1772
|
))}
|
|
1173
1773
|
</div>
|
|
1174
1774
|
</div>
|
|
@@ -1176,19 +1776,6 @@ export function TaskFocusPanel({
|
|
|
1176
1776
|
</div>
|
|
1177
1777
|
) : null}
|
|
1178
1778
|
|
|
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
1779
|
<DashboardAlertModal
|
|
1193
1780
|
open={alertMessage !== null}
|
|
1194
1781
|
message={alertMessage ?? ""}
|
|
@@ -1214,11 +1801,27 @@ export function TaskFocusPanel({
|
|
|
1214
1801
|
const id = deleteTaskConfirmId;
|
|
1215
1802
|
setDeleteTaskConfirmId(null);
|
|
1216
1803
|
if (id) {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1804
|
+
setDeletingTaskIds((prev) => {
|
|
1805
|
+
const next = new Set(prev);
|
|
1806
|
+
next.add(String(id));
|
|
1807
|
+
return next;
|
|
1221
1808
|
});
|
|
1809
|
+
globalThis.setTimeout(() => {
|
|
1810
|
+
void post({
|
|
1811
|
+
type: "deleteTask",
|
|
1812
|
+
taskId: id,
|
|
1813
|
+
sessionId: inspectingId,
|
|
1814
|
+
}).finally(() => {
|
|
1815
|
+
setDeletingTaskIds((prev) => {
|
|
1816
|
+
if (!prev.has(String(id))) {
|
|
1817
|
+
return prev;
|
|
1818
|
+
}
|
|
1819
|
+
const next = new Set(prev);
|
|
1820
|
+
next.delete(String(id));
|
|
1821
|
+
return next;
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
}, TASK_DELETE_ANIM_MS);
|
|
1222
1825
|
}
|
|
1223
1826
|
}}
|
|
1224
1827
|
/>
|