@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,832 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Timer,
|
|
6
|
+
Play,
|
|
7
|
+
Pause,
|
|
8
|
+
CheckCircle2,
|
|
9
|
+
Trash2,
|
|
10
|
+
GitCommit,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
|
|
13
|
+
import { formatIsoInstantShort } from "@/lib/formatIsoShort";
|
|
14
|
+
import { DEFAULT_DASHBOARD_TIME_ZONE } from "@/lib/dashboardTimeZone";
|
|
15
|
+
import {
|
|
16
|
+
filterTaskTagsForProject,
|
|
17
|
+
formatDuration,
|
|
18
|
+
formatStopwatchMs,
|
|
19
|
+
formatProjectDisplay,
|
|
20
|
+
mergeTagsForDisplay,
|
|
21
|
+
normalizeTagKey,
|
|
22
|
+
parseTaskWithAutoTags,
|
|
23
|
+
resolveProjectForTaskUpdate,
|
|
24
|
+
taskTitleEditBaseline,
|
|
25
|
+
taskTitleForDisplay,
|
|
26
|
+
} from "@/lib/taskParsing";
|
|
27
|
+
import { TagPills } from "./TagPills";
|
|
28
|
+
import { tbEmeraldIcon } from "@/lib/translucentButtonClasses";
|
|
29
|
+
import {
|
|
30
|
+
PROJECT_CHIP_APPLIED_CLASS,
|
|
31
|
+
TASK_ACTIVE_TITLE_EDIT_INPUT_CLASS,
|
|
32
|
+
TASK_ACTIVE_TITLE_READ_CLASS,
|
|
33
|
+
} from "./taskFieldStyles";
|
|
34
|
+
import { TaskSubtasksBlock } from "./TaskSubtasksBlock";
|
|
35
|
+
import { DashboardConfirmModal } from "./DashboardSimpleModal";
|
|
36
|
+
import { useSmoothStopwatchDisplayMs } from "./useSmoothStopwatchMs";
|
|
37
|
+
import {
|
|
38
|
+
KronosysDatetimePopoverField,
|
|
39
|
+
formatDatetimeLocalValue,
|
|
40
|
+
} from "./KronosysDatetimePopoverField";
|
|
41
|
+
|
|
42
|
+
const FINISH_TASK_SUBTASKS_WARN_STORAGE_KEY =
|
|
43
|
+
"kronosys_finish_task_subtasks_warn_dismiss_v1";
|
|
44
|
+
|
|
45
|
+
function readFinishSubtasksWarnDismissed(): boolean {
|
|
46
|
+
try {
|
|
47
|
+
return (
|
|
48
|
+
globalThis.localStorage?.getItem(
|
|
49
|
+
FINISH_TASK_SUBTASKS_WARN_STORAGE_KEY,
|
|
50
|
+
) === "1"
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function persistFinishSubtasksWarnDismissed(): void {
|
|
58
|
+
try {
|
|
59
|
+
globalThis.localStorage?.setItem(
|
|
60
|
+
FINISH_TASK_SUBTASKS_WARN_STORAGE_KEY,
|
|
61
|
+
"1",
|
|
62
|
+
);
|
|
63
|
+
} catch {
|
|
64
|
+
/* quota / navigation privée */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DONE_TITLE_READ_CLASS =
|
|
69
|
+
"text-zinc-500 line-through decoration-zinc-400/[0.35] decoration-[1.5px] dark:text-zinc-300 dark:decoration-zinc-500/[0.35]";
|
|
70
|
+
|
|
71
|
+
const DONE_TITLE_PLAIN_CLASS =
|
|
72
|
+
"text-zinc-500 line-through decoration-zinc-400/[0.35] decoration-[1.5px] dark:text-zinc-400 dark:decoration-zinc-500/[0.35]";
|
|
73
|
+
|
|
74
|
+
const TASK_CARD_SHELL_CLASS =
|
|
75
|
+
"rounded-xl border border-zinc-200 bg-zinc-100/80 p-5 dark:border-zinc-800 dark:bg-black/20 sm:p-6";
|
|
76
|
+
|
|
77
|
+
/** Tâches en cours : pas de cadre, intégration dans le flux. */
|
|
78
|
+
const TASK_CARD_SHELL_PLAIN_CLASS =
|
|
79
|
+
"rounded-none border-0 bg-transparent py-4 shadow-none first:pt-2 dark:bg-transparent";
|
|
80
|
+
|
|
81
|
+
export type TaskSessionLiveCardTask = {
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
durationMs: number;
|
|
85
|
+
isDone: boolean;
|
|
86
|
+
/** Début du suivi ou de l’entrée passée (ISO 8601), si connu. */
|
|
87
|
+
startTime?: string;
|
|
88
|
+
/** Fin du suivi ou de l’entrée passée (ISO 8601), si connu. */
|
|
89
|
+
endTime?: string;
|
|
90
|
+
tags?: string[];
|
|
91
|
+
project?: string | null;
|
|
92
|
+
subtasks?: Array<{
|
|
93
|
+
id: string;
|
|
94
|
+
title: string;
|
|
95
|
+
done: boolean;
|
|
96
|
+
durationMs?: number;
|
|
97
|
+
}>;
|
|
98
|
+
activeSubtaskTimerId?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function TaskTimingFieldRow({
|
|
102
|
+
label,
|
|
103
|
+
displayValue,
|
|
104
|
+
editable,
|
|
105
|
+
draftValue,
|
|
106
|
+
onDraftChange,
|
|
107
|
+
onDraftBlur,
|
|
108
|
+
lang,
|
|
109
|
+
t,
|
|
110
|
+
}: {
|
|
111
|
+
label: string;
|
|
112
|
+
displayValue: string | null;
|
|
113
|
+
editable: boolean;
|
|
114
|
+
draftValue: string;
|
|
115
|
+
onDraftChange: (next: string) => void;
|
|
116
|
+
onDraftBlur: () => void;
|
|
117
|
+
lang: Lang;
|
|
118
|
+
t: DashboardStrings;
|
|
119
|
+
}) {
|
|
120
|
+
if (!editable && !displayValue) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return (
|
|
124
|
+
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-2 text-[0.7rem] leading-snug text-zinc-600 dark:text-zinc-400">
|
|
125
|
+
<span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
126
|
+
{label}
|
|
127
|
+
</span>
|
|
128
|
+
{editable ? (
|
|
129
|
+
<>
|
|
130
|
+
<KronosysDatetimePopoverField
|
|
131
|
+
value={draftValue}
|
|
132
|
+
onChange={onDraftChange}
|
|
133
|
+
onBlur={onDraftBlur}
|
|
134
|
+
aria-label={label}
|
|
135
|
+
lang={lang}
|
|
136
|
+
t={t}
|
|
137
|
+
/>
|
|
138
|
+
</>
|
|
139
|
+
) : (
|
|
140
|
+
<span className="min-w-0 tabular-nums text-zinc-800 dark:text-zinc-200">
|
|
141
|
+
{displayValue}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolvedDoneTaskDurationMinutes(
|
|
149
|
+
task: TaskSessionLiveCardTask,
|
|
150
|
+
): number | null {
|
|
151
|
+
const ms = task.durationMs;
|
|
152
|
+
if (typeof ms === "number" && Number.isFinite(ms) && ms > 0) {
|
|
153
|
+
return ms / 60000;
|
|
154
|
+
}
|
|
155
|
+
const st =
|
|
156
|
+
typeof task.startTime === "string"
|
|
157
|
+
? new Date(task.startTime).getTime()
|
|
158
|
+
: Number.NaN;
|
|
159
|
+
const en =
|
|
160
|
+
typeof task.endTime === "string"
|
|
161
|
+
? new Date(task.endTime).getTime()
|
|
162
|
+
: Number.NaN;
|
|
163
|
+
if (Number.isFinite(st) && Number.isFinite(en) && en >= st) {
|
|
164
|
+
return (en - st) / 60000;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function TaskSessionLiveCard({
|
|
170
|
+
task,
|
|
171
|
+
lang,
|
|
172
|
+
displayTimeZone = DEFAULT_DASHBOARD_TIME_ZONE,
|
|
173
|
+
use24HourClock = true,
|
|
174
|
+
isInspecting,
|
|
175
|
+
inspectingId,
|
|
176
|
+
knownTags,
|
|
177
|
+
knownProjects,
|
|
178
|
+
post,
|
|
179
|
+
t,
|
|
180
|
+
confirmDeleteTask,
|
|
181
|
+
kronoFocusIsRunningOrPaused,
|
|
182
|
+
showKronoFocusTaskActions = true,
|
|
183
|
+
startKronoFocusFromTask,
|
|
184
|
+
pausePlayMode,
|
|
185
|
+
onPausePlay,
|
|
186
|
+
anchorId,
|
|
187
|
+
variant = "card",
|
|
188
|
+
allowTaskStartTimeEdit = true,
|
|
189
|
+
allowTaskEndTimeEdit = true,
|
|
190
|
+
}: {
|
|
191
|
+
task: TaskSessionLiveCardTask;
|
|
192
|
+
lang: Lang;
|
|
193
|
+
displayTimeZone?: string;
|
|
194
|
+
use24HourClock?: boolean;
|
|
195
|
+
isInspecting: boolean;
|
|
196
|
+
inspectingId: string | null;
|
|
197
|
+
knownTags: string[];
|
|
198
|
+
knownProjects: string[];
|
|
199
|
+
post: (body: Record<string, unknown>) => Promise<void>;
|
|
200
|
+
t: DashboardStrings;
|
|
201
|
+
confirmDeleteTask: (taskId: string) => void;
|
|
202
|
+
kronoFocusIsRunningOrPaused: boolean;
|
|
203
|
+
/** Bouton « démarrer le KronoFocus » sur les tâches non terminées (paramètre tableau de bord). */
|
|
204
|
+
showKronoFocusTaskActions?: boolean;
|
|
205
|
+
startKronoFocusFromTask: () => void;
|
|
206
|
+
/** `null` : tâche terminée (pas de pause / reprise). */
|
|
207
|
+
pausePlayMode: "pause" | "resume" | null;
|
|
208
|
+
onPausePlay: () => void;
|
|
209
|
+
/** Ex. `kronosys-active-task-<id>` pour ancre KronoFocus. */
|
|
210
|
+
anchorId?: string;
|
|
211
|
+
/** `plain` : sans bordure ni fond (ex. tâches au minuteur). */
|
|
212
|
+
variant?: "card" | "plain";
|
|
213
|
+
/** Option `dashboardAllowTaskStartTimeEdit` : correction du `startTime`. */
|
|
214
|
+
allowTaskStartTimeEdit?: boolean;
|
|
215
|
+
/** Option `dashboardAllowTaskEndTimeEdit` : correction du `endTime` (tâches terminées). */
|
|
216
|
+
allowTaskEndTimeEdit?: boolean;
|
|
217
|
+
}) {
|
|
218
|
+
const [shouldCommit, setShouldCommit] = useState(false);
|
|
219
|
+
const [finishOpenSubtasksWarnOpen, setFinishOpenSubtasksWarnOpen] =
|
|
220
|
+
useState(false);
|
|
221
|
+
const [dontRemindOpenSubtasksFinish, setDontRemindOpenSubtasksFinish] =
|
|
222
|
+
useState(false);
|
|
223
|
+
const [titleEditing, setTitleEditing] = useState(false);
|
|
224
|
+
const [titleDraft, setTitleDraft] = useState("");
|
|
225
|
+
const [taskStartDraft, setTaskStartDraft] = useState("");
|
|
226
|
+
const [optimisticLiveBaseMs, setOptimisticLiveBaseMs] = useState<
|
|
227
|
+
number | null
|
|
228
|
+
>(null);
|
|
229
|
+
const [derivedLiveBaseMs, setDerivedLiveBaseMs] = useState<number | null>(
|
|
230
|
+
null,
|
|
231
|
+
);
|
|
232
|
+
const [taskEndDraft, setTaskEndDraft] = useState("");
|
|
233
|
+
const [optimisticDoneDurationMin, setOptimisticDoneDurationMin] = useState<
|
|
234
|
+
number | null
|
|
235
|
+
>(null);
|
|
236
|
+
const titleInputRef = useRef<HTMLInputElement>(null);
|
|
237
|
+
const skipTitleBlurCommitRef = useRef(false);
|
|
238
|
+
|
|
239
|
+
const mergedTags = mergeTagsForDisplay(task.name, task.tags);
|
|
240
|
+
const trimmedProject = task.project?.trim() ?? "";
|
|
241
|
+
/** Affichage lecture seule : sans `#tags` ni `@projet` (pastilles en dessous). */
|
|
242
|
+
const titleDisplay = taskTitleForDisplay(task.name);
|
|
243
|
+
/** Valeur initiale à l’édition : libellé seul ; on peut toujours saisir `#tag` ou `@projet` dans le champ. */
|
|
244
|
+
const titleDraftSource = taskTitleEditBaseline(task.name);
|
|
245
|
+
const isDone = task.isDone === true;
|
|
246
|
+
const smoothTaskTimer = !isDone && !isInspecting && pausePlayMode === "pause";
|
|
247
|
+
const taskStopwatchBaseMs =
|
|
248
|
+
optimisticLiveBaseMs !== null
|
|
249
|
+
? optimisticLiveBaseMs
|
|
250
|
+
: derivedLiveBaseMs !== null
|
|
251
|
+
? derivedLiveBaseMs
|
|
252
|
+
: Math.max(0, task.durationMs ?? 0);
|
|
253
|
+
const liveTaskStopwatchMs = useSmoothStopwatchDisplayMs(
|
|
254
|
+
taskStopwatchBaseMs,
|
|
255
|
+
smoothTaskTimer,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const startFmt =
|
|
259
|
+
typeof task.startTime === "string"
|
|
260
|
+
? formatIsoInstantShort(
|
|
261
|
+
task.startTime,
|
|
262
|
+
lang,
|
|
263
|
+
displayTimeZone,
|
|
264
|
+
use24HourClock,
|
|
265
|
+
)
|
|
266
|
+
: null;
|
|
267
|
+
const endFmt =
|
|
268
|
+
typeof task.endTime === "string"
|
|
269
|
+
? formatIsoInstantShort(
|
|
270
|
+
task.endTime,
|
|
271
|
+
lang,
|
|
272
|
+
displayTimeZone,
|
|
273
|
+
use24HourClock,
|
|
274
|
+
)
|
|
275
|
+
: null;
|
|
276
|
+
const durationMinResolved = isDone
|
|
277
|
+
? optimisticDoneDurationMin !== null
|
|
278
|
+
? optimisticDoneDurationMin
|
|
279
|
+
: resolvedDoneTaskDurationMinutes(task)
|
|
280
|
+
: null;
|
|
281
|
+
const durationLabel =
|
|
282
|
+
durationMinResolved !== null ? formatDuration(durationMinResolved) : null;
|
|
283
|
+
const showDoneTimingRow =
|
|
284
|
+
isDone && (startFmt !== null || endFmt !== null || durationLabel !== null);
|
|
285
|
+
const canEditTaskStartTime =
|
|
286
|
+
allowTaskStartTimeEdit &&
|
|
287
|
+
typeof task.startTime === "string" &&
|
|
288
|
+
task.startTime.trim() !== "";
|
|
289
|
+
const canEditTaskEndTime =
|
|
290
|
+
allowTaskEndTimeEdit &&
|
|
291
|
+
isDone &&
|
|
292
|
+
typeof task.startTime === "string" &&
|
|
293
|
+
task.startTime.trim() !== "" &&
|
|
294
|
+
typeof task.endTime === "string" &&
|
|
295
|
+
task.endTime.trim() !== "";
|
|
296
|
+
const doneTimingAria = [
|
|
297
|
+
startFmt ? `${t.archiveTaskStartLabel} ${startFmt}` : "",
|
|
298
|
+
endFmt ? `${t.archiveTaskEndLabel} ${endFmt}` : "",
|
|
299
|
+
durationLabel ? `${t.taskTimingDurationLabel} ${durationLabel}` : "",
|
|
300
|
+
]
|
|
301
|
+
.filter(Boolean)
|
|
302
|
+
.join(", ");
|
|
303
|
+
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
setTitleEditing(false);
|
|
306
|
+
}, [task.id]);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
const st = typeof task.startTime === "string" ? task.startTime.trim() : "";
|
|
310
|
+
const parsed = st ? new Date(st) : null;
|
|
311
|
+
if (!parsed || Number.isNaN(parsed.getTime())) {
|
|
312
|
+
setTaskStartDraft("");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
setTaskStartDraft(formatDatetimeLocalValue(parsed));
|
|
316
|
+
}, [task.id, task.startTime]);
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
const et = typeof task.endTime === "string" ? task.endTime.trim() : "";
|
|
320
|
+
const parsed = et ? new Date(et) : null;
|
|
321
|
+
if (!parsed || Number.isNaN(parsed.getTime())) {
|
|
322
|
+
setTaskEndDraft("");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
setTaskEndDraft(formatDatetimeLocalValue(parsed));
|
|
326
|
+
}, [task.id, task.endTime]);
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
setOptimisticLiveBaseMs(null);
|
|
330
|
+
}, [task.id, task.durationMs]);
|
|
331
|
+
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
setOptimisticDoneDurationMin(null);
|
|
334
|
+
}, [task.id, task.durationMs, task.endTime]);
|
|
335
|
+
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
if (!smoothTaskTimer) {
|
|
338
|
+
setDerivedLiveBaseMs(null);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const startMs =
|
|
342
|
+
typeof task.startTime === "string"
|
|
343
|
+
? Date.parse(task.startTime)
|
|
344
|
+
: Number.NaN;
|
|
345
|
+
if (!Number.isFinite(startMs)) {
|
|
346
|
+
setDerivedLiveBaseMs(null);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
setDerivedLiveBaseMs(Math.max(0, Date.now() - startMs));
|
|
350
|
+
}, [smoothTaskTimer, task.startTime, task.id]);
|
|
351
|
+
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
if (titleEditing && titleInputRef.current) {
|
|
354
|
+
const el = titleInputRef.current;
|
|
355
|
+
el.focus();
|
|
356
|
+
el.select();
|
|
357
|
+
}
|
|
358
|
+
}, [titleEditing]);
|
|
359
|
+
|
|
360
|
+
const beginEditTitle = useCallback(() => {
|
|
361
|
+
setTitleDraft(titleDraftSource);
|
|
362
|
+
setTitleEditing(true);
|
|
363
|
+
}, [titleDraftSource]);
|
|
364
|
+
|
|
365
|
+
const commitTitleEdit = useCallback(() => {
|
|
366
|
+
const trimmed = titleDraft.trim();
|
|
367
|
+
setTitleEditing(false);
|
|
368
|
+
if (trimmed === "") {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (trimmed === titleDraftSource) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const parsed = parseTaskWithAutoTags(trimmed);
|
|
375
|
+
const nextName = parsed.name.trim() || parsed.tags[0] || trimmed;
|
|
376
|
+
const mergedTagsNext = mergeTagsForDisplay(trimmed, mergedTags);
|
|
377
|
+
const projUp = resolveProjectForTaskUpdate(trimmed);
|
|
378
|
+
const resolvedProject =
|
|
379
|
+
projUp !== undefined ? projUp : (task.project ?? null);
|
|
380
|
+
const tagsFiltered = filterTaskTagsForProject(
|
|
381
|
+
mergedTagsNext,
|
|
382
|
+
resolvedProject,
|
|
383
|
+
);
|
|
384
|
+
const body: Record<string, unknown> = {
|
|
385
|
+
type: "updateTask",
|
|
386
|
+
taskId: task.id,
|
|
387
|
+
name: nextName,
|
|
388
|
+
tags: tagsFiltered,
|
|
389
|
+
};
|
|
390
|
+
if (inspectingId) {
|
|
391
|
+
body.sessionId = inspectingId;
|
|
392
|
+
}
|
|
393
|
+
if (projUp !== undefined) {
|
|
394
|
+
body.project = projUp;
|
|
395
|
+
}
|
|
396
|
+
void post(body);
|
|
397
|
+
}, [titleDraft, titleDraftSource, task.id, mergedTags, inspectingId, post]);
|
|
398
|
+
|
|
399
|
+
const cancelTitleEdit = useCallback(() => {
|
|
400
|
+
skipTitleBlurCommitRef.current = true;
|
|
401
|
+
setTitleDraft(titleDraftSource);
|
|
402
|
+
setTitleEditing(false);
|
|
403
|
+
}, [titleDraftSource]);
|
|
404
|
+
|
|
405
|
+
const runFinishTask = useCallback(() => {
|
|
406
|
+
void post({
|
|
407
|
+
type: "finishTask",
|
|
408
|
+
taskId: task.id,
|
|
409
|
+
shouldCommit,
|
|
410
|
+
});
|
|
411
|
+
}, [post, task.id, shouldCommit]);
|
|
412
|
+
|
|
413
|
+
const requestFinishTask = useCallback(() => {
|
|
414
|
+
if (isDone) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const subs = task.subtasks;
|
|
418
|
+
const openCount = Array.isArray(subs)
|
|
419
|
+
? subs.filter((s) => !s.done).length
|
|
420
|
+
: 0;
|
|
421
|
+
if (openCount > 0 && !readFinishSubtasksWarnDismissed()) {
|
|
422
|
+
setDontRemindOpenSubtasksFinish(false);
|
|
423
|
+
setFinishOpenSubtasksWarnOpen(true);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
runFinishTask();
|
|
427
|
+
}, [isDone, task.subtasks, runFinishTask]);
|
|
428
|
+
|
|
429
|
+
const applyTaskStartTimeEdit = useCallback(() => {
|
|
430
|
+
if (!canEditTaskStartTime) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const nextMs = Date.parse(taskStartDraft);
|
|
434
|
+
if (!Number.isFinite(nextMs)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const nextIso = new Date(nextMs).toISOString();
|
|
438
|
+
if (nextIso === task.startTime) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (smoothTaskTimer) {
|
|
442
|
+
setOptimisticLiveBaseMs(Math.max(0, Date.now() - nextMs));
|
|
443
|
+
}
|
|
444
|
+
const body: Record<string, unknown> = {
|
|
445
|
+
type: "setTaskStartTime",
|
|
446
|
+
taskId: task.id,
|
|
447
|
+
startTime: nextIso,
|
|
448
|
+
};
|
|
449
|
+
if (inspectingId) {
|
|
450
|
+
body.sessionId = inspectingId;
|
|
451
|
+
}
|
|
452
|
+
void post(body);
|
|
453
|
+
}, [
|
|
454
|
+
canEditTaskStartTime,
|
|
455
|
+
taskStartDraft,
|
|
456
|
+
task.startTime,
|
|
457
|
+
task.id,
|
|
458
|
+
inspectingId,
|
|
459
|
+
post,
|
|
460
|
+
smoothTaskTimer,
|
|
461
|
+
]);
|
|
462
|
+
|
|
463
|
+
const applyTaskEndTimeEdit = useCallback(() => {
|
|
464
|
+
if (!canEditTaskEndTime) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const endMs = Date.parse(taskEndDraft);
|
|
468
|
+
if (!Number.isFinite(endMs)) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const startMs =
|
|
472
|
+
typeof task.startTime === "string"
|
|
473
|
+
? Date.parse(task.startTime)
|
|
474
|
+
: Number.NaN;
|
|
475
|
+
if (!Number.isFinite(startMs) || endMs < startMs) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const nextIso = new Date(endMs).toISOString();
|
|
479
|
+
if (nextIso === task.endTime) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
setOptimisticDoneDurationMin((endMs - startMs) / 60000);
|
|
483
|
+
const body: Record<string, unknown> = {
|
|
484
|
+
type: "setTaskEndTime",
|
|
485
|
+
taskId: task.id,
|
|
486
|
+
endTime: nextIso,
|
|
487
|
+
};
|
|
488
|
+
if (inspectingId) {
|
|
489
|
+
body.sessionId = inspectingId;
|
|
490
|
+
}
|
|
491
|
+
void post(body);
|
|
492
|
+
}, [
|
|
493
|
+
canEditTaskEndTime,
|
|
494
|
+
taskEndDraft,
|
|
495
|
+
task.startTime,
|
|
496
|
+
task.endTime,
|
|
497
|
+
task.id,
|
|
498
|
+
inspectingId,
|
|
499
|
+
post,
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
const shellBase =
|
|
503
|
+
variant === "plain" ? TASK_CARD_SHELL_PLAIN_CLASS : TASK_CARD_SHELL_CLASS;
|
|
504
|
+
const shellClass = anchorId ? `scroll-mt-24 ${shellBase}` : shellBase;
|
|
505
|
+
|
|
506
|
+
const titleUnderline =
|
|
507
|
+
variant === "plain"
|
|
508
|
+
? "border-b border-zinc-300 dark:border-zinc-600"
|
|
509
|
+
: "border-b border-zinc-300 bg-transparent dark:border-zinc-600";
|
|
510
|
+
/** Carte claire : pas de `text-white` (illisible sur zinc-100). */
|
|
511
|
+
const titleActiveClass =
|
|
512
|
+
variant === "plain"
|
|
513
|
+
? "text-zinc-900 dark:text-zinc-100"
|
|
514
|
+
: "text-zinc-950 dark:text-zinc-50";
|
|
515
|
+
const titleDoneClass =
|
|
516
|
+
variant === "plain" ? DONE_TITLE_PLAIN_CLASS : DONE_TITLE_READ_CLASS;
|
|
517
|
+
const titleHitClass =
|
|
518
|
+
variant === "plain"
|
|
519
|
+
? "hover:bg-zinc-200/60 dark:hover:bg-zinc-800/40"
|
|
520
|
+
: "hover:bg-zinc-200/70 dark:hover:bg-zinc-800/40";
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<>
|
|
524
|
+
<div id={anchorId} className={shellClass}>
|
|
525
|
+
{trimmedProject ? (
|
|
526
|
+
<div className="mb-1.5 flex min-w-0 justify-start sm:mb-2">
|
|
527
|
+
<span
|
|
528
|
+
className={`${PROJECT_CHIP_APPLIED_CLASS} inline-flex items-center gap-1 max-w-full truncate pr-1`}
|
|
529
|
+
title={formatProjectDisplay(trimmedProject)}
|
|
530
|
+
>
|
|
531
|
+
<span className="truncate">
|
|
532
|
+
{formatProjectDisplay(trimmedProject)}
|
|
533
|
+
</span>
|
|
534
|
+
<button
|
|
535
|
+
type="button"
|
|
536
|
+
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full hover:bg-black/10 dark:hover:bg-white/10"
|
|
537
|
+
onClick={() => {
|
|
538
|
+
const body: Record<string, unknown> = {
|
|
539
|
+
type: "updateTask",
|
|
540
|
+
taskId: task.id,
|
|
541
|
+
name: task.name,
|
|
542
|
+
tags: task.tags ?? [],
|
|
543
|
+
project: null,
|
|
544
|
+
};
|
|
545
|
+
if (inspectingId) {
|
|
546
|
+
body.sessionId = inspectingId;
|
|
547
|
+
}
|
|
548
|
+
void post(body);
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
<span className="text-[0.65rem] font-bold leading-none">×</span>
|
|
552
|
+
</button>
|
|
553
|
+
</span>
|
|
554
|
+
</div>
|
|
555
|
+
) : null}
|
|
556
|
+
<div className="flex min-h-10 items-center gap-2 sm:gap-3">
|
|
557
|
+
<div
|
|
558
|
+
className={`flex h-10 min-w-0 flex-1 bg-transparent ${titleUnderline}`}
|
|
559
|
+
>
|
|
560
|
+
{titleEditing ? (
|
|
561
|
+
<input
|
|
562
|
+
ref={titleInputRef}
|
|
563
|
+
type="text"
|
|
564
|
+
className={TASK_ACTIVE_TITLE_EDIT_INPUT_CLASS}
|
|
565
|
+
value={titleDraft}
|
|
566
|
+
onChange={(e) => setTitleDraft(e.target.value)}
|
|
567
|
+
onBlur={() => {
|
|
568
|
+
if (skipTitleBlurCommitRef.current) {
|
|
569
|
+
skipTitleBlurCommitRef.current = false;
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
commitTitleEdit();
|
|
573
|
+
}}
|
|
574
|
+
onKeyDown={(e) => {
|
|
575
|
+
if (e.key === "Enter") {
|
|
576
|
+
e.preventDefault();
|
|
577
|
+
skipTitleBlurCommitRef.current = true;
|
|
578
|
+
commitTitleEdit();
|
|
579
|
+
}
|
|
580
|
+
if (e.key === "Escape") {
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
cancelTitleEdit();
|
|
583
|
+
}
|
|
584
|
+
}}
|
|
585
|
+
aria-label={t.activeTaskTitleInputAria}
|
|
586
|
+
/>
|
|
587
|
+
) : (
|
|
588
|
+
<div
|
|
589
|
+
role="button"
|
|
590
|
+
tabIndex={0}
|
|
591
|
+
className={`flex h-full min-h-10 min-w-0 flex-1 cursor-pointer items-center rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-zinc-500/40 ${titleHitClass}`}
|
|
592
|
+
onClick={() => beginEditTitle()}
|
|
593
|
+
onKeyDown={(e) => {
|
|
594
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
beginEditTitle();
|
|
597
|
+
}
|
|
598
|
+
}}
|
|
599
|
+
title={t.activeTaskTitleEditHint}
|
|
600
|
+
aria-label={t.activeTaskTitleEditHint}
|
|
601
|
+
>
|
|
602
|
+
<div
|
|
603
|
+
className={`flex h-full min-h-0 min-w-0 flex-1 flex-nowrap items-center overflow-x-auto overflow-y-hidden ${TASK_ACTIVE_TITLE_READ_CLASS} ${
|
|
604
|
+
isDone ? titleDoneClass : titleActiveClass
|
|
605
|
+
}`}
|
|
606
|
+
>
|
|
607
|
+
<span className="whitespace-nowrap">
|
|
608
|
+
{titleDisplay || "—"}
|
|
609
|
+
</span>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
{!isDone && !isInspecting ? (
|
|
615
|
+
<span
|
|
616
|
+
className="shrink-0 self-center font-mono text-sm font-semibold tabular-nums tracking-tight text-emerald-700 dark:text-emerald-400"
|
|
617
|
+
aria-live={smoothTaskTimer ? "polite" : "off"}
|
|
618
|
+
title={formatStopwatchMs(liveTaskStopwatchMs)}
|
|
619
|
+
>
|
|
620
|
+
{formatStopwatchMs(liveTaskStopwatchMs)}
|
|
621
|
+
</span>
|
|
622
|
+
) : null}
|
|
623
|
+
{isInspecting ? (
|
|
624
|
+
<button
|
|
625
|
+
type="button"
|
|
626
|
+
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-zinc-300 text-zinc-600 hover:border-red-600/60 hover:bg-red-50 hover:text-red-800 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-red-800/70 dark:hover:bg-red-950/45 dark:hover:text-red-300"
|
|
627
|
+
title={t.taskDeleteBtn}
|
|
628
|
+
aria-label={t.taskDeleteBtn}
|
|
629
|
+
onClick={() => confirmDeleteTask(task.id)}
|
|
630
|
+
>
|
|
631
|
+
<Trash2 size={20} />
|
|
632
|
+
</button>
|
|
633
|
+
) : (
|
|
634
|
+
<div className="flex h-10 shrink-0 items-center gap-1">
|
|
635
|
+
{!isDone && showKronoFocusTaskActions ? (
|
|
636
|
+
<button
|
|
637
|
+
type="button"
|
|
638
|
+
className={`inline-flex h-10 w-10 items-center justify-center rounded-lg ${
|
|
639
|
+
kronoFocusIsRunningOrPaused
|
|
640
|
+
? "border border-violet-500/55 bg-violet-100 text-violet-900 hover:bg-violet-200/90 dark:border-violet-500/55 dark:bg-violet-950/45 dark:text-violet-200 dark:hover:bg-violet-900/55"
|
|
641
|
+
: "border border-violet-500/45 bg-violet-50 text-violet-900 hover:bg-violet-100/90 dark:border-violet-600/45 dark:bg-violet-950/30 dark:text-violet-200 dark:hover:bg-violet-900/40"
|
|
642
|
+
}`}
|
|
643
|
+
title={t.startKronoFocusFromActiveTask}
|
|
644
|
+
aria-label={t.startKronoFocusFromActiveTask}
|
|
645
|
+
onClick={() => void startKronoFocusFromTask()}
|
|
646
|
+
>
|
|
647
|
+
<Timer size={20} />
|
|
648
|
+
</button>
|
|
649
|
+
) : null}
|
|
650
|
+
<button
|
|
651
|
+
type="button"
|
|
652
|
+
role="switch"
|
|
653
|
+
disabled={isDone}
|
|
654
|
+
aria-checked={shouldCommit ? "true" : "false"}
|
|
655
|
+
aria-label={
|
|
656
|
+
shouldCommit
|
|
657
|
+
? t.commitOnFinishToggleAriaOn
|
|
658
|
+
: t.commitOnFinishToggleAriaOff
|
|
659
|
+
}
|
|
660
|
+
title={t.commitOnFinishHint}
|
|
661
|
+
onClick={() => {
|
|
662
|
+
if (!isDone) {
|
|
663
|
+
setShouldCommit((v) => !v);
|
|
664
|
+
}
|
|
665
|
+
}}
|
|
666
|
+
className={
|
|
667
|
+
shouldCommit
|
|
668
|
+
? tbEmeraldIcon
|
|
669
|
+
: "inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-zinc-300 bg-zinc-100/80 text-zinc-600 transition-colors hover:bg-zinc-200/80 disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-600 dark:bg-zinc-900/40 dark:text-zinc-500 dark:hover:bg-zinc-800/50 [&_svg]:shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500/30 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900"
|
|
670
|
+
}
|
|
671
|
+
>
|
|
672
|
+
<GitCommit size={18} strokeWidth={2} aria-hidden />
|
|
673
|
+
</button>
|
|
674
|
+
{pausePlayMode === null ? (
|
|
675
|
+
<div className="h-10 w-10 shrink-0" aria-hidden />
|
|
676
|
+
) : (
|
|
677
|
+
<button
|
|
678
|
+
type="button"
|
|
679
|
+
className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
680
|
+
title={
|
|
681
|
+
pausePlayMode === "pause" ? t.pauseTaskBtn : t.resumeTaskBtn
|
|
682
|
+
}
|
|
683
|
+
aria-label={
|
|
684
|
+
pausePlayMode === "pause" ? t.pauseTaskBtn : t.resumeTaskBtn
|
|
685
|
+
}
|
|
686
|
+
onClick={onPausePlay}
|
|
687
|
+
>
|
|
688
|
+
{pausePlayMode === "pause" ? (
|
|
689
|
+
<Pause size={20} />
|
|
690
|
+
) : (
|
|
691
|
+
<Play size={20} className="ml-px" />
|
|
692
|
+
)}
|
|
693
|
+
</button>
|
|
694
|
+
)}
|
|
695
|
+
<button
|
|
696
|
+
type="button"
|
|
697
|
+
disabled={isDone}
|
|
698
|
+
className={tbEmeraldIcon}
|
|
699
|
+
title={t.finishTaskBtn}
|
|
700
|
+
aria-label={t.finishTaskBtn}
|
|
701
|
+
onClick={() => {
|
|
702
|
+
if (!isDone) {
|
|
703
|
+
requestFinishTask();
|
|
704
|
+
}
|
|
705
|
+
}}
|
|
706
|
+
>
|
|
707
|
+
<CheckCircle2 size={20} />
|
|
708
|
+
</button>
|
|
709
|
+
<button
|
|
710
|
+
type="button"
|
|
711
|
+
className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-300 text-zinc-600 hover:border-red-600/60 hover:bg-red-50 hover:text-red-800 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-red-800/70 dark:hover:bg-red-950/45 dark:hover:text-red-300"
|
|
712
|
+
title={t.taskDeleteBtn}
|
|
713
|
+
aria-label={t.taskDeleteBtn}
|
|
714
|
+
onClick={() => confirmDeleteTask(task.id)}
|
|
715
|
+
>
|
|
716
|
+
<Trash2 size={20} />
|
|
717
|
+
</button>
|
|
718
|
+
</div>
|
|
719
|
+
)}
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
{showDoneTimingRow || canEditTaskStartTime || canEditTaskEndTime ? (
|
|
723
|
+
<div aria-label={doneTimingAria || undefined}>
|
|
724
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-2">
|
|
725
|
+
<TaskTimingFieldRow
|
|
726
|
+
label={t.archiveTaskStartLabel}
|
|
727
|
+
displayValue={startFmt}
|
|
728
|
+
editable={canEditTaskStartTime}
|
|
729
|
+
draftValue={taskStartDraft}
|
|
730
|
+
onDraftChange={setTaskStartDraft}
|
|
731
|
+
onDraftBlur={applyTaskStartTimeEdit}
|
|
732
|
+
lang={lang}
|
|
733
|
+
t={t}
|
|
734
|
+
/>
|
|
735
|
+
<TaskTimingFieldRow
|
|
736
|
+
label={t.archiveTaskEndLabel}
|
|
737
|
+
displayValue={endFmt}
|
|
738
|
+
editable={canEditTaskEndTime}
|
|
739
|
+
draftValue={taskEndDraft}
|
|
740
|
+
onDraftChange={setTaskEndDraft}
|
|
741
|
+
onDraftBlur={applyTaskEndTimeEdit}
|
|
742
|
+
lang={lang}
|
|
743
|
+
t={t}
|
|
744
|
+
/>
|
|
745
|
+
{durationLabel ? (
|
|
746
|
+
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-2 text-[0.7rem] leading-snug text-zinc-600 dark:text-zinc-400 sm:mt-0">
|
|
747
|
+
<span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
748
|
+
{t.taskTimingDurationLabel}
|
|
749
|
+
</span>
|
|
750
|
+
<span className="tabular-nums text-zinc-800 dark:text-zinc-200">
|
|
751
|
+
{durationLabel}
|
|
752
|
+
</span>
|
|
753
|
+
</div>
|
|
754
|
+
) : null}
|
|
755
|
+
</div>
|
|
756
|
+
<div className="mt-3">
|
|
757
|
+
<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">
|
|
758
|
+
{mergedTags.length > 0 ? (
|
|
759
|
+
<TagPills
|
|
760
|
+
variant="applied"
|
|
761
|
+
differentiateProjectScopedTags
|
|
762
|
+
className="flex min-w-0 max-w-full flex-wrap items-baseline gap-x-1 gap-y-0.5"
|
|
763
|
+
tags={mergedTags}
|
|
764
|
+
defaultTagBucketLabel={t.taskTagDefaultBucket}
|
|
765
|
+
onRemove={(tagToRemove) => {
|
|
766
|
+
const nextTags = mergedTags.filter(
|
|
767
|
+
(t) => normalizeTagKey(t) !== normalizeTagKey(tagToRemove),
|
|
768
|
+
);
|
|
769
|
+
const filteredTags = filterTaskTagsForProject(
|
|
770
|
+
nextTags,
|
|
771
|
+
task.project ?? null,
|
|
772
|
+
);
|
|
773
|
+
const body: Record<string, unknown> = {
|
|
774
|
+
type: "updateTask",
|
|
775
|
+
taskId: task.id,
|
|
776
|
+
name: task.name,
|
|
777
|
+
tags: filteredTags,
|
|
778
|
+
};
|
|
779
|
+
if (task.project !== undefined) {
|
|
780
|
+
body.project = task.project;
|
|
781
|
+
}
|
|
782
|
+
if (inspectingId) {
|
|
783
|
+
body.sessionId = inspectingId;
|
|
784
|
+
}
|
|
785
|
+
void post(body);
|
|
786
|
+
}}
|
|
787
|
+
/>
|
|
788
|
+
) : null}
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
<TaskSubtasksBlock
|
|
792
|
+
taskId={task.id}
|
|
793
|
+
sessionId={inspectingId}
|
|
794
|
+
subtasks={task.subtasks}
|
|
795
|
+
enableSubtaskTimer={!isInspecting && !isDone}
|
|
796
|
+
activeSubtaskTimerId={task.activeSubtaskTimerId}
|
|
797
|
+
allowAddSubtasks={!isDone}
|
|
798
|
+
subtasksReadOnly={isDone}
|
|
799
|
+
t={t}
|
|
800
|
+
post={post}
|
|
801
|
+
/>
|
|
802
|
+
</div>
|
|
803
|
+
) : null}
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
<DashboardConfirmModal
|
|
808
|
+
open={finishOpenSubtasksWarnOpen}
|
|
809
|
+
title={t.finishTaskOpenSubtasksWarnTitle}
|
|
810
|
+
message={t.finishTaskOpenSubtasksWarnBody}
|
|
811
|
+
cancelLabel={t.dialogCancelBtn}
|
|
812
|
+
confirmLabel={t.finishTaskBtn}
|
|
813
|
+
dismissCheckbox={{
|
|
814
|
+
label: t.sessionArchiveDontShowAgain,
|
|
815
|
+
checked: dontRemindOpenSubtasksFinish,
|
|
816
|
+
onChange: setDontRemindOpenSubtasksFinish,
|
|
817
|
+
}}
|
|
818
|
+
onCancel={() => {
|
|
819
|
+
setFinishOpenSubtasksWarnOpen(false);
|
|
820
|
+
setDontRemindOpenSubtasksFinish(false);
|
|
821
|
+
}}
|
|
822
|
+
onConfirm={() => {
|
|
823
|
+
if (dontRemindOpenSubtasksFinish) {
|
|
824
|
+
persistFinishSubtasksWarnDismissed();
|
|
825
|
+
}
|
|
826
|
+
setFinishOpenSubtasksWarnOpen(false);
|
|
827
|
+
runFinishTask();
|
|
828
|
+
}}
|
|
829
|
+
/>
|
|
830
|
+
</>
|
|
831
|
+
);
|
|
832
|
+
}
|