@open-mercato/core 0.6.6-develop.6451.1.5b071f47fc → 0.6.6-develop.6453.1.6cbc2d5d47
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +21 -34
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/useActiveTimesheetTimer.js +117 -0
- package/dist/modules/staff/lib/timesheets-ui/useActiveTimesheetTimer.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +58 -68
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +5 -73
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +20 -39
- package/src/modules/staff/lib/timesheets-ui/useActiveTimesheetTimer.ts +160 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +65 -89
- package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +5 -87
package/.turbo/turbo-build.log
CHANGED
|
@@ -5,10 +5,11 @@ import { Play, Square } from "lucide-react";
|
|
|
5
5
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
6
|
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
7
7
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
|
-
import {
|
|
8
|
+
import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
9
9
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
10
10
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
11
11
|
import { ProjectColorDot } from "./ProjectColorDot.js";
|
|
12
|
+
import { useActiveTimesheetTimer } from "./useActiveTimesheetTimer.js";
|
|
12
13
|
import { startTimerEntry } from "./startTimer.js";
|
|
13
14
|
import { resolveTimerActionError } from "./timerErrors.js";
|
|
14
15
|
const TIMER_MUTATION_CONTEXT_ID = "staff-timesheets-timer-bar";
|
|
@@ -31,8 +32,6 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
|
|
|
31
32
|
contextId: TIMER_MUTATION_CONTEXT_ID,
|
|
32
33
|
blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
|
|
33
34
|
});
|
|
34
|
-
const [activeEntryId, setActiveEntryId] = useState(null);
|
|
35
|
-
const [activeProjectId, setActiveProjectId] = useState(null);
|
|
36
35
|
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
37
36
|
const [description, setDescription] = useState("");
|
|
38
37
|
const [selectedProjectId, setSelectedProjectId] = useState(null);
|
|
@@ -42,8 +41,13 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
|
|
|
42
41
|
const [projectFilter, setProjectFilter] = useState("");
|
|
43
42
|
const dropdownRef = useRef(null);
|
|
44
43
|
const intervalRef = useRef(null);
|
|
45
|
-
const
|
|
44
|
+
const activeTimer = useActiveTimesheetTimer({ staffMemberId });
|
|
45
|
+
const activeEntryId = activeTimer.entryId;
|
|
46
|
+
const activeProjectId = activeTimer.projectId;
|
|
47
|
+
const isRunning = activeTimer.running;
|
|
46
48
|
const activeProject = projects.find((p) => p.id === activeProjectId);
|
|
49
|
+
const activeProjectName = activeProject?.name ?? activeTimer.projectName;
|
|
50
|
+
const activeProjectColor = activeProject?.color ?? activeTimer.projectColor;
|
|
47
51
|
const selectedProject = projects.find((p) => p.id === selectedProjectId);
|
|
48
52
|
const filteredProjects = projects.filter(
|
|
49
53
|
(p) => p.name.toLowerCase().includes(projectFilter.toLowerCase())
|
|
@@ -65,26 +69,13 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
|
|
|
65
69
|
setElapsedSeconds(0);
|
|
66
70
|
}, []);
|
|
67
71
|
useEffect(() => {
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const items = data?.items ?? data?.data ?? [];
|
|
76
|
-
const activeEntry = items.find(
|
|
77
|
-
(entry) => entry.started_at && !entry.ended_at
|
|
78
|
-
);
|
|
79
|
-
if (activeEntry) {
|
|
80
|
-
setActiveEntryId(String(activeEntry.id ?? ""));
|
|
81
|
-
setActiveProjectId(String(activeEntry.time_project_id ?? ""));
|
|
82
|
-
setDescription(String(activeEntry.notes ?? ""));
|
|
83
|
-
startElapsedCounter(String(activeEntry.started_at ?? ""));
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
checkActiveTimer();
|
|
87
|
-
}, [staffMemberId, startElapsedCounter]);
|
|
72
|
+
if (activeTimer.running && activeTimer.startedAt) {
|
|
73
|
+
startElapsedCounter(activeTimer.startedAt);
|
|
74
|
+
if (activeTimer.notes != null) setDescription(activeTimer.notes);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
stopElapsedCounter();
|
|
78
|
+
}, [activeTimer.running, activeTimer.startedAt, activeTimer.notes, startElapsedCounter, stopElapsedCounter]);
|
|
88
79
|
useEffect(() => {
|
|
89
80
|
return () => {
|
|
90
81
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
@@ -113,7 +104,7 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
|
|
|
113
104
|
date: today,
|
|
114
105
|
notes: description || null
|
|
115
106
|
};
|
|
116
|
-
|
|
107
|
+
await runMutation({
|
|
117
108
|
operation: () => startTimerEntry(startPayload),
|
|
118
109
|
context: {
|
|
119
110
|
formId: TIMER_MUTATION_CONTEXT_ID,
|
|
@@ -125,9 +116,7 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
|
|
|
125
116
|
},
|
|
126
117
|
mutationPayload: startPayload
|
|
127
118
|
});
|
|
128
|
-
|
|
129
|
-
setActiveProjectId(selectedProjectId);
|
|
130
|
-
startElapsedCounter((/* @__PURE__ */ new Date()).toISOString());
|
|
119
|
+
await activeTimer.refresh();
|
|
131
120
|
} catch (err) {
|
|
132
121
|
flash(
|
|
133
122
|
resolveTimerActionError(err, t("staff.timesheets.my.timer.startError", "Failed to start timer")),
|
|
@@ -161,10 +150,8 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
|
|
|
161
150
|
},
|
|
162
151
|
mutationPayload: stopPayload
|
|
163
152
|
});
|
|
164
|
-
setActiveEntryId(null);
|
|
165
|
-
setActiveProjectId(null);
|
|
166
153
|
setDescription("");
|
|
167
|
-
|
|
154
|
+
await activeTimer.refresh();
|
|
168
155
|
onTimerStopped();
|
|
169
156
|
} catch (err) {
|
|
170
157
|
flash(
|
|
@@ -190,9 +177,9 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
|
|
|
190
177
|
className: "flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground"
|
|
191
178
|
}
|
|
192
179
|
),
|
|
193
|
-
/* @__PURE__ */ jsx("div", { className: "relative", ref: dropdownRef, children: isRunning ?
|
|
194
|
-
/* @__PURE__ */ jsx(ProjectColorDot, { colorKey:
|
|
195
|
-
|
|
180
|
+
/* @__PURE__ */ jsx("div", { className: "relative", ref: dropdownRef, children: isRunning ? activeProjectName ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted", children: [
|
|
181
|
+
/* @__PURE__ */ jsx(ProjectColorDot, { colorKey: activeProjectColor, projectName: activeProjectName, size: "xs" }),
|
|
182
|
+
activeProjectName
|
|
196
183
|
] }) : null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
197
184
|
/* @__PURE__ */ jsx(
|
|
198
185
|
Button,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/timesheets-ui/TimerBar.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport { useState, useEffect, useRef, useCallback } from 'react'\nimport { Play, Square } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ProjectColorDot } from './ProjectColorDot'\nimport { startTimerEntry } from './startTimer'\nimport { resolveTimerActionError } from './timerErrors'\n\ntype ProjectOption = {\n id: string\n name: string\n code: string | null\n color?: string | null\n}\n\ntype TimerBarProps = {\n projects: ProjectOption[]\n staffMemberId: string | null\n onTimerStopped: () => void\n}\n\nconst TIMER_MUTATION_CONTEXT_ID = 'staff-timesheets-timer-bar'\n\ntype TimerMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n staffMemberId: string | null\n action: 'timer-create' | 'timer-start' | 'timer-stop'\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction formatElapsed(seconds: number): string {\n const h = Math.floor(seconds / 3600)\n const m = Math.floor((seconds % 3600) / 60)\n const s = seconds % 60\n return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\n\nfunction getToday(): string {\n const now = new Date()\n const year = now.getFullYear()\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nexport function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarProps) {\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<TimerMutationContext>({\n contextId: TIMER_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const [activeEntryId, setActiveEntryId] = useState<string | null>(null)\n const [activeProjectId, setActiveProjectId] = useState<string | null>(null)\n const [elapsedSeconds, setElapsedSeconds] = useState(0)\n const [description, setDescription] = useState('')\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)\n const [isStarting, setIsStarting] = useState(false)\n const [isStopping, setIsStopping] = useState(false)\n const [showProjectDropdown, setShowProjectDropdown] = useState(false)\n const [projectFilter, setProjectFilter] = useState('')\n\n const dropdownRef = useRef<HTMLDivElement>(null)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n\n const isRunning = activeEntryId !== null\n\n const activeProject = projects.find((p) => p.id === activeProjectId)\n const selectedProject = projects.find((p) => p.id === selectedProjectId)\n\n const filteredProjects = projects.filter((p) =>\n p.name.toLowerCase().includes(projectFilter.toLowerCase()),\n )\n\n const startElapsedCounter = useCallback((startedAt: string) => {\n const startTime = new Date(startedAt).getTime()\n const calcElapsed = () => Math.max(0, Math.floor((Date.now() - startTime) / 1000))\n setElapsedSeconds(calcElapsed())\n\n if (intervalRef.current) clearInterval(intervalRef.current)\n intervalRef.current = setInterval(() => {\n setElapsedSeconds(calcElapsed())\n }, 1000)\n }, [])\n\n const stopElapsedCounter = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n setElapsedSeconds(0)\n }, [])\n\n useEffect(() => {\n if (!staffMemberId) return\n\n const checkActiveTimer = async () => {\n const response = await apiCall(\n `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&running=true&pageSize=50`,\n )\n if (!response.ok) return\n\n const data = response.result as Record<string, unknown> | null\n const items = (data?.items ?? data?.data ?? []) as Array<Record<string, unknown>>\n\n const activeEntry = items.find(\n (entry: Record<string, unknown>) =>\n entry.started_at && !entry.ended_at,\n )\n\n if (activeEntry) {\n setActiveEntryId(String(activeEntry.id ?? ''))\n setActiveProjectId(String(activeEntry.time_project_id ?? ''))\n setDescription(String(activeEntry.notes ?? ''))\n startElapsedCounter(String(activeEntry.started_at ?? ''))\n }\n }\n\n checkActiveTimer()\n }, [staffMemberId, startElapsedCounter])\n\n useEffect(() => {\n return () => {\n if (intervalRef.current) clearInterval(intervalRef.current)\n }\n }, [])\n\n useEffect(() => {\n function handleClickOutside(event: MouseEvent) {\n if (\n dropdownRef.current &&\n !dropdownRef.current.contains(event.target as Node)\n ) {\n setShowProjectDropdown(false)\n setProjectFilter('')\n }\n }\n\n if (showProjectDropdown) {\n document.addEventListener('mousedown', handleClickOutside)\n }\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [showProjectDropdown])\n\n const handleStart = async () => {\n if (!selectedProjectId || !staffMemberId) return\n\n setIsStarting(true)\n try {\n const today = getToday()\n const startPayload = {\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: description || null,\n }\n const { id: entryId } = await runMutation({\n operation: () => startTimerEntry(startPayload),\n context: {\n formId: TIMER_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: staffMemberId,\n staffMemberId,\n action: 'timer-start',\n retryLastMutation,\n },\n mutationPayload: startPayload,\n })\n\n setActiveEntryId(entryId)\n setActiveProjectId(selectedProjectId)\n startElapsedCounter(new Date().toISOString())\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),\n 'error',\n )\n } finally {\n setIsStarting(false)\n }\n }\n\n const handleStop = async () => {\n if (!activeEntryId) return\n\n setIsStopping(true)\n try {\n const stopPayload = {\n id: activeEntryId,\n action: 'timer-stop',\n staffMemberId,\n }\n await runMutation({\n operation: () =>\n apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,\n { method: 'POST' },\n ),\n context: {\n formId: TIMER_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: activeEntryId,\n staffMemberId,\n action: 'timer-stop',\n retryLastMutation,\n },\n mutationPayload: stopPayload,\n })\n\n setActiveEntryId(null)\n setActiveProjectId(null)\n setDescription('')\n stopElapsedCounter()\n onTimerStopped()\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.stopError', 'Failed to stop timer')),\n 'error',\n )\n } finally {\n setIsStopping(false)\n }\n }\n\n return (\n <div className=\"flex items-center gap-3 rounded-lg border bg-card p-3 mb-4\">\n <input\n type=\"text\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n readOnly={isRunning}\n placeholder={t(\n 'staff.timesheets.my.timer.placeholder',\n 'What are you working on?',\n )}\n className=\"flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground\"\n />\n\n <div className=\"relative\" ref={dropdownRef}>\n {isRunning ? (\n activeProject ? (\n <span className=\"inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted\">\n <ProjectColorDot colorKey={activeProject.color} projectName={activeProject.name} size=\"xs\" />\n {activeProject.name}\n </span>\n ) : null\n ) : (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setShowProjectDropdown(!showProjectDropdown)\n setProjectFilter('')\n }}\n className=\"text-xs font-medium\"\n >\n {selectedProject\n ? (<><ProjectColorDot colorKey={selectedProject.color} projectName={selectedProject.name} size=\"xs\" /><span className=\"ml-1\">{selectedProject.name}</span></>)\n : t('staff.timesheets.my.timer.selectProject', 'Project')}\n </Button>\n\n {showProjectDropdown && (\n <div className=\"absolute right-0 top-full mt-1 z-50 min-w-[200px] rounded-md border bg-popover p-1 shadow-md\">\n <input\n type=\"text\"\n value={projectFilter}\n onChange={(e) => setProjectFilter(e.target.value)}\n placeholder={t(\n 'staff.timesheets.my.timer.searchProject',\n 'Search projects...',\n )}\n className=\"w-full bg-transparent border-b px-2 py-1.5 text-xs outline-none placeholder:text-muted-foreground mb-1\"\n autoFocus\n />\n <div className=\"max-h-[200px] overflow-y-auto\">\n {filteredProjects.length === 0 ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {t(\n 'staff.timesheets.my.timer.noProjects',\n 'No projects found',\n )}\n </div>\n ) : (\n filteredProjects.map((project) => (\n <Button\n key={project.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs h-auto py-1.5\"\n onClick={() => {\n setSelectedProjectId(project.id)\n setShowProjectDropdown(false)\n setProjectFilter('')\n }}\n >\n <ProjectColorDot colorKey={project.color} projectName={project.name} size=\"xs\" />\n <span className=\"ml-1\">{project.name}</span>\n {project.code ? (\n <span className=\"ml-1 text-muted-foreground\">\n ({project.code})\n </span>\n ) : null}\n </Button>\n ))\n )}\n </div>\n </div>\n )}\n </>\n )}\n </div>\n\n <span className=\"font-mono text-sm tabular-nums min-w-[64px] text-right\">\n {formatElapsed(elapsedSeconds)}\n </span>\n\n {isRunning ? (\n <IconButton\n type=\"button\"\n variant=\"destructive\"\n size=\"default\"\n onClick={handleStop}\n disabled={isStopping}\n aria-label={t('staff.timesheets.my.timer.stop', 'Stop timer')}\n >\n <Square className=\"size-4\" />\n </IconButton>\n ) : (\n <IconButton\n type=\"button\"\n variant=\"primary\"\n size=\"default\"\n onClick={handleStart}\n disabled={isStarting || !selectedProjectId}\n aria-label={t('staff.timesheets.my.timer.start', 'Start timer')}\n >\n <Play className=\"size-4\" />\n </IconButton>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport { useState, useEffect, useRef, useCallback } from 'react'\nimport { Play, Square } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { ProjectColorDot } from './ProjectColorDot'\nimport { useActiveTimesheetTimer } from './useActiveTimesheetTimer'\nimport { startTimerEntry } from './startTimer'\nimport { resolveTimerActionError } from './timerErrors'\n\ntype ProjectOption = {\n id: string\n name: string\n code: string | null\n color?: string | null\n}\n\ntype TimerBarProps = {\n projects: ProjectOption[]\n staffMemberId: string | null\n onTimerStopped: () => void\n}\n\nconst TIMER_MUTATION_CONTEXT_ID = 'staff-timesheets-timer-bar'\n\ntype TimerMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n staffMemberId: string | null\n action: 'timer-create' | 'timer-start' | 'timer-stop'\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction formatElapsed(seconds: number): string {\n const h = Math.floor(seconds / 3600)\n const m = Math.floor((seconds % 3600) / 60)\n const s = seconds % 60\n return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\n\nfunction getToday(): string {\n const now = new Date()\n const year = now.getFullYear()\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n return `${year}-${month}-${day}`\n}\n\nexport function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarProps) {\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<TimerMutationContext>({\n contextId: TIMER_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const [elapsedSeconds, setElapsedSeconds] = useState(0)\n const [description, setDescription] = useState('')\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)\n const [isStarting, setIsStarting] = useState(false)\n const [isStopping, setIsStopping] = useState(false)\n const [showProjectDropdown, setShowProjectDropdown] = useState(false)\n const [projectFilter, setProjectFilter] = useState('')\n\n const dropdownRef = useRef<HTMLDivElement>(null)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const activeTimer = useActiveTimesheetTimer({ staffMemberId })\n\n const activeEntryId = activeTimer.entryId\n const activeProjectId = activeTimer.projectId\n const isRunning = activeTimer.running\n\n const activeProject = projects.find((p) => p.id === activeProjectId)\n const activeProjectName = activeProject?.name ?? activeTimer.projectName\n const activeProjectColor = activeProject?.color ?? activeTimer.projectColor\n const selectedProject = projects.find((p) => p.id === selectedProjectId)\n\n const filteredProjects = projects.filter((p) =>\n p.name.toLowerCase().includes(projectFilter.toLowerCase()),\n )\n\n const startElapsedCounter = useCallback((startedAt: string) => {\n const startTime = new Date(startedAt).getTime()\n const calcElapsed = () => Math.max(0, Math.floor((Date.now() - startTime) / 1000))\n setElapsedSeconds(calcElapsed())\n\n if (intervalRef.current) clearInterval(intervalRef.current)\n intervalRef.current = setInterval(() => {\n setElapsedSeconds(calcElapsed())\n }, 1000)\n }, [])\n\n const stopElapsedCounter = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n setElapsedSeconds(0)\n }, [])\n\n useEffect(() => {\n if (activeTimer.running && activeTimer.startedAt) {\n startElapsedCounter(activeTimer.startedAt)\n if (activeTimer.notes != null) setDescription(activeTimer.notes)\n return\n }\n stopElapsedCounter()\n }, [activeTimer.running, activeTimer.startedAt, activeTimer.notes, startElapsedCounter, stopElapsedCounter])\n\n useEffect(() => {\n return () => {\n if (intervalRef.current) clearInterval(intervalRef.current)\n }\n }, [])\n\n useEffect(() => {\n function handleClickOutside(event: MouseEvent) {\n if (\n dropdownRef.current &&\n !dropdownRef.current.contains(event.target as Node)\n ) {\n setShowProjectDropdown(false)\n setProjectFilter('')\n }\n }\n\n if (showProjectDropdown) {\n document.addEventListener('mousedown', handleClickOutside)\n }\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [showProjectDropdown])\n\n const handleStart = async () => {\n if (!selectedProjectId || !staffMemberId) return\n\n setIsStarting(true)\n try {\n const today = getToday()\n const startPayload = {\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: description || null,\n }\n await runMutation({\n operation: () => startTimerEntry(startPayload),\n context: {\n formId: TIMER_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: staffMemberId,\n staffMemberId,\n action: 'timer-start',\n retryLastMutation,\n },\n mutationPayload: startPayload,\n })\n\n await activeTimer.refresh()\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),\n 'error',\n )\n } finally {\n setIsStarting(false)\n }\n }\n\n const handleStop = async () => {\n if (!activeEntryId) return\n\n setIsStopping(true)\n try {\n const stopPayload = {\n id: activeEntryId,\n action: 'timer-stop',\n staffMemberId,\n }\n await runMutation({\n operation: () =>\n apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,\n { method: 'POST' },\n ),\n context: {\n formId: TIMER_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: activeEntryId,\n staffMemberId,\n action: 'timer-stop',\n retryLastMutation,\n },\n mutationPayload: stopPayload,\n })\n\n setDescription('')\n await activeTimer.refresh()\n onTimerStopped()\n } catch (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.stopError', 'Failed to stop timer')),\n 'error',\n )\n } finally {\n setIsStopping(false)\n }\n }\n\n return (\n <div className=\"flex items-center gap-3 rounded-lg border bg-card p-3 mb-4\">\n <input\n type=\"text\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n readOnly={isRunning}\n placeholder={t(\n 'staff.timesheets.my.timer.placeholder',\n 'What are you working on?',\n )}\n className=\"flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground\"\n />\n\n <div className=\"relative\" ref={dropdownRef}>\n {isRunning ? (\n activeProjectName ? (\n <span className=\"inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted\">\n <ProjectColorDot colorKey={activeProjectColor} projectName={activeProjectName} size=\"xs\" />\n {activeProjectName}\n </span>\n ) : null\n ) : (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setShowProjectDropdown(!showProjectDropdown)\n setProjectFilter('')\n }}\n className=\"text-xs font-medium\"\n >\n {selectedProject\n ? (<><ProjectColorDot colorKey={selectedProject.color} projectName={selectedProject.name} size=\"xs\" /><span className=\"ml-1\">{selectedProject.name}</span></>)\n : t('staff.timesheets.my.timer.selectProject', 'Project')}\n </Button>\n\n {showProjectDropdown && (\n <div className=\"absolute right-0 top-full mt-1 z-50 min-w-[200px] rounded-md border bg-popover p-1 shadow-md\">\n <input\n type=\"text\"\n value={projectFilter}\n onChange={(e) => setProjectFilter(e.target.value)}\n placeholder={t(\n 'staff.timesheets.my.timer.searchProject',\n 'Search projects...',\n )}\n className=\"w-full bg-transparent border-b px-2 py-1.5 text-xs outline-none placeholder:text-muted-foreground mb-1\"\n autoFocus\n />\n <div className=\"max-h-[200px] overflow-y-auto\">\n {filteredProjects.length === 0 ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {t(\n 'staff.timesheets.my.timer.noProjects',\n 'No projects found',\n )}\n </div>\n ) : (\n filteredProjects.map((project) => (\n <Button\n key={project.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs h-auto py-1.5\"\n onClick={() => {\n setSelectedProjectId(project.id)\n setShowProjectDropdown(false)\n setProjectFilter('')\n }}\n >\n <ProjectColorDot colorKey={project.color} projectName={project.name} size=\"xs\" />\n <span className=\"ml-1\">{project.name}</span>\n {project.code ? (\n <span className=\"ml-1 text-muted-foreground\">\n ({project.code})\n </span>\n ) : null}\n </Button>\n ))\n )}\n </div>\n </div>\n )}\n </>\n )}\n </div>\n\n <span className=\"font-mono text-sm tabular-nums min-w-[64px] text-right\">\n {formatElapsed(elapsedSeconds)}\n </span>\n\n {isRunning ? (\n <IconButton\n type=\"button\"\n variant=\"destructive\"\n size=\"default\"\n onClick={handleStop}\n disabled={isStopping}\n aria-label={t('staff.timesheets.my.timer.stop', 'Stop timer')}\n >\n <Square className=\"size-4\" />\n </IconButton>\n ) : (\n <IconButton\n type=\"button\"\n variant=\"primary\"\n size=\"default\"\n onClick={handleStart}\n disabled={isStarting || !selectedProjectId}\n aria-label={t('staff.timesheets.my.timer.start', 'Start timer')}\n >\n <Play className=\"size-4\" />\n </IconButton>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
const ACTIVE_TIMER_REVALIDATE_MS = 3e4;
|
|
6
|
+
const BASE_ACTIVE_TIMER_KEY = ["staff", "timesheets", "activeTimer"];
|
|
7
|
+
const EMPTY_ACTIVE_TIMER = {
|
|
8
|
+
staffMemberId: null,
|
|
9
|
+
entryId: null,
|
|
10
|
+
running: false,
|
|
11
|
+
startedAt: null,
|
|
12
|
+
projectId: null,
|
|
13
|
+
projectName: null,
|
|
14
|
+
projectColor: null,
|
|
15
|
+
notes: null
|
|
16
|
+
};
|
|
17
|
+
const activeTimesheetTimerQueryKey = () => BASE_ACTIVE_TIMER_KEY;
|
|
18
|
+
function getString(value) {
|
|
19
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
20
|
+
}
|
|
21
|
+
function getErrorMessage(result, fallback) {
|
|
22
|
+
if (result && typeof result === "object") {
|
|
23
|
+
const error = result.error;
|
|
24
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
25
|
+
}
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
async function loadCurrentStaffMemberId(staffMemberId) {
|
|
29
|
+
if (staffMemberId) return staffMemberId;
|
|
30
|
+
const selfRes = await apiCall("/api/staff/team-members/self");
|
|
31
|
+
if (!selfRes.ok) {
|
|
32
|
+
throw new Error(getErrorMessage(selfRes.result, "Failed to load current staff member."));
|
|
33
|
+
}
|
|
34
|
+
return selfRes.result?.member?.id ?? null;
|
|
35
|
+
}
|
|
36
|
+
async function loadProjectDisplay(projectId) {
|
|
37
|
+
const projectRes = await apiCall(
|
|
38
|
+
`/api/staff/timesheets/time-projects?ids=${encodeURIComponent(projectId)}&pageSize=1`
|
|
39
|
+
);
|
|
40
|
+
if (!projectRes.ok) {
|
|
41
|
+
return { projectName: null, projectColor: null };
|
|
42
|
+
}
|
|
43
|
+
const project = projectRes.result?.items?.[0];
|
|
44
|
+
return {
|
|
45
|
+
projectName: getString(project?.name),
|
|
46
|
+
projectColor: getString(project?.color)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function fetchActiveTimesheetTimer(staffMemberId) {
|
|
50
|
+
const memberId = await loadCurrentStaffMemberId(staffMemberId);
|
|
51
|
+
if (!memberId) {
|
|
52
|
+
return EMPTY_ACTIVE_TIMER;
|
|
53
|
+
}
|
|
54
|
+
const entriesRes = await apiCall(
|
|
55
|
+
`/api/staff/timesheets/time-entries?staffMemberId=${encodeURIComponent(memberId)}&running=true&pageSize=50`
|
|
56
|
+
);
|
|
57
|
+
if (!entriesRes.ok) {
|
|
58
|
+
throw new Error(getErrorMessage(entriesRes.result, "Failed to load active timer."));
|
|
59
|
+
}
|
|
60
|
+
const entries = Array.isArray(entriesRes.result?.items) ? entriesRes.result.items : [];
|
|
61
|
+
const active = entries.find((entry) => {
|
|
62
|
+
const startedAt = entry.started_at ?? entry.startedAt;
|
|
63
|
+
const endedAt = entry.ended_at ?? entry.endedAt;
|
|
64
|
+
return startedAt != null && endedAt == null;
|
|
65
|
+
});
|
|
66
|
+
if (!active) {
|
|
67
|
+
return { ...EMPTY_ACTIVE_TIMER, staffMemberId: memberId };
|
|
68
|
+
}
|
|
69
|
+
const projectId = getString(active.time_project_id ?? active.timeProjectId);
|
|
70
|
+
const projectDisplay = projectId ? await loadProjectDisplay(projectId) : { projectName: null, projectColor: null };
|
|
71
|
+
return {
|
|
72
|
+
staffMemberId: memberId,
|
|
73
|
+
entryId: getString(active.id),
|
|
74
|
+
running: true,
|
|
75
|
+
startedAt: getString(active.started_at ?? active.startedAt),
|
|
76
|
+
projectId,
|
|
77
|
+
projectName: projectDisplay.projectName,
|
|
78
|
+
projectColor: projectDisplay.projectColor,
|
|
79
|
+
notes: getString(active.notes)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const activeTimesheetTimerQueryOptions = (staffMemberId) => ({
|
|
83
|
+
queryKey: activeTimesheetTimerQueryKey(),
|
|
84
|
+
staleTime: ACTIVE_TIMER_REVALIDATE_MS,
|
|
85
|
+
gcTime: ACTIVE_TIMER_REVALIDATE_MS,
|
|
86
|
+
refetchInterval: ACTIVE_TIMER_REVALIDATE_MS,
|
|
87
|
+
queryFn: () => fetchActiveTimesheetTimer(staffMemberId)
|
|
88
|
+
});
|
|
89
|
+
async function refreshActiveTimesheetTimer(queryClient, staffMemberId) {
|
|
90
|
+
return queryClient.fetchQuery({
|
|
91
|
+
...activeTimesheetTimerQueryOptions(staffMemberId),
|
|
92
|
+
staleTime: 0
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function useActiveTimesheetTimer(options = {}) {
|
|
96
|
+
const queryClient = useQueryClient();
|
|
97
|
+
const query = useQuery(activeTimesheetTimerQueryOptions(options.staffMemberId));
|
|
98
|
+
const data = query.data ?? EMPTY_ACTIVE_TIMER;
|
|
99
|
+
const refresh = React.useCallback(
|
|
100
|
+
() => refreshActiveTimesheetTimer(queryClient, options.staffMemberId),
|
|
101
|
+
[queryClient, options.staffMemberId]
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
...data,
|
|
105
|
+
isLoading: query.isLoading,
|
|
106
|
+
isFetching: query.isFetching,
|
|
107
|
+
error: query.error instanceof Error ? query.error : null,
|
|
108
|
+
refresh
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
activeTimesheetTimerQueryKey,
|
|
113
|
+
activeTimesheetTimerQueryOptions,
|
|
114
|
+
refreshActiveTimesheetTimer,
|
|
115
|
+
useActiveTimesheetTimer
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=useActiveTimesheetTimer.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/staff/lib/timesheets-ui/useActiveTimesheetTimer.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport * as React from 'react'\nimport { useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'\n\nexport type ActiveTimesheetTimer = {\n staffMemberId: string | null\n entryId: string | null\n running: boolean\n startedAt: string | null\n projectId: string | null\n projectName: string | null\n projectColor: string | null\n notes: string | null\n}\n\ntype ActiveTimesheetTimerResult = ActiveTimesheetTimer & {\n isLoading: boolean\n isFetching: boolean\n error: Error | null\n refresh: () => Promise<ActiveTimesheetTimer>\n}\n\ntype ActiveTimesheetTimerOptions = {\n staffMemberId?: string | null\n}\n\nconst ACTIVE_TIMER_REVALIDATE_MS = 30_000\nconst BASE_ACTIVE_TIMER_KEY = ['staff', 'timesheets', 'activeTimer'] as const\n\nconst EMPTY_ACTIVE_TIMER: ActiveTimesheetTimer = {\n staffMemberId: null,\n entryId: null,\n running: false,\n startedAt: null,\n projectId: null,\n projectName: null,\n projectColor: null,\n notes: null,\n}\n\nexport const activeTimesheetTimerQueryKey = () => BASE_ACTIVE_TIMER_KEY\n\nfunction getString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction getErrorMessage(result: unknown, fallback: string): string {\n if (result && typeof result === 'object') {\n const error = (result as Record<string, unknown>).error\n if (typeof error === 'string' && error.length > 0) return error\n }\n return fallback\n}\n\nasync function loadCurrentStaffMemberId(staffMemberId?: string | null): Promise<string | null> {\n if (staffMemberId) return staffMemberId\n const selfRes = await apiCall<{ member?: { id?: string | null } | null }>('/api/staff/team-members/self')\n if (!selfRes.ok) {\n throw new Error(getErrorMessage(selfRes.result, 'Failed to load current staff member.'))\n }\n return selfRes.result?.member?.id ?? null\n}\n\nasync function loadProjectDisplay(projectId: string): Promise<Pick<ActiveTimesheetTimer, 'projectName' | 'projectColor'>> {\n const projectRes = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-projects?ids=${encodeURIComponent(projectId)}&pageSize=1`,\n )\n if (!projectRes.ok) {\n return { projectName: null, projectColor: null }\n }\n const project = projectRes.result?.items?.[0]\n return {\n projectName: getString(project?.name),\n projectColor: getString(project?.color),\n }\n}\n\nasync function fetchActiveTimesheetTimer(staffMemberId?: string | null): Promise<ActiveTimesheetTimer> {\n const memberId = await loadCurrentStaffMemberId(staffMemberId)\n if (!memberId) {\n return EMPTY_ACTIVE_TIMER\n }\n\n // Look up the active timer by running state rather than by today's date (issue #3717):\n // a timer started before midnight is still running the next day, and a from=today&to=today\n // lookup would hide it once the date rolls over, leaving an orphaned timer unstoppable.\n const entriesRes = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-entries?staffMemberId=${encodeURIComponent(memberId)}&running=true&pageSize=50`,\n )\n if (!entriesRes.ok) {\n throw new Error(getErrorMessage(entriesRes.result, 'Failed to load active timer.'))\n }\n\n const entries = Array.isArray(entriesRes.result?.items) ? entriesRes.result.items : []\n const active = entries.find((entry) => {\n const startedAt = entry.started_at ?? entry.startedAt\n const endedAt = entry.ended_at ?? entry.endedAt\n return startedAt != null && endedAt == null\n })\n\n if (!active) {\n return { ...EMPTY_ACTIVE_TIMER, staffMemberId: memberId }\n }\n\n const projectId = getString(active.time_project_id ?? active.timeProjectId)\n const projectDisplay = projectId\n ? await loadProjectDisplay(projectId)\n : { projectName: null, projectColor: null }\n\n return {\n staffMemberId: memberId,\n entryId: getString(active.id),\n running: true,\n startedAt: getString(active.started_at ?? active.startedAt),\n projectId,\n projectName: projectDisplay.projectName,\n projectColor: projectDisplay.projectColor,\n notes: getString(active.notes),\n }\n}\n\nexport const activeTimesheetTimerQueryOptions = (staffMemberId?: string | null) => ({\n queryKey: activeTimesheetTimerQueryKey(),\n staleTime: ACTIVE_TIMER_REVALIDATE_MS,\n gcTime: ACTIVE_TIMER_REVALIDATE_MS,\n refetchInterval: ACTIVE_TIMER_REVALIDATE_MS,\n queryFn: () => fetchActiveTimesheetTimer(staffMemberId),\n})\n\nexport async function refreshActiveTimesheetTimer(\n queryClient: QueryClient,\n staffMemberId?: string | null,\n): Promise<ActiveTimesheetTimer> {\n return queryClient.fetchQuery({\n ...activeTimesheetTimerQueryOptions(staffMemberId),\n staleTime: 0,\n })\n}\n\nexport function useActiveTimesheetTimer(\n options: ActiveTimesheetTimerOptions = {},\n): ActiveTimesheetTimerResult {\n const queryClient = useQueryClient()\n const query = useQuery(activeTimesheetTimerQueryOptions(options.staffMemberId))\n const data = query.data ?? EMPTY_ACTIVE_TIMER\n const refresh = React.useCallback(\n () => refreshActiveTimesheetTimer(queryClient, options.staffMemberId),\n [queryClient, options.staffMemberId],\n )\n\n return {\n ...data,\n isLoading: query.isLoading,\n isFetching: query.isFetching,\n error: query.error instanceof Error ? query.error : null,\n refresh,\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,eAAe;AACxB,YAAY,WAAW;AACvB,SAAS,UAAU,sBAAwC;AAwB3D,MAAM,6BAA6B;AACnC,MAAM,wBAAwB,CAAC,SAAS,cAAc,aAAa;AAEnE,MAAM,qBAA2C;AAAA,EAC/C,eAAe;AAAA,EACf,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc;AAAA,EACd,OAAO;AACT;AAEO,MAAM,+BAA+B,MAAM;AAElD,SAAS,UAAU,OAA+B;AAChD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEA,SAAS,gBAAgB,QAAiB,UAA0B;AAClE,MAAI,UAAU,OAAO,WAAW,UAAU;AACxC,UAAM,QAAS,OAAmC;AAClD,QAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO;AAAA,EAC5D;AACA,SAAO;AACT;AAEA,eAAe,yBAAyB,eAAuD;AAC7F,MAAI,cAAe,QAAO;AAC1B,QAAM,UAAU,MAAM,QAAoD,8BAA8B;AACxG,MAAI,CAAC,QAAQ,IAAI;AACf,UAAM,IAAI,MAAM,gBAAgB,QAAQ,QAAQ,sCAAsC,CAAC;AAAA,EACzF;AACA,SAAO,QAAQ,QAAQ,QAAQ,MAAM;AACvC;AAEA,eAAe,mBAAmB,WAAwF;AACxH,QAAM,aAAa,MAAM;AAAA,IACvB,2CAA2C,mBAAmB,SAAS,CAAC;AAAA,EAC1E;AACA,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,EAAE,aAAa,MAAM,cAAc,KAAK;AAAA,EACjD;AACA,QAAM,UAAU,WAAW,QAAQ,QAAQ,CAAC;AAC5C,SAAO;AAAA,IACL,aAAa,UAAU,SAAS,IAAI;AAAA,IACpC,cAAc,UAAU,SAAS,KAAK;AAAA,EACxC;AACF;AAEA,eAAe,0BAA0B,eAA8D;AACrG,QAAM,WAAW,MAAM,yBAAyB,aAAa;AAC7D,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AAKA,QAAM,aAAa,MAAM;AAAA,IACvB,oDAAoD,mBAAmB,QAAQ,CAAC;AAAA,EAClF;AACA,MAAI,CAAC,WAAW,IAAI;AAClB,UAAM,IAAI,MAAM,gBAAgB,WAAW,QAAQ,8BAA8B,CAAC;AAAA,EACpF;AAEA,QAAM,UAAU,MAAM,QAAQ,WAAW,QAAQ,KAAK,IAAI,WAAW,OAAO,QAAQ,CAAC;AACrF,QAAM,SAAS,QAAQ,KAAK,CAAC,UAAU;AACrC,UAAM,YAAY,MAAM,cAAc,MAAM;AAC5C,UAAM,UAAU,MAAM,YAAY,MAAM;AACxC,WAAO,aAAa,QAAQ,WAAW;AAAA,EACzC,CAAC;AAED,MAAI,CAAC,QAAQ;AACX,WAAO,EAAE,GAAG,oBAAoB,eAAe,SAAS;AAAA,EAC1D;AAEA,QAAM,YAAY,UAAU,OAAO,mBAAmB,OAAO,aAAa;AAC1E,QAAM,iBAAiB,YACnB,MAAM,mBAAmB,SAAS,IAClC,EAAE,aAAa,MAAM,cAAc,KAAK;AAE5C,SAAO;AAAA,IACL,eAAe;AAAA,IACf,SAAS,UAAU,OAAO,EAAE;AAAA,IAC5B,SAAS;AAAA,IACT,WAAW,UAAU,OAAO,cAAc,OAAO,SAAS;AAAA,IAC1D;AAAA,IACA,aAAa,eAAe;AAAA,IAC5B,cAAc,eAAe;AAAA,IAC7B,OAAO,UAAU,OAAO,KAAK;AAAA,EAC/B;AACF;AAEO,MAAM,mCAAmC,CAAC,mBAAmC;AAAA,EAClF,UAAU,6BAA6B;AAAA,EACvC,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,SAAS,MAAM,0BAA0B,aAAa;AACxD;AAEA,eAAsB,4BACpB,aACA,eAC+B;AAC/B,SAAO,YAAY,WAAW;AAAA,IAC5B,GAAG,iCAAiC,aAAa;AAAA,IACjD,WAAW;AAAA,EACb,CAAC;AACH;AAEO,SAAS,wBACd,UAAuC,CAAC,GACZ;AAC5B,QAAM,cAAc,eAAe;AACnC,QAAM,QAAQ,SAAS,iCAAiC,QAAQ,aAAa,CAAC;AAC9E,QAAM,OAAO,MAAM,QAAQ;AAC3B,QAAM,UAAU,MAAM;AAAA,IACpB,MAAM,4BAA4B,aAAa,QAAQ,aAAa;AAAA,IACpE,CAAC,aAAa,QAAQ,aAAa;AAAA,EACrC;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,WAAW,MAAM;AAAA,IACjB,YAAY,MAAM;AAAA,IAClB,OAAO,MAAM,iBAAiB,QAAQ,MAAM,QAAQ;AAAA,IACpD;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -5,6 +5,7 @@ import { apiCall, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/ap
|
|
|
5
5
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
6
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
7
7
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
|
+
import { useActiveTimesheetTimer } from "../../../lib/timesheets-ui/useActiveTimesheetTimer.js";
|
|
8
9
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
9
10
|
import { startTimerEntry } from "../../../lib/timesheets-ui/startTimer.js";
|
|
10
11
|
import { resolveTimerActionError } from "../../../lib/timesheets-ui/timerErrors.js";
|
|
@@ -34,68 +35,49 @@ const TimeReportingWidget = ({
|
|
|
34
35
|
const [projects, setProjects] = React.useState([]);
|
|
35
36
|
const [selectedProjectId, setSelectedProjectId] = React.useState(hydrated.lastProjectId);
|
|
36
37
|
const [notes, setNotes] = React.useState("");
|
|
37
|
-
const [timer, setTimer] = React.useState({ entryId: null, running: false, startedAt: null, projectId: null });
|
|
38
38
|
const [elapsed, setElapsed] = React.useState("00:00:00");
|
|
39
|
-
const [staffMemberId, setStaffMemberId] = React.useState(null);
|
|
40
39
|
const [loading, setLoading] = React.useState(true);
|
|
41
40
|
const [actionLoading, setActionLoading] = React.useState(false);
|
|
42
41
|
const [error, setError] = React.useState(null);
|
|
42
|
+
const didLoadRef = React.useRef(false);
|
|
43
|
+
const activeTimer = useActiveTimesheetTimer();
|
|
44
|
+
const {
|
|
45
|
+
staffMemberId,
|
|
46
|
+
entryId: activeEntryId,
|
|
47
|
+
running: timerRunning,
|
|
48
|
+
startedAt: timerStartedAt,
|
|
49
|
+
projectId: activeProjectId,
|
|
50
|
+
projectName: activeProjectName,
|
|
51
|
+
isLoading: activeTimerLoading,
|
|
52
|
+
error: activeTimerError,
|
|
53
|
+
refresh: refreshActiveTimer
|
|
54
|
+
} = activeTimer;
|
|
43
55
|
const loadState = React.useCallback(async () => {
|
|
44
56
|
onRefreshStateChange?.(true);
|
|
45
57
|
setLoading(true);
|
|
46
58
|
setError(null);
|
|
47
59
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
),
|
|
54
|
-
readApiResultOrThrow(
|
|
55
|
-
"/api/staff/team-members/self",
|
|
56
|
-
void 0,
|
|
57
|
-
{ errorMessage: "", fallback: { member: null } }
|
|
58
|
-
)
|
|
59
|
-
]);
|
|
60
|
+
const assignmentsRes = await readApiResultOrThrow(
|
|
61
|
+
"/api/staff/timesheets/my-projects?pageSize=100",
|
|
62
|
+
void 0,
|
|
63
|
+
{ errorMessage: "", fallback: { items: [] } }
|
|
64
|
+
);
|
|
60
65
|
const assignmentItems = Array.isArray(assignmentsRes.items) ? assignmentsRes.items : [];
|
|
61
66
|
const projectIds = assignmentItems.map((item) => String(item.time_project_id ?? item.timeProjectId ?? "")).filter((id) => id.length > 0);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const [projectsRes, entriesRes] = await Promise.all([
|
|
65
|
-
projectIds.length > 0 ? readApiResultOrThrow(
|
|
67
|
+
if (projectIds.length > 0) {
|
|
68
|
+
const projectsRes = await readApiResultOrThrow(
|
|
66
69
|
`/api/staff/timesheets/time-projects?ids=${projectIds.join(",")}&pageSize=100`,
|
|
67
70
|
void 0,
|
|
68
71
|
{ errorMessage: "", fallback: { items: [] } }
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
id: String(item.id ?? ""),
|
|
79
|
-
name: String(item.name ?? ""),
|
|
80
|
-
code: typeof item.code === "string" ? item.code : null
|
|
81
|
-
})));
|
|
82
|
-
if (memberId) {
|
|
83
|
-
const entries = Array.isArray(entriesRes.items) ? entriesRes.items : [];
|
|
84
|
-
const running = entries.find((entry) => {
|
|
85
|
-
const startedAt = entry.started_at ?? entry.startedAt;
|
|
86
|
-
const endedAt = entry.ended_at ?? entry.endedAt;
|
|
87
|
-
return startedAt != null && endedAt == null;
|
|
88
|
-
});
|
|
89
|
-
if (running) {
|
|
90
|
-
setTimer({
|
|
91
|
-
entryId: String(running.id ?? ""),
|
|
92
|
-
running: true,
|
|
93
|
-
startedAt: String(running.started_at ?? running.startedAt ?? ""),
|
|
94
|
-
projectId: String(running.time_project_id ?? running.timeProjectId ?? "")
|
|
95
|
-
});
|
|
96
|
-
} else {
|
|
97
|
-
setTimer({ entryId: null, running: false, startedAt: null, projectId: null });
|
|
98
|
-
}
|
|
72
|
+
);
|
|
73
|
+
const items = Array.isArray(projectsRes.items) ? projectsRes.items : [];
|
|
74
|
+
setProjects(items.map((item) => ({
|
|
75
|
+
id: String(item.id ?? ""),
|
|
76
|
+
name: String(item.name ?? ""),
|
|
77
|
+
code: typeof item.code === "string" ? item.code : null
|
|
78
|
+
})));
|
|
79
|
+
} else {
|
|
80
|
+
setProjects([]);
|
|
99
81
|
}
|
|
100
82
|
} catch (err) {
|
|
101
83
|
console.error("staff.timesheets.timeReporting.load", err);
|
|
@@ -106,18 +88,26 @@ const TimeReportingWidget = ({
|
|
|
106
88
|
}
|
|
107
89
|
}, [onRefreshStateChange, t]);
|
|
108
90
|
React.useEffect(() => {
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
if (!didLoadRef.current) {
|
|
92
|
+
didLoadRef.current = true;
|
|
93
|
+
void loadState();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
void Promise.allSettled([loadState(), refreshActiveTimer()]);
|
|
97
|
+
}, [loadState, refreshActiveTimer, refreshToken]);
|
|
111
98
|
React.useEffect(() => {
|
|
112
|
-
if (!
|
|
113
|
-
|
|
99
|
+
if (!timerRunning || !timerStartedAt) {
|
|
100
|
+
setElapsed("00:00:00");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const tick = () => setElapsed(formatElapsed(timerStartedAt));
|
|
114
104
|
tick();
|
|
115
105
|
const interval = setInterval(tick, 1e3);
|
|
116
106
|
return () => clearInterval(interval);
|
|
117
|
-
}, [
|
|
107
|
+
}, [timerRunning, timerStartedAt]);
|
|
118
108
|
const handleStart = React.useCallback(async () => {
|
|
119
109
|
if (!selectedProjectId || !staffMemberId) return;
|
|
120
|
-
if (
|
|
110
|
+
if (timerRunning) return;
|
|
121
111
|
setActionLoading(true);
|
|
122
112
|
try {
|
|
123
113
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -140,59 +130,59 @@ const TimeReportingWidget = ({
|
|
|
140
130
|
mutationPayload: startPayload
|
|
141
131
|
});
|
|
142
132
|
onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId });
|
|
143
|
-
await
|
|
133
|
+
await refreshActiveTimer();
|
|
144
134
|
} catch (err) {
|
|
145
135
|
console.error("staff.timesheets.timeReporting.start", err);
|
|
146
136
|
setError(resolveTimerActionError(err, t("staff.timesheets.widgets.timeReporting.startError", "Failed to start timer")));
|
|
147
137
|
} finally {
|
|
148
138
|
setActionLoading(false);
|
|
149
139
|
}
|
|
150
|
-
}, [selectedProjectId, staffMemberId,
|
|
140
|
+
}, [selectedProjectId, staffMemberId, timerRunning, notes, runMutation, retryLastMutation, hydrated, onSettingsChange, refreshActiveTimer, t]);
|
|
151
141
|
const handleStop = React.useCallback(async () => {
|
|
152
|
-
if (!
|
|
142
|
+
if (!activeEntryId || !staffMemberId) return;
|
|
153
143
|
setActionLoading(true);
|
|
154
144
|
try {
|
|
155
145
|
const stopPayload = {
|
|
156
|
-
id:
|
|
146
|
+
id: activeEntryId,
|
|
157
147
|
action: "timer-stop",
|
|
158
148
|
staffMemberId
|
|
159
149
|
};
|
|
160
150
|
await runMutation({
|
|
161
|
-
operation: () => apiCall(`/api/staff/timesheets/time-entries/${
|
|
151
|
+
operation: () => apiCall(`/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`, { method: "POST" }),
|
|
162
152
|
context: {
|
|
163
153
|
formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
|
|
164
154
|
resourceKind: "staff.timesheets.time_entry",
|
|
165
|
-
resourceId:
|
|
155
|
+
resourceId: activeEntryId,
|
|
166
156
|
staffMemberId,
|
|
167
157
|
action: "timer-stop",
|
|
168
158
|
retryLastMutation
|
|
169
159
|
},
|
|
170
160
|
mutationPayload: stopPayload
|
|
171
161
|
});
|
|
172
|
-
await
|
|
162
|
+
await refreshActiveTimer();
|
|
173
163
|
} catch (err) {
|
|
174
164
|
console.error("staff.timesheets.timeReporting.stop", err);
|
|
175
165
|
setError(resolveTimerActionError(err, t("staff.timesheets.widgets.timeReporting.stopError", "Failed to stop timer")));
|
|
176
166
|
} finally {
|
|
177
167
|
setActionLoading(false);
|
|
178
168
|
}
|
|
179
|
-
}, [
|
|
169
|
+
}, [activeEntryId, staffMemberId, runMutation, retryLastMutation, refreshActiveTimer, t]);
|
|
180
170
|
if (mode === "settings") {
|
|
181
171
|
return /* @__PURE__ */ jsx("div", { className: "space-y-2 text-sm", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("staff.timesheets.widgets.timeReporting.settings.description", "No additional settings. Select a project and start tracking from the widget.") }) });
|
|
182
172
|
}
|
|
183
|
-
if (loading) {
|
|
173
|
+
if (loading || activeTimerLoading) {
|
|
184
174
|
return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.timesheets.widgets.timeReporting.loading", "Loading...") }) });
|
|
185
175
|
}
|
|
186
|
-
if (error) {
|
|
187
|
-
return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", children: error }) });
|
|
176
|
+
if (error || activeTimerError) {
|
|
177
|
+
return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", children: error ?? t("staff.timesheets.widgets.timeReporting.error", "Failed to load timer state") }) });
|
|
188
178
|
}
|
|
189
179
|
if (projects.length === 0) {
|
|
190
180
|
return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.timesheets.widgets.timeReporting.noProjects", "No projects assigned.") }) });
|
|
191
181
|
}
|
|
192
|
-
if (
|
|
193
|
-
const runningProject = projects.find((p) => p.id ===
|
|
182
|
+
if (timerRunning) {
|
|
183
|
+
const runningProject = projects.find((p) => p.id === activeProjectId);
|
|
194
184
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
195
|
-
/* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: runningProject?.name ?? t("staff.timesheets.widgets.timeReporting.unknownProject", "Unknown project") }),
|
|
185
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: runningProject?.name ?? activeProjectName ?? t("staff.timesheets.widgets.timeReporting.unknownProject", "Unknown project") }),
|
|
196
186
|
/* @__PURE__ */ jsxs("div", { className: "text-center", children: [
|
|
197
187
|
/* @__PURE__ */ jsx("p", { className: "font-mono text-3xl font-bold tabular-nums", children: elapsed }),
|
|
198
188
|
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("staff.timesheets.widgets.timeReporting.running", "Timer running") })
|