@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 {
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
4
10
|
import { createPortal } from "react-dom";
|
|
5
|
-
import { Clock } from "lucide-react";
|
|
11
|
+
import { Check, Clock } from "lucide-react";
|
|
6
12
|
|
|
7
13
|
import type { Lang } from "@/lib/dashboardCopy";
|
|
8
14
|
import type { SettingsCopy } from "@/lib/settingsCopy";
|
|
@@ -40,7 +46,12 @@ type KronosysTimePopoverFieldProps = {
|
|
|
40
46
|
lang: Lang;
|
|
41
47
|
/** Aperçu localisé sous les sélecteurs : `true` = 24 h, `false` = 12 h (AM/PM). */
|
|
42
48
|
use24HourClock?: boolean;
|
|
43
|
-
t: Pick<
|
|
49
|
+
t: Pick<
|
|
50
|
+
SettingsCopy,
|
|
51
|
+
| "timePickerPopoverTimeLabel"
|
|
52
|
+
| "timePickerPopoverHourAria"
|
|
53
|
+
| "timePickerPopoverMinuteAria"
|
|
54
|
+
>;
|
|
44
55
|
};
|
|
45
56
|
|
|
46
57
|
export function KronosysTimePopoverField({
|
|
@@ -72,15 +83,19 @@ export function KronosysTimePopoverField({
|
|
|
72
83
|
(h: number, m: number) => {
|
|
73
84
|
onChange(formatTimeHHmm(h, m));
|
|
74
85
|
},
|
|
75
|
-
[onChange]
|
|
86
|
+
[onChange],
|
|
76
87
|
);
|
|
77
88
|
|
|
78
89
|
const display = formatTimeHHmm(hour, minute);
|
|
90
|
+
const acceptAriaLabel = lang === "fr" ? "Accepter l'heure" : "Accept time";
|
|
79
91
|
const displayLocalized = (() => {
|
|
80
92
|
const d = new Date();
|
|
81
93
|
d.setHours(hour, minute, 0, 0);
|
|
82
94
|
const loc = lang === "fr" ? "fr-CA" : "en-CA";
|
|
83
|
-
return new Intl.DateTimeFormat(loc, {
|
|
95
|
+
return new Intl.DateTimeFormat(loc, {
|
|
96
|
+
timeStyle: "short",
|
|
97
|
+
hour12: !use24HourClock,
|
|
98
|
+
}).format(d);
|
|
84
99
|
})();
|
|
85
100
|
|
|
86
101
|
const updatePosition = useCallback(() => {
|
|
@@ -121,7 +136,10 @@ export function KronosysTimePopoverField({
|
|
|
121
136
|
}
|
|
122
137
|
const onDoc = (e: MouseEvent) => {
|
|
123
138
|
const tNode = e.target as Node;
|
|
124
|
-
if (
|
|
139
|
+
if (
|
|
140
|
+
popoverRef.current?.contains(tNode) ||
|
|
141
|
+
triggerRef.current?.contains(tNode)
|
|
142
|
+
) {
|
|
125
143
|
return;
|
|
126
144
|
}
|
|
127
145
|
setOpen(false);
|
|
@@ -159,7 +177,9 @@ export function KronosysTimePopoverField({
|
|
|
159
177
|
}}
|
|
160
178
|
>
|
|
161
179
|
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
162
|
-
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400">
|
|
180
|
+
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400">
|
|
181
|
+
{t.timePickerPopoverTimeLabel}
|
|
182
|
+
</span>
|
|
163
183
|
<div className="inline-flex items-center gap-1.5">
|
|
164
184
|
<select
|
|
165
185
|
className="h-9 rounded-lg border border-violet-500/45 bg-white px-2.5 text-sm font-mono text-zinc-900 outline-none dark:border-violet-400/50 dark:bg-zinc-800 dark:text-zinc-100"
|
|
@@ -195,12 +215,26 @@ export function KronosysTimePopoverField({
|
|
|
195
215
|
))}
|
|
196
216
|
</select>
|
|
197
217
|
</div>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-emerald-500/50 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-400/60 dark:bg-emerald-950/35 dark:text-emerald-200 dark:hover:bg-emerald-900/40"
|
|
221
|
+
aria-label={acceptAriaLabel}
|
|
222
|
+
onClick={() => {
|
|
223
|
+
commit(hour, minute);
|
|
224
|
+
setOpen(false);
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<Check className="h-4 w-4" strokeWidth={2.5} aria-hidden />
|
|
228
|
+
</button>
|
|
198
229
|
</div>
|
|
199
|
-
<p
|
|
230
|
+
<p
|
|
231
|
+
className="mt-2 text-center text-xs text-zinc-500 dark:text-zinc-400"
|
|
232
|
+
aria-hidden
|
|
233
|
+
>
|
|
200
234
|
{display} · {displayLocalized}
|
|
201
235
|
</p>
|
|
202
236
|
</div>,
|
|
203
|
-
document.body
|
|
237
|
+
document.body,
|
|
204
238
|
)
|
|
205
239
|
: null;
|
|
206
240
|
|
|
@@ -212,7 +246,9 @@ export function KronosysTimePopoverField({
|
|
|
212
246
|
disabled={disabled}
|
|
213
247
|
className={
|
|
214
248
|
"inline-flex w-full max-w-md items-center justify-between gap-2 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-left text-sm outline-none ring-violet-500/30 focus:ring-2 dark:border-zinc-700 dark:bg-zinc-950 " +
|
|
215
|
-
(disabled
|
|
249
|
+
(disabled
|
|
250
|
+
? "cursor-not-allowed opacity-50"
|
|
251
|
+
: "cursor-pointer hover:border-zinc-400 dark:hover:border-zinc-600")
|
|
216
252
|
}
|
|
217
253
|
aria-label={ariaLabel}
|
|
218
254
|
aria-expanded={open}
|
|
@@ -224,8 +260,14 @@ export function KronosysTimePopoverField({
|
|
|
224
260
|
setOpen((o) => !o);
|
|
225
261
|
}}
|
|
226
262
|
>
|
|
227
|
-
<span className="min-w-0 flex-1 truncate font-mono text-sm tabular-nums text-zinc-900 dark:text-zinc-100">
|
|
228
|
-
|
|
263
|
+
<span className="min-w-0 flex-1 truncate font-mono text-sm tabular-nums text-zinc-900 dark:text-zinc-100">
|
|
264
|
+
{display}
|
|
265
|
+
</span>
|
|
266
|
+
<Clock
|
|
267
|
+
className="h-4 w-4 shrink-0 opacity-70"
|
|
268
|
+
strokeWidth={2}
|
|
269
|
+
aria-hidden
|
|
270
|
+
/>
|
|
229
271
|
</button>
|
|
230
272
|
{popover}
|
|
231
273
|
</div>
|
|
@@ -16,6 +16,15 @@ export type NewSessionScopePayload =
|
|
|
16
16
|
timeEndLocal?: string;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/** Résultat de la modale : portée + début de session (immédiat ou ISO passé/présent). */
|
|
20
|
+
export type NewSessionConfirmPayload = {
|
|
21
|
+
scope: NewSessionScopePayload;
|
|
22
|
+
/** `null` = maintenant (comportement par défaut). */
|
|
23
|
+
sessionStartAtIso: string | null;
|
|
24
|
+
/** Fin de session (ISO) — obligatoire avec `sessionStartAtIso` pour une session entièrement passée. */
|
|
25
|
+
sessionEndAtIso: string | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
19
28
|
const WD_EN = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
20
29
|
const WD_FR = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"];
|
|
21
30
|
|
|
@@ -34,6 +43,24 @@ function localHmNow(): string {
|
|
|
34
43
|
return `${h}:${m}`;
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
/** Valeur pour `<input type="datetime-local">` (fuseau local du navigateur). */
|
|
47
|
+
function localDatetimeLocalNow(): string {
|
|
48
|
+
const d = new Date();
|
|
49
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
50
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(
|
|
51
|
+
d.getHours(),
|
|
52
|
+
)}:${pad(d.getMinutes())}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Décalage par rapport à maintenant (ex. début de session il y a quelques minutes). */
|
|
56
|
+
function localDatetimeLocalOffsetMs(offsetMs: number): string {
|
|
57
|
+
const d = new Date(Date.now() + offsetMs);
|
|
58
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
59
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(
|
|
60
|
+
d.getHours(),
|
|
61
|
+
)}:${pad(d.getMinutes())}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
37
64
|
function useEscapeClose(open: boolean, onClose: () => void) {
|
|
38
65
|
useEffect(() => {
|
|
39
66
|
if (!open) {
|
|
@@ -60,9 +87,14 @@ export function NewSessionScopeModal({
|
|
|
60
87
|
lang: Lang;
|
|
61
88
|
t: DashboardStrings;
|
|
62
89
|
onCancel: () => void;
|
|
63
|
-
onConfirm: (
|
|
90
|
+
onConfirm: (payload: NewSessionConfirmPayload) => void;
|
|
64
91
|
}) {
|
|
65
92
|
const titleId = useId();
|
|
93
|
+
const [timingMode, setTimingMode] = useState<"immediate" | "past">(
|
|
94
|
+
"immediate",
|
|
95
|
+
);
|
|
96
|
+
const [pastStartLocal, setPastStartLocal] = useState("");
|
|
97
|
+
const [pastEndLocal, setPastEndLocal] = useState("");
|
|
66
98
|
const [mode, setMode] = useState<NewSessionScopePayload["mode"]>("none");
|
|
67
99
|
const [maxHoursStr, setMaxHoursStr] = useState("4");
|
|
68
100
|
const [dateFrom, setDateFrom] = useState("");
|
|
@@ -87,6 +119,9 @@ export function NewSessionScopeModal({
|
|
|
87
119
|
if (!open) {
|
|
88
120
|
return;
|
|
89
121
|
}
|
|
122
|
+
setTimingMode("immediate");
|
|
123
|
+
setPastStartLocal("");
|
|
124
|
+
setPastEndLocal("");
|
|
90
125
|
setMode("none");
|
|
91
126
|
setMaxHoursStr("4");
|
|
92
127
|
setDateFrom("");
|
|
@@ -110,37 +145,81 @@ export function NewSessionScopeModal({
|
|
|
110
145
|
|
|
111
146
|
const handleSubmit = useCallback(() => {
|
|
112
147
|
setError(null);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
148
|
+
let sessionStartAtIso: string | null = null;
|
|
149
|
+
let sessionEndAtIso: string | null = null;
|
|
150
|
+
if (timingMode === "past") {
|
|
151
|
+
const rawStart = pastStartLocal.trim();
|
|
152
|
+
const rawEnd = pastEndLocal.trim();
|
|
153
|
+
if (!rawStart) {
|
|
154
|
+
setError(t.newSessionErrorPastMissing);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!rawEnd) {
|
|
158
|
+
setError(t.newSessionErrorPastEndMissing);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const msStart = Date.parse(rawStart);
|
|
162
|
+
const msEnd = Date.parse(rawEnd);
|
|
163
|
+
if (!Number.isFinite(msStart) || !Number.isFinite(msEnd)) {
|
|
164
|
+
setError(t.newSessionErrorPastInvalid);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (msStart > Date.now() + 60_000) {
|
|
168
|
+
setError(t.newSessionErrorPastFuture);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (msEnd > Date.now() + 60_000) {
|
|
172
|
+
setError(t.newSessionErrorPastEndFuture);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (msEnd <= msStart) {
|
|
176
|
+
setError(t.newSessionErrorPastEndBeforeStart);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
sessionStartAtIso = new Date(msStart).toISOString();
|
|
180
|
+
sessionEndAtIso = new Date(msEnd).toISOString();
|
|
181
|
+
const slackMs = 1000;
|
|
182
|
+
if (msStart >= Date.now() - slackMs) {
|
|
183
|
+
sessionStartAtIso = null;
|
|
184
|
+
sessionEndAtIso = null;
|
|
185
|
+
}
|
|
116
186
|
}
|
|
117
|
-
|
|
187
|
+
|
|
188
|
+
let scope: NewSessionScopePayload;
|
|
189
|
+
if (mode === "none") {
|
|
190
|
+
scope = { mode: "none" };
|
|
191
|
+
} else if (mode === "maxWallClock") {
|
|
118
192
|
const h = Number(maxHoursStr.replace(",", "."));
|
|
119
193
|
if (!Number.isFinite(h) || h < 0.1 || h > 8760) {
|
|
120
194
|
setError(t.newSessionErrorMax);
|
|
121
195
|
return;
|
|
122
196
|
}
|
|
123
|
-
|
|
197
|
+
scope = {
|
|
124
198
|
mode: "maxWallClock",
|
|
125
199
|
maxWallClockMinutes: Math.round(h * 60),
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (mode === "calendar") {
|
|
200
|
+
};
|
|
201
|
+
} else if (mode === "calendar") {
|
|
202
|
+
const ymd = /^\d{4}-\d{2}-\d{2}$/;
|
|
130
203
|
const a = dateFrom.trim();
|
|
131
204
|
const b = dateTo.trim();
|
|
132
205
|
if (!a && !b) {
|
|
133
206
|
setError(t.newSessionErrorCalendar);
|
|
134
207
|
return;
|
|
135
208
|
}
|
|
136
|
-
|
|
209
|
+
if ((a && !ymd.test(a)) || (b && !ymd.test(b))) {
|
|
210
|
+
setError(
|
|
211
|
+
lang === "fr"
|
|
212
|
+
? "Utilisez une date complète (AAAA-MM-JJ) pour chaque champ renseigné."
|
|
213
|
+
: "Use a full date (YYYY-MM-DD) for each filled field.",
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
scope = {
|
|
137
218
|
mode: "calendar",
|
|
138
219
|
...(a ? { calendarStart: a } : {}),
|
|
139
220
|
...(b ? { calendarEnd: b } : {}),
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
if (mode === "weekly") {
|
|
221
|
+
};
|
|
222
|
+
} else {
|
|
144
223
|
const days = weekdays.map((on, i) => (on ? i : -1)).filter((i) => i >= 0);
|
|
145
224
|
if (days.length === 0) {
|
|
146
225
|
setError(t.newSessionErrorWeekly);
|
|
@@ -160,16 +239,17 @@ export function NewSessionScopeModal({
|
|
|
160
239
|
);
|
|
161
240
|
return;
|
|
162
241
|
}
|
|
163
|
-
|
|
242
|
+
scope = {
|
|
164
243
|
mode: "weekly",
|
|
165
244
|
weekdays: days,
|
|
166
245
|
timeStartLocal: ts,
|
|
167
246
|
timeEndLocal: te,
|
|
168
|
-
}
|
|
169
|
-
|
|
247
|
+
};
|
|
248
|
+
} else {
|
|
249
|
+
scope = { mode: "weekly", weekdays: days };
|
|
170
250
|
}
|
|
171
|
-
onConfirm({ mode: "weekly", weekdays: days });
|
|
172
251
|
}
|
|
252
|
+
onConfirm({ scope, sessionStartAtIso, sessionEndAtIso });
|
|
173
253
|
}, [
|
|
174
254
|
dateFrom,
|
|
175
255
|
dateTo,
|
|
@@ -177,11 +257,20 @@ export function NewSessionScopeModal({
|
|
|
177
257
|
maxHoursStr,
|
|
178
258
|
mode,
|
|
179
259
|
onConfirm,
|
|
260
|
+
pastEndLocal,
|
|
261
|
+
pastStartLocal,
|
|
180
262
|
t.newSessionErrorCalendar,
|
|
181
263
|
t.newSessionErrorMax,
|
|
264
|
+
t.newSessionErrorPastEndBeforeStart,
|
|
265
|
+
t.newSessionErrorPastEndFuture,
|
|
266
|
+
t.newSessionErrorPastEndMissing,
|
|
267
|
+
t.newSessionErrorPastFuture,
|
|
268
|
+
t.newSessionErrorPastInvalid,
|
|
269
|
+
t.newSessionErrorPastMissing,
|
|
182
270
|
t.newSessionErrorWeekly,
|
|
183
271
|
timeFrom,
|
|
184
272
|
timeTo,
|
|
273
|
+
timingMode,
|
|
185
274
|
useTimeWindow,
|
|
186
275
|
weekdays,
|
|
187
276
|
]);
|
|
@@ -214,7 +303,105 @@ export function NewSessionScopeModal({
|
|
|
214
303
|
</div>
|
|
215
304
|
|
|
216
305
|
<fieldset className="mt-4 space-y-2">
|
|
217
|
-
<legend className="
|
|
306
|
+
<legend className="mb-1 text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
|
307
|
+
{t.newSessionTimingSectionTitle}
|
|
308
|
+
</legend>
|
|
309
|
+
{(
|
|
310
|
+
[
|
|
311
|
+
["immediate", t.newSessionTimingImmediate],
|
|
312
|
+
["past", t.newSessionTimingPast],
|
|
313
|
+
] as const
|
|
314
|
+
).map(([value, label]) => (
|
|
315
|
+
<label
|
|
316
|
+
key={value}
|
|
317
|
+
className="flex cursor-pointer items-center gap-2 rounded-lg border border-zinc-200 px-3 py-2 text-sm text-zinc-800 hover:bg-zinc-100/90 has-[:checked]:border-violet-500/50 has-[:checked]:bg-violet-50 dark:border-zinc-700/80 dark:text-zinc-200 dark:hover:bg-zinc-800/50 dark:has-[:checked]:border-violet-500/60 dark:has-[:checked]:bg-violet-950/30"
|
|
318
|
+
>
|
|
319
|
+
<input
|
|
320
|
+
type="radio"
|
|
321
|
+
name="session-start-timing"
|
|
322
|
+
value={value}
|
|
323
|
+
checked={timingMode === value}
|
|
324
|
+
onChange={() => {
|
|
325
|
+
setTimingMode(value);
|
|
326
|
+
if (value === "past") {
|
|
327
|
+
if (pastStartLocal.trim() === "") {
|
|
328
|
+
setPastStartLocal(localDatetimeLocalOffsetMs(-5 * 60_000));
|
|
329
|
+
}
|
|
330
|
+
if (pastEndLocal.trim() === "") {
|
|
331
|
+
setPastEndLocal(localDatetimeLocalNow());
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}}
|
|
335
|
+
className="size-4 accent-violet-500"
|
|
336
|
+
/>
|
|
337
|
+
{label}
|
|
338
|
+
</label>
|
|
339
|
+
))}
|
|
340
|
+
</fieldset>
|
|
341
|
+
|
|
342
|
+
{timingMode === "past" ? (
|
|
343
|
+
<div className="mt-3 space-y-4">
|
|
344
|
+
<label className="block text-sm text-zinc-700 dark:text-zinc-300">
|
|
345
|
+
<span className="mb-1 block font-medium text-zinc-800 dark:text-zinc-200">
|
|
346
|
+
{t.newSessionPastStartLabel}
|
|
347
|
+
</span>
|
|
348
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
349
|
+
<input
|
|
350
|
+
name="session-past-start"
|
|
351
|
+
type="datetime-local"
|
|
352
|
+
value={pastStartLocal}
|
|
353
|
+
min="1000-01-01T00:00"
|
|
354
|
+
max="9999-12-31T23:59"
|
|
355
|
+
onChange={(e) => setPastStartLocal(e.target.value)}
|
|
356
|
+
className="min-w-0 flex-1 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-zinc-900 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
|
|
357
|
+
/>
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
className="shrink-0 rounded-lg border border-zinc-300 px-2.5 py-2 text-xs text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
|
361
|
+
onClick={() => setPastStartLocal(localDatetimeLocalOffsetMs(-5 * 60_000))}
|
|
362
|
+
>
|
|
363
|
+
{t.newSessionNowBtn}
|
|
364
|
+
</button>
|
|
365
|
+
</div>
|
|
366
|
+
<span className="mt-1 block text-xs text-zinc-500 dark:text-zinc-400">
|
|
367
|
+
{t.newSessionPastStartHint}
|
|
368
|
+
</span>
|
|
369
|
+
</label>
|
|
370
|
+
<label className="block text-sm text-zinc-700 dark:text-zinc-300">
|
|
371
|
+
<span className="mb-1 block font-medium text-zinc-800 dark:text-zinc-200">
|
|
372
|
+
{t.newSessionPastEndLabel}
|
|
373
|
+
</span>
|
|
374
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
375
|
+
<input
|
|
376
|
+
name="session-past-end"
|
|
377
|
+
type="datetime-local"
|
|
378
|
+
value={pastEndLocal}
|
|
379
|
+
min={
|
|
380
|
+
pastStartLocal.trim() !== ""
|
|
381
|
+
? pastStartLocal
|
|
382
|
+
: "1000-01-01T00:00"
|
|
383
|
+
}
|
|
384
|
+
max="9999-12-31T23:59"
|
|
385
|
+
onChange={(e) => setPastEndLocal(e.target.value)}
|
|
386
|
+
className="min-w-0 flex-1 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-zinc-900 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
|
|
387
|
+
/>
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
className="shrink-0 rounded-lg border border-zinc-300 px-2.5 py-2 text-xs text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
|
391
|
+
onClick={() => setPastEndLocal(localDatetimeLocalNow())}
|
|
392
|
+
>
|
|
393
|
+
{t.newSessionNowBtn}
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
<span className="mt-1 block text-xs text-zinc-500 dark:text-zinc-400">
|
|
397
|
+
{t.newSessionPastEndHint}
|
|
398
|
+
</span>
|
|
399
|
+
</label>
|
|
400
|
+
</div>
|
|
401
|
+
) : null}
|
|
402
|
+
|
|
403
|
+
<fieldset className="mt-6 space-y-2">
|
|
404
|
+
<legend className="sr-only">{t.newSessionScopeFieldsetLegend}</legend>
|
|
218
405
|
{(
|
|
219
406
|
[
|
|
220
407
|
["none", t.newSessionModeNone],
|
|
@@ -267,6 +454,8 @@ export function NewSessionScopeModal({
|
|
|
267
454
|
<input
|
|
268
455
|
type="date"
|
|
269
456
|
value={dateFrom}
|
|
457
|
+
min="1000-01-01"
|
|
458
|
+
max="9999-12-31"
|
|
270
459
|
onChange={(e) => setDateFrom(e.target.value)}
|
|
271
460
|
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-zinc-900 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
|
|
272
461
|
/>
|
|
@@ -287,6 +476,8 @@ export function NewSessionScopeModal({
|
|
|
287
476
|
<input
|
|
288
477
|
type="date"
|
|
289
478
|
value={dateTo}
|
|
479
|
+
min="1000-01-01"
|
|
480
|
+
max="9999-12-31"
|
|
290
481
|
onChange={(e) => setDateTo(e.target.value)}
|
|
291
482
|
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-zinc-900 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
|
|
292
483
|
/>
|