@nightkatana/kronosys-app 1.0.0-beta.21 → 1.0.0-beta.22

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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/app/changelog/page.tsx +87 -19
  3. package/app/globals.css +10 -8
  4. package/app/guide/page.tsx +71 -34
  5. package/app/implementation/page.tsx +70 -60
  6. package/app/licenses/page.tsx +79 -47
  7. package/app/logs/page.tsx +103 -47
  8. package/app/page.tsx +104 -169
  9. package/app/reporting/page.tsx +1918 -1436
  10. package/app/settings/page.tsx +66 -44
  11. package/components/KronosysPayloadProvider.tsx +19 -5
  12. package/components/dashboard/AppShellHeaderKronoFocus.tsx +78 -0
  13. package/components/dashboard/AppShellHeaderToolbarLayout.tsx +36 -0
  14. package/components/dashboard/AppShellHeaderUtilityRibbon.tsx +19 -0
  15. package/components/dashboard/AppShellHeaderWallClock.tsx +23 -17
  16. package/components/dashboard/AppShellRouteNav.tsx +336 -209
  17. package/components/dashboard/AppShellToolbarCommandCenter.tsx +225 -0
  18. package/components/dashboard/AppShellToolbarRouteNav.tsx +204 -0
  19. package/components/dashboard/DashboardCommandCenter.tsx +119 -30
  20. package/components/dashboard/KronoFocusPanel.tsx +287 -260
  21. package/components/dashboard/LanguageMenu.tsx +23 -7
  22. package/components/dashboard/PageRefreshButton.tsx +42 -16
  23. package/components/dashboard/ReportingTour.tsx +20 -2
  24. package/components/dashboard/SessionListPanel.tsx +4 -4
  25. package/components/dashboard/ThemeToggle.tsx +4 -3
  26. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +9 -2
  27. package/components/dashboard/useKronoFocusLiveSeconds.ts +4 -2
  28. package/lib/appShellHeaderClasses.ts +22 -3
  29. package/lib/appShellToolbarChrome.ts +112 -0
  30. package/lib/appShellToolbarDeferredIntents.ts +112 -0
  31. package/lib/appShellToolbarSessionSlices.ts +67 -0
  32. package/lib/dashboardCopy.ts +78 -29
  33. package/lib/dashboardQuickSearch.ts +37 -6
  34. package/lib/dashboardUrlSession.ts +36 -0
  35. package/lib/generatedUserChangelog.ts +14 -0
  36. package/lib/implementationNotes.ts +18 -14
  37. package/lib/reportingAggregate.ts +68 -9
  38. package/lib/reportingMetricHelp.ts +8 -8
  39. package/lib/reportingStrings.ts +118 -9
  40. package/lib/reportingTagWeekBreakdown.ts +55 -13
  41. package/lib/settingsCopy.ts +6 -7
  42. package/lib/userGuideCopy.ts +29 -26
  43. package/package.json +7 -5
  44. package/server/db.ts +6 -4
  45. package/server/dbSchema.ts +2 -2
  46. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +0 -17
@@ -0,0 +1,225 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
6
+ import { postKronosysAction } from "@/lib/kronosysApi";
7
+ import {
8
+ DashboardCommandCenter,
9
+ type DashboardCommandHandlers,
10
+ } from "@/components/dashboard/DashboardCommandCenter";
11
+ import type { DashboardStrings } from "@/lib/dashboardCopy";
12
+ import type { Lang } from "@/lib/dashboardCopy";
13
+ import { buildDashboardQuickSearchItems } from "@/lib/dashboardQuickSearch";
14
+ import { parseTaskTemplatesFromPayload } from "@/lib/taskTemplateDraft";
15
+ import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
16
+ import { computeToolbarSessionSlices } from "@/lib/appShellToolbarSessionSlices";
17
+ import {
18
+ stashAppShellDeferredNewSession,
19
+ stashAppShellDeferredScroll,
20
+ stashAppShellDeferredSessionListFocus,
21
+ stashAppShellDeferredTaskFocus,
22
+ stashAppShellDeferredTaskTemplateDraft,
23
+ } from "@/lib/appShellToolbarDeferredIntents";
24
+
25
+ type ToolbarLiveShape = { sessionId?: string };
26
+
27
+ export type AppShellToolbarCommandCenterProps = Readonly<{
28
+ dt: DashboardStrings;
29
+ lang: Lang;
30
+ /** Valeur courante de `?session=` sur la page (navigation stable). */
31
+ dashboardSessionNavId: string | null;
32
+ onManualRefresh: () => void | Promise<unknown>;
33
+ toolbarDomId?: string;
34
+ }>;
35
+
36
+ export function AppShellToolbarCommandCenter({
37
+ dt,
38
+ lang,
39
+ dashboardSessionNavId,
40
+ onManualRefresh,
41
+ toolbarDomId,
42
+ }: AppShellToolbarCommandCenterProps) {
43
+ const router = useRouter();
44
+ const { payload, refresh } = useKronosysPayload();
45
+
46
+ const navSessionTrim =
47
+ typeof dashboardSessionNavId === "string"
48
+ ? dashboardSessionNavId.trim()
49
+ : "";
50
+
51
+ const pushHome = useCallback(
52
+ (sessionId?: string | null) => {
53
+ void router.push(
54
+ withDashboardSessionParam(
55
+ "/",
56
+ typeof sessionId === "string" && sessionId.trim() !== ""
57
+ ? sessionId.trim()
58
+ : undefined,
59
+ ),
60
+ );
61
+ },
62
+ [router],
63
+ );
64
+
65
+ const postAndRefresh = useCallback(
66
+ async (body: Record<string, unknown>) => {
67
+ await postKronosysAction(body);
68
+ await refresh({ routerInvalidate: true });
69
+ },
70
+ [refresh],
71
+ );
72
+
73
+ const selectSessionAcrossApp = useCallback(
74
+ async (sessionId: string) => {
75
+ const { live } = computeToolbarSessionSlices(payload, navSessionTrim);
76
+ const liveSid =
77
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
78
+ const pickLive = sessionId === liveSid;
79
+ if (pickLive) {
80
+ await postKronosysAction({ type: "inspectSession", sessionId: null });
81
+ await postKronosysAction({ type: "setPaused", paused: false });
82
+ } else {
83
+ await postKronosysAction({ type: "inspectSession", sessionId });
84
+ }
85
+ await refresh({ routerInvalidate: true });
86
+ pushHome(pickLive ? null : sessionId);
87
+ },
88
+ [navSessionTrim, payload, pushHome, refresh],
89
+ );
90
+
91
+ const commandHandlers = useMemo(
92
+ (): DashboardCommandHandlers => ({
93
+ newSession: () => {
94
+ stashAppShellDeferredNewSession();
95
+ pushHome(navSessionTrim !== "" ? navSessionTrim : null);
96
+ },
97
+ refresh: () => void onManualRefresh(),
98
+ openReporting: () =>
99
+ void router.push(
100
+ withDashboardSessionParam("/reporting", navSessionTrim || undefined),
101
+ ),
102
+ openSettings: () =>
103
+ void router.push(
104
+ withDashboardSessionParam("/settings", navSessionTrim || undefined),
105
+ ),
106
+ openUserGuide: () =>
107
+ void router.push(
108
+ withDashboardSessionParam("/guide", navSessionTrim || undefined),
109
+ ),
110
+ focusSessions: () => {
111
+ stashAppShellDeferredScroll("sessions");
112
+ pushHome(navSessionTrim !== "" ? navSessionTrim : null);
113
+ },
114
+ focusTasks: () => {
115
+ stashAppShellDeferredScroll("tasks");
116
+ pushHome(navSessionTrim !== "" ? navSessionTrim : null);
117
+ },
118
+ focusTags: () => {
119
+ stashAppShellDeferredScroll("tags");
120
+ pushHome(navSessionTrim !== "" ? navSessionTrim : null);
121
+ },
122
+ toggleLang: () =>
123
+ void postAndRefresh({
124
+ type: "setLanguage",
125
+ lang: lang === "fr" ? "en" : "fr",
126
+ }),
127
+ }),
128
+ [lang, navSessionTrim, onManualRefresh, postAndRefresh, pushHome, router],
129
+ );
130
+
131
+ const { history, historyArchived, sessionCurrent, live } = useMemo(
132
+ () => computeToolbarSessionSlices(payload, navSessionTrim),
133
+ [navSessionTrim, payload],
134
+ );
135
+
136
+ const liveSearchContext =
137
+ live &&
138
+ typeof (live as ToolbarLiveShape).sessionId === "string" &&
139
+ (live as ToolbarLiveShape).sessionId!.trim() !== ""
140
+ ? {
141
+ sessionId: (live as ToolbarLiveShape).sessionId!.trim(),
142
+ sessionPaused: (live as { isPaused?: boolean }).isPaused === true,
143
+ }
144
+ : null;
145
+
146
+ const taskTemplatesForSearch = useMemo(
147
+ () =>
148
+ parseTaskTemplatesFromPayload(
149
+ (payload as Record<string, unknown> | null | undefined)?.taskTemplates,
150
+ ),
151
+ [payload],
152
+ );
153
+
154
+ const quickSearchItems = useMemo(
155
+ () =>
156
+ buildDashboardQuickSearchItems({
157
+ history,
158
+ historyArchived,
159
+ sessionCurrent,
160
+ dt,
161
+ liveSearchContext,
162
+ taskTemplates: taskTemplatesForSearch,
163
+ onSelectTaskTemplate: (draft) => {
164
+ stashAppShellDeferredTaskTemplateDraft(draft);
165
+ stashAppShellDeferredScroll("tasks");
166
+ const sid =
167
+ typeof (sessionCurrent as { sessionId?: string } | undefined)
168
+ ?.sessionId === "string"
169
+ ? (sessionCurrent as { sessionId: string }).sessionId.trim()
170
+ : typeof live?.sessionId === "string"
171
+ ? live.sessionId.trim()
172
+ : navSessionTrim !== ""
173
+ ? navSessionTrim
174
+ : "";
175
+ pushHome(sid !== "" ? sid : null);
176
+ },
177
+ onSelectSession: (id) => {
178
+ void selectSessionAcrossApp(id);
179
+ },
180
+ scrollToSession: (sessionId) => {
181
+ stashAppShellDeferredSessionListFocus(sessionId);
182
+ void selectSessionAcrossApp(sessionId);
183
+ },
184
+ focusTasksColumn: () => {
185
+ stashAppShellDeferredScroll("tasks");
186
+ pushHome(navSessionTrim !== "" ? navSessionTrim : null);
187
+ },
188
+ scrollToTask: (taskId) => {
189
+ stashAppShellDeferredTaskFocus(taskId);
190
+ stashAppShellDeferredScroll("tasks");
191
+ const sid =
192
+ typeof (sessionCurrent as { sessionId?: string } | undefined)
193
+ ?.sessionId === "string"
194
+ ? (sessionCurrent as { sessionId: string }).sessionId.trim()
195
+ : typeof live?.sessionId === "string"
196
+ ? live.sessionId.trim()
197
+ : navSessionTrim !== ""
198
+ ? navSessionTrim
199
+ : "";
200
+ pushHome(sid !== "" ? sid : null);
201
+ },
202
+ }),
203
+ [
204
+ dt,
205
+ history,
206
+ historyArchived,
207
+ live,
208
+ liveSearchContext,
209
+ navSessionTrim,
210
+ pushHome,
211
+ selectSessionAcrossApp,
212
+ sessionCurrent,
213
+ taskTemplatesForSearch,
214
+ ],
215
+ );
216
+
217
+ return (
218
+ <DashboardCommandCenter
219
+ dt={dt}
220
+ handlers={commandHandlers}
221
+ searchItems={quickSearchItems}
222
+ toolbarDomId={toolbarDomId}
223
+ />
224
+ );
225
+ }
@@ -0,0 +1,204 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { usePathname, useRouter } from "next/navigation";
5
+ import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
6
+ import { postKronosysAction } from "@/lib/kronosysApi";
7
+ import { GlobalPauseConfirmModal } from "@/components/dashboard/GlobalPauseConfirmModal";
8
+ import {
9
+ AppShellRouteNav,
10
+ type AppShellRouteNavCurrent,
11
+ type AppShellRouteNavLabels,
12
+ } from "@/components/dashboard/AppShellRouteNav";
13
+ import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
14
+ import {
15
+ buildGlobalPauseActivationPreview,
16
+ isGlobalPauseActivationNoOp,
17
+ } from "@/lib/globalPausePreview";
18
+ import { readDashboardUse24HourClockFromCfg } from "@/lib/dashboardClockFormat";
19
+ import { readDashboardTimeZoneFromCfg } from "@/lib/dashboardTimeZone";
20
+ import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
21
+ import { stashAppShellDeferredTodayGantt } from "@/lib/appShellToolbarDeferredIntents";
22
+
23
+ type ToolbarLiveShape = {
24
+ sessionId?: string;
25
+ archived?: boolean;
26
+ globalPauseContext?: unknown;
27
+ };
28
+
29
+ export type AppShellToolbarRouteNavProps = Readonly<{
30
+ current: AppShellRouteNavCurrent;
31
+ labels: AppShellRouteNavLabels;
32
+ navAriaLabel: string;
33
+ dashboardSessionId?: string | null;
34
+ lang: Lang;
35
+ dt: DashboardStrings;
36
+ /** Tableau de bord : ouvre le Gantt « aujourd’hui » sans quitter la page. */
37
+ onOpenTodayGantt?: () => void;
38
+ }>;
39
+
40
+ export function AppShellToolbarRouteNav({
41
+ current,
42
+ labels,
43
+ navAriaLabel,
44
+ dashboardSessionId,
45
+ lang,
46
+ dt,
47
+ onOpenTodayGantt,
48
+ }: AppShellToolbarRouteNavProps) {
49
+ const { payload, refresh } = useKronosysPayload();
50
+ const router = useRouter();
51
+ const pathname = usePathname() ?? "";
52
+ const isHome = pathname === "/" || pathname === "";
53
+ const [globalPauseConfirmOpen, setGlobalPauseConfirmOpen] = useState(false);
54
+
55
+ const live = payload?.current as ToolbarLiveShape | undefined;
56
+ const globalPauseContextActive = Boolean(
57
+ live && typeof live === "object" && live.globalPauseContext,
58
+ );
59
+ const globalPauseActivationPreview = useMemo(
60
+ () =>
61
+ live && typeof live === "object"
62
+ ? buildGlobalPauseActivationPreview(live as Record<string, unknown>)
63
+ : null,
64
+ [live],
65
+ );
66
+ const globalPauseNavDisabled = useMemo(
67
+ () =>
68
+ !globalPauseContextActive &&
69
+ globalPauseActivationPreview !== null &&
70
+ isGlobalPauseActivationNoOp(globalPauseActivationPreview),
71
+ [globalPauseActivationPreview, globalPauseContextActive],
72
+ );
73
+
74
+ const post = useCallback(
75
+ async (body: Record<string, unknown>) => {
76
+ await postKronosysAction(body);
77
+ await refresh({ routerInvalidate: true });
78
+ },
79
+ [refresh],
80
+ );
81
+
82
+ const onGlobalPauseNavPress = useCallback(() => {
83
+ if (globalPauseContextActive) {
84
+ void post({ type: "toggleGlobalPauseContext" });
85
+ return;
86
+ }
87
+ if (globalPauseNavDisabled) {
88
+ return;
89
+ }
90
+ setGlobalPauseConfirmOpen(true);
91
+ }, [globalPauseContextActive, globalPauseNavDisabled, post]);
92
+
93
+ const confirmGlobalPauseActivation = useCallback(() => {
94
+ setGlobalPauseConfirmOpen(false);
95
+ void post({ type: "toggleGlobalPauseContext" });
96
+ }, [post]);
97
+
98
+ const cancelGlobalPauseConfirm = useCallback(() => {
99
+ setGlobalPauseConfirmOpen(false);
100
+ }, []);
101
+
102
+ useEffect(() => {
103
+ if (globalPauseContextActive) {
104
+ setGlobalPauseConfirmOpen(false);
105
+ }
106
+ }, [globalPauseContextActive]);
107
+
108
+ useEffect(() => {
109
+ if (!live) {
110
+ setGlobalPauseConfirmOpen(false);
111
+ }
112
+ }, [live]);
113
+
114
+ const navSessionTrim =
115
+ typeof dashboardSessionId === "string" ? dashboardSessionId.trim() : "";
116
+
117
+ const ganttOnPress = useCallback(() => {
118
+ if (onOpenTodayGantt && isHome) {
119
+ onOpenTodayGantt();
120
+ return;
121
+ }
122
+ stashAppShellDeferredTodayGantt();
123
+ void router.push(
124
+ withDashboardSessionParam(
125
+ "/",
126
+ navSessionTrim !== "" ? navSessionTrim : undefined,
127
+ ),
128
+ );
129
+ }, [isHome, navSessionTrim, onOpenTodayGantt, router]);
130
+
131
+ const ganttControl = useMemo(
132
+ () => ({
133
+ label: labels.navGanttButtonLabel,
134
+ onPress: ganttOnPress,
135
+ }),
136
+ [ganttOnPress, labels.navGanttButtonLabel],
137
+ );
138
+
139
+ const globalPauseControl = useMemo(() => {
140
+ if (live && live.archived !== true) {
141
+ return {
142
+ active: globalPauseContextActive,
143
+ label: globalPauseContextActive
144
+ ? dt.appShellRouteNavGlobalResume
145
+ : dt.appShellRouteNavGlobalPause,
146
+ disabled: globalPauseNavDisabled,
147
+ disabledTooltip: dt.appShellRouteNavGlobalPauseDisabledTooltip,
148
+ onPress: onGlobalPauseNavPress,
149
+ };
150
+ }
151
+ return {
152
+ active: false,
153
+ label: labels.navGlobalPauseButtonLabel,
154
+ disabled: true,
155
+ disabledTooltip:
156
+ current === "dashboard"
157
+ ? labels.navGlobalPauseNoSessionContextTooltip
158
+ : labels.navGlobalPauseDashboardOnlyTooltip,
159
+ onPress: () => {},
160
+ };
161
+ }, [
162
+ current,
163
+ dt.appShellRouteNavGlobalPause,
164
+ dt.appShellRouteNavGlobalPauseDisabledTooltip,
165
+ dt.appShellRouteNavGlobalResume,
166
+ globalPauseContextActive,
167
+ globalPauseNavDisabled,
168
+ labels.navGlobalPauseButtonLabel,
169
+ labels.navGlobalPauseDashboardOnlyTooltip,
170
+ labels.navGlobalPauseNoSessionContextTooltip,
171
+ live,
172
+ onGlobalPauseNavPress,
173
+ ]);
174
+
175
+ const displayTimeZone = readDashboardTimeZoneFromCfg(
176
+ payload?.cfg as Record<string, unknown> | undefined,
177
+ );
178
+ const use24HourClock = readDashboardUse24HourClockFromCfg(
179
+ payload?.cfg as Record<string, unknown> | undefined,
180
+ );
181
+
182
+ return (
183
+ <>
184
+ <GlobalPauseConfirmModal
185
+ open={globalPauseConfirmOpen}
186
+ preview={globalPauseActivationPreview}
187
+ lang={lang}
188
+ displayTimeZone={displayTimeZone}
189
+ use24HourClock={use24HourClock}
190
+ t={dt}
191
+ onCancel={cancelGlobalPauseConfirm}
192
+ onConfirm={confirmGlobalPauseActivation}
193
+ />
194
+ <AppShellRouteNav
195
+ current={current}
196
+ labels={labels}
197
+ navAriaLabel={navAriaLabel}
198
+ dashboardSessionId={dashboardSessionId}
199
+ ganttControl={ganttControl}
200
+ globalPauseControl={globalPauseControl}
201
+ />
202
+ </>
203
+ );
204
+ }