@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.
@@ -1,4 +1,4 @@
1
- [build:core] found 3456 entry points
1
+ [build:core] found 3457 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 186 entry points
4
4
  [build:core:generated] built successfully
@@ -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 { apiCall, apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
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 isRunning = activeEntryId !== null;
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 (!staffMemberId) return;
69
- const checkActiveTimer = async () => {
70
- const response = await apiCall(
71
- `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&running=true&pageSize=50`
72
- );
73
- if (!response.ok) return;
74
- const data = response.result;
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
- const { id: entryId } = await runMutation({
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
- setActiveEntryId(entryId);
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
- stopElapsedCounter();
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 ? activeProject ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted", children: [
194
- /* @__PURE__ */ jsx(ProjectColorDot, { colorKey: activeProject.color, projectName: activeProject.name, size: "xs" }),
195
- activeProject.name
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": ";AA0OM,SAiCa,UAjCb,KAeM,YAfN;AAxON,SAAS,UAAU,WAAW,QAAQ,mBAAmB;AACzD,SAAS,MAAM,cAAc;AAC7B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,SAAS,sBAAsB;AACxC,SAAS,aAAa;AACtB,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AAexC,MAAM,4BAA4B;AAWlC,SAAS,cAAc,SAAyB;AAC9C,QAAM,IAAI,KAAK,MAAM,UAAU,IAAI;AACnC,QAAM,IAAI,KAAK,MAAO,UAAU,OAAQ,EAAE;AAC1C,QAAM,IAAI,UAAU;AACpB,SAAO,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AACzE;AAEA,SAAS,WAAmB;AAC1B,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,OAAO,IAAI,YAAY;AAC7B,QAAM,QAAQ,OAAO,IAAI,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACxD,QAAM,MAAM,OAAO,IAAI,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AACjD,SAAO,GAAG,IAAI,IAAI,KAAK,IAAI,GAAG;AAChC;AAEO,SAAS,SAAS,EAAE,UAAU,eAAe,eAAe,GAAkB;AACnF,QAAM,IAAI,KAAK;AACf,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAAyC;AAAA,IAClF,WAAW;AAAA,IACX,gBAAgB,EAAE,8BAA8B,4BAA4B;AAAA,EAC9E,CAAC;AAED,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAwB,IAAI;AACtE,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAwB,IAAI;AAC1E,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,SAAS,CAAC;AACtD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,EAAE;AACjD,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAwB,IAAI;AAC9E,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,qBAAqB,sBAAsB,IAAI,SAAS,KAAK;AACpE,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,EAAE;AAErD,QAAM,cAAc,OAAuB,IAAI;AAC/C,QAAM,cAAc,OAA8C,IAAI;AAEtE,QAAM,YAAY,kBAAkB;AAEpC,QAAM,gBAAgB,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,eAAe;AACnE,QAAM,kBAAkB,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,iBAAiB;AAEvE,QAAM,mBAAmB,SAAS;AAAA,IAAO,CAAC,MACxC,EAAE,KAAK,YAAY,EAAE,SAAS,cAAc,YAAY,CAAC;AAAA,EAC3D;AAEA,QAAM,sBAAsB,YAAY,CAAC,cAAsB;AAC7D,UAAM,YAAY,IAAI,KAAK,SAAS,EAAE,QAAQ;AAC9C,UAAM,cAAc,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI,CAAC;AACjF,sBAAkB,YAAY,CAAC;AAE/B,QAAI,YAAY,QAAS,eAAc,YAAY,OAAO;AAC1D,gBAAY,UAAU,YAAY,MAAM;AACtC,wBAAkB,YAAY,CAAC;AAAA,IACjC,GAAG,GAAI;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,qBAAqB,YAAY,MAAM;AAC3C,QAAI,YAAY,SAAS;AACvB,oBAAc,YAAY,OAAO;AACjC,kBAAY,UAAU;AAAA,IACxB;AACA,sBAAkB,CAAC;AAAA,EACrB,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,CAAC,cAAe;AAEpB,UAAM,mBAAmB,YAAY;AACnC,YAAM,WAAW,MAAM;AAAA,QACrB,oDAAoD,aAAa;AAAA,MACnE;AACA,UAAI,CAAC,SAAS,GAAI;AAElB,YAAM,OAAO,SAAS;AACtB,YAAM,QAAS,MAAM,SAAS,MAAM,QAAQ,CAAC;AAE7C,YAAM,cAAc,MAAM;AAAA,QACxB,CAAC,UACC,MAAM,cAAc,CAAC,MAAM;AAAA,MAC/B;AAEA,UAAI,aAAa;AACf,yBAAiB,OAAO,YAAY,MAAM,EAAE,CAAC;AAC7C,2BAAmB,OAAO,YAAY,mBAAmB,EAAE,CAAC;AAC5D,uBAAe,OAAO,YAAY,SAAS,EAAE,CAAC;AAC9C,4BAAoB,OAAO,YAAY,cAAc,EAAE,CAAC;AAAA,MAC1D;AAAA,IACF;AAEA,qBAAiB;AAAA,EACnB,GAAG,CAAC,eAAe,mBAAmB,CAAC;AAEvC,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,YAAY,QAAS,eAAc,YAAY,OAAO;AAAA,IAC5D;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,aAAS,mBAAmB,OAAmB;AAC7C,UACE,YAAY,WACZ,CAAC,YAAY,QAAQ,SAAS,MAAM,MAAc,GAClD;AACA,+BAAuB,KAAK;AAC5B,yBAAiB,EAAE;AAAA,MACrB;AAAA,IACF;AAEA,QAAI,qBAAqB;AACvB,eAAS,iBAAiB,aAAa,kBAAkB;AAAA,IAC3D;AACA,WAAO,MAAM,SAAS,oBAAoB,aAAa,kBAAkB;AAAA,EAC3E,GAAG,CAAC,mBAAmB,CAAC;AAExB,QAAM,cAAc,YAAY;AAC9B,QAAI,CAAC,qBAAqB,CAAC,cAAe;AAE1C,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,QAAQ,SAAS;AACvB,YAAM,eAAe;AAAA,QACnB;AAAA,QACA,eAAe;AAAA,QACf,MAAM;AAAA,QACN,OAAO,eAAe;AAAA,MACxB;AACA,YAAM,EAAE,IAAI,QAAQ,IAAI,MAAM,YAAY;AAAA,QACxC,WAAW,MAAM,gBAAgB,YAAY;AAAA,QAC7C,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY;AAAA,UACZ;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,QACF;AAAA,QACA,iBAAiB;AAAA,MACnB,CAAC;AAED,uBAAiB,OAAO;AACxB,yBAAmB,iBAAiB;AACpC,2BAAoB,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,IAC9C,SAAS,KAAK;AACZ;AAAA,QACE,wBAAwB,KAAK,EAAE,wCAAwC,uBAAuB,CAAC;AAAA,QAC/F;AAAA,MACF;AAAA,IACF,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,QAAM,aAAa,YAAY;AAC7B,QAAI,CAAC,cAAe;AAEpB,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,cAAc;AAAA,QAClB,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR;AAAA,MACF;AACA,YAAM,YAAY;AAAA,QAChB,WAAW,MACT;AAAA,UACE,sCAAsC,aAAa;AAAA,UACnD,EAAE,QAAQ,OAAO;AAAA,QACnB;AAAA,QACF,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY;AAAA,UACZ;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,QACF;AAAA,QACA,iBAAiB;AAAA,MACnB,CAAC;AAED,uBAAiB,IAAI;AACrB,yBAAmB,IAAI;AACvB,qBAAe,EAAE;AACjB,yBAAmB;AACnB,qBAAe;AAAA,IACjB,SAAS,KAAK;AACZ;AAAA,QACE,wBAAwB,KAAK,EAAE,uCAAuC,sBAAsB,CAAC;AAAA,QAC7F;AAAA,MACF;AAAA,IACF,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,8DACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,eAAe,EAAE,OAAO,KAAK;AAAA,QAC9C,UAAU;AAAA,QACV,aAAa;AAAA,UACX;AAAA,UACA;AAAA,QACF;AAAA,QACA,WAAU;AAAA;AAAA,IACZ;AAAA,IAEA,oBAAC,SAAI,WAAU,YAAW,KAAK,aAC5B,sBACC,gBACE,qBAAC,UAAK,WAAU,0FACd;AAAA,0BAAC,mBAAgB,UAAU,cAAc,OAAO,aAAa,cAAc,MAAM,MAAK,MAAK;AAAA,MAC1F,cAAc;AAAA,OACjB,IACE,OAEJ,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM;AACb,mCAAuB,CAAC,mBAAmB;AAC3C,6BAAiB,EAAE;AAAA,UACrB;AAAA,UACA,WAAU;AAAA,UAET,4BACI,iCAAE;AAAA,gCAAC,mBAAgB,UAAU,gBAAgB,OAAO,aAAa,gBAAgB,MAAM,MAAK,MAAK;AAAA,YAAE,oBAAC,UAAK,WAAU,QAAQ,0BAAgB,MAAK;AAAA,aAAO,IACxJ,EAAE,2CAA2C,SAAS;AAAA;AAAA,MAC5D;AAAA,MAEC,uBACC,qBAAC,SAAI,WAAU,gGACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,iBAAiB,EAAE,OAAO,KAAK;AAAA,YAChD,aAAa;AAAA,cACX;AAAA,cACA;AAAA,YACF;AAAA,YACA,WAAU;AAAA,YACV,WAAS;AAAA;AAAA,QACX;AAAA,QACA,oBAAC,SAAI,WAAU,iCACZ,2BAAiB,WAAW,IAC3B,oBAAC,SAAI,WAAU,6CACZ;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF,IAEA,iBAAiB,IAAI,CAAC,YACpB;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS,MAAM;AACb,mCAAqB,QAAQ,EAAE;AAC/B,qCAAuB,KAAK;AAC5B,+BAAiB,EAAE;AAAA,YACrB;AAAA,YAEA;AAAA,kCAAC,mBAAgB,UAAU,QAAQ,OAAO,aAAa,QAAQ,MAAM,MAAK,MAAK;AAAA,cAC/E,oBAAC,UAAK,WAAU,QAAQ,kBAAQ,MAAK;AAAA,cACpC,QAAQ,OACP,qBAAC,UAAK,WAAU,8BAA6B;AAAA;AAAA,gBACzC,QAAQ;AAAA,gBAAK;AAAA,iBACjB,IACE;AAAA;AAAA;AAAA,UAjBC,QAAQ;AAAA,QAkBf,CACD,GAEL;AAAA,SACF;AAAA,OAEJ,GAEJ;AAAA,IAEA,oBAAC,UAAK,WAAU,0DACb,wBAAc,cAAc,GAC/B;AAAA,IAEC,YACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS;AAAA,QACT,UAAU;AAAA,QACV,cAAY,EAAE,kCAAkC,YAAY;AAAA,QAE5D,8BAAC,UAAO,WAAU,UAAS;AAAA;AAAA,IAC7B,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS;AAAA,QACT,UAAU,cAAc,CAAC;AAAA,QACzB,cAAY,EAAE,mCAAmC,aAAa;AAAA,QAE9D,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,KAEJ;AAEJ;",
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": "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",
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 [assignmentsRes, selfRes] = await Promise.all([
49
- readApiResultOrThrow(
50
- "/api/staff/timesheets/my-projects?pageSize=100",
51
- void 0,
52
- { errorMessage: "", fallback: { items: [] } }
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
- const memberId = selfRes.member?.id ?? null;
63
- setStaffMemberId(memberId);
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
- ) : Promise.resolve({ items: [] }),
70
- memberId ? readApiResultOrThrow(
71
- `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=100`,
72
- void 0,
73
- { errorMessage: "", fallback: { items: [] } }
74
- ) : Promise.resolve({ items: [] })
75
- ]);
76
- const projectItems = Array.isArray(projectsRes.items) ? projectsRes.items : [];
77
- setProjects(projectItems.map((item) => ({
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
- void loadState();
110
- }, [loadState, refreshToken]);
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 (!timer.running || !timer.startedAt) return;
113
- const tick = () => setElapsed(formatElapsed(timer.startedAt));
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
- }, [timer.running, timer.startedAt]);
107
+ }, [timerRunning, timerStartedAt]);
118
108
  const handleStart = React.useCallback(async () => {
119
109
  if (!selectedProjectId || !staffMemberId) return;
120
- if (timer.running) return;
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 loadState();
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, timer.running, notes, hydrated, onSettingsChange, loadState, runMutation, retryLastMutation, t]);
140
+ }, [selectedProjectId, staffMemberId, timerRunning, notes, runMutation, retryLastMutation, hydrated, onSettingsChange, refreshActiveTimer, t]);
151
141
  const handleStop = React.useCallback(async () => {
152
- if (!timer.entryId || !staffMemberId) return;
142
+ if (!activeEntryId || !staffMemberId) return;
153
143
  setActionLoading(true);
154
144
  try {
155
145
  const stopPayload = {
156
- id: timer.entryId,
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/${timer.entryId}/timer-stop`, { method: "POST" }),
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: timer.entryId,
155
+ resourceId: activeEntryId,
166
156
  staffMemberId,
167
157
  action: "timer-stop",
168
158
  retryLastMutation
169
159
  },
170
160
  mutationPayload: stopPayload
171
161
  });
172
- await loadState();
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
- }, [timer.entryId, staffMemberId, loadState, runMutation, retryLastMutation, t]);
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 (timer.running) {
193
- const runningProject = projects.find((p) => p.id === timer.projectId);
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") })