@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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'\nimport { resolveTimerActionError } from '../../../lib/timesheets-ui/timerErrors'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'\n\ntype ProjectOption = { id: string; name: string; code: string | null }\n\ntype TimerState = {\n entryId: string | null\n running: boolean\n startedAt: string | null\n projectId: string | null\n}\n\nconst TIMER_WIDGET_MUTATION_CONTEXT_ID = 'staff-timesheets-time-reporting-widget'\n\ntype TimerWidgetMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n staffMemberId: string\n action: 'timer-create' | 'timer-start' | 'timer-stop'\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction formatElapsed(startedAt: string): string {\n const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime())\n const totalSeconds = Math.floor(elapsed / 1000)\n const hours = Math.floor(totalSeconds / 3600)\n const minutes = Math.floor((totalSeconds % 3600) / 60)\n const seconds = totalSeconds % 60\n return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`\n}\n\nconst TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<TimerWidgetMutationContext>({\n contextId: TIMER_WIDGET_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n\n const [projects, setProjects] = React.useState<ProjectOption[]>([])\n const [selectedProjectId, setSelectedProjectId] = React.useState<string | null>(hydrated.lastProjectId)\n const [notes, setNotes] = React.useState('')\n const [timer, setTimer] = React.useState<TimerState>({ entryId: null, running: false, startedAt: null, projectId: null })\n const [elapsed, setElapsed] = React.useState('00:00:00')\n const [staffMemberId, setStaffMemberId] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [actionLoading, setActionLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const loadState = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n // Assignments and the current user's staff member are independent \u2014 fetch them together.\n const [assignmentsRes, selfRes] = await Promise.all([\n readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/staff/timesheets/my-projects?pageSize=100',\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n ),\n readApiResultOrThrow<{ member?: { id: string } | null }>(\n '/api/staff/team-members/self',\n undefined,\n { errorMessage: '', fallback: { member: null } },\n ),\n ])\n\n const assignmentItems = Array.isArray(assignmentsRes.items) ? assignmentsRes.items : []\n const projectIds = assignmentItems\n .map((item) => String(item.time_project_id ?? item.timeProjectId ?? ''))\n .filter((id) => id.length > 0)\n\n const memberId = selfRes.member?.id ?? null\n setStaffMemberId(memberId)\n\n // Project details depend on the assignment ids and the active timer depends on the staff\n // member id, but the two requests are independent of each other \u2014 fetch them together.\n const [projectsRes, entriesRes] = await Promise.all([\n projectIds.length > 0\n ? readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-projects?ids=${projectIds.join(',')}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n : Promise.resolve<{ items?: Array<Record<string, unknown>> }>({ items: [] }),\n memberId\n ? readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n : Promise.resolve<{ items?: Array<Record<string, unknown>> }>({ items: [] }),\n ])\n\n const projectItems = Array.isArray(projectsRes.items) ? projectsRes.items : []\n setProjects(projectItems.map((item) => ({\n id: String(item.id ?? ''),\n name: String(item.name ?? ''),\n code: typeof item.code === 'string' ? item.code : null,\n })))\n\n if (memberId) {\n const entries = Array.isArray(entriesRes.items) ? entriesRes.items : []\n const running = 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 if (running) {\n setTimer({\n entryId: String(running.id ?? ''),\n running: true,\n startedAt: String(running.started_at ?? running.startedAt ?? ''),\n projectId: String(running.time_project_id ?? running.timeProjectId ?? ''),\n })\n } else {\n setTimer({ entryId: null, running: false, startedAt: null, projectId: null })\n }\n }\n } catch (err) {\n console.error('staff.timesheets.timeReporting.load', err)\n setError(t('staff.timesheets.widgets.timeReporting.error', 'Failed to load timer state'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [onRefreshStateChange, t])\n\n React.useEffect(() => {\n void loadState()\n }, [loadState, refreshToken])\n\n // Tick elapsed time\n React.useEffect(() => {\n if (!timer.running || !timer.startedAt) return\n const tick = () => setElapsed(formatElapsed(timer.startedAt!))\n tick()\n const interval = setInterval(tick, 1000)\n return () => clearInterval(interval)\n }, [timer.running, timer.startedAt])\n\n const handleStart = React.useCallback(async () => {\n if (!selectedProjectId || !staffMemberId) return\n if (timer.running) return\n setActionLoading(true)\n try {\n const today = new Date().toISOString().slice(0, 10)\n // Single atomic create+start request (issue #3311): a partial failure can\n // no longer leave an orphaned, unstarted timer entry, and a rejected start\n // now surfaces here instead of being silently ignored. The call is routed\n // through the mutation guard (issue #3308) so global injection modules can\n // run onBeforeSave/onAfterSave and surface conflicts consistently.\n const startPayload = {\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: notes.trim() || null,\n }\n await runMutation({\n operation: () => startTimerEntry(startPayload),\n context: {\n formId: TIMER_WIDGET_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 onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.start', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer')))\n } finally {\n setActionLoading(false)\n }\n }, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, runMutation, retryLastMutation, t])\n\n const handleStop = React.useCallback(async () => {\n if (!timer.entryId || !staffMemberId) return\n setActionLoading(true)\n try {\n const stopPayload = {\n id: timer.entryId,\n action: 'timer-stop',\n staffMemberId,\n }\n await runMutation({\n operation: () =>\n apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' }),\n context: {\n formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,\n resourceKind: 'staff.timesheets.time_entry',\n resourceId: timer.entryId,\n staffMemberId,\n action: 'timer-stop',\n retryLastMutation,\n },\n mutationPayload: stopPayload,\n })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.stop', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer')))\n } finally {\n setActionLoading(false)\n }\n }, [timer.entryId, staffMemberId, loadState, runMutation, retryLastMutation, t])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-2 text-sm\">\n <p className=\"text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.settings.description', 'No additional settings. Select a project and start tracking from the widget.')}\n </p>\n </div>\n )\n }\n\n if (loading) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">{t('staff.timesheets.widgets.timeReporting.loading', 'Loading...')}</p>\n </div>\n )\n }\n\n if (error) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p role=\"alert\" className=\"text-sm text-destructive\">{error}</p>\n </div>\n )\n }\n\n if (projects.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.noProjects', 'No projects assigned.')}\n </p>\n </div>\n )\n }\n\n // Timer is running\n if (timer.running) {\n const runningProject = projects.find((p) => p.id === timer.projectId)\n return (\n <div className=\"space-y-3\">\n <div className=\"text-sm text-muted-foreground\">\n {runningProject?.name ?? t('staff.timesheets.widgets.timeReporting.unknownProject', 'Unknown project')}\n </div>\n <div className=\"text-center\">\n <p className=\"font-mono text-3xl font-bold tabular-nums\">{elapsed}</p>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.running', 'Timer running')}\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"destructive\"\n className=\"w-full\"\n onClick={handleStop}\n disabled={actionLoading}\n >\n {t('staff.timesheets.widgets.timeReporting.stop', 'Stop Timer')}\n </Button>\n </div>\n )\n }\n\n // Timer not running \u2014 show start form\n return (\n <div className=\"space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-project\">\n {t('staff.timesheets.widgets.timeReporting.project', 'Project')}\n </label>\n <select\n id=\"timer-project\"\n className=\"w-full rounded-md border bg-background px-3 py-2 text-sm\"\n value={selectedProjectId ?? ''}\n onChange={(e) => setSelectedProjectId(e.target.value || null)}\n >\n <option value=\"\">{t('staff.timesheets.widgets.timeReporting.selectProject', 'Select project...')}</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}{project.code ? ` (${project.code})` : ''}\n </option>\n ))}\n </select>\n </div>\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-notes\">\n {t('staff.timesheets.widgets.timeReporting.taskNote', 'Task / Note')}\n </label>\n <Input\n id=\"timer-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={t('staff.timesheets.widgets.timeReporting.notesPlaceholder', 'What are you working on?')}\n />\n </div>\n <div className=\"text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.notRunning', 'Not running')}\n </p>\n </div>\n <Button\n type=\"button\"\n className=\"w-full\"\n onClick={handleStart}\n disabled={actionLoading || !selectedProjectId}\n >\n {t('staff.timesheets.widgets.timeReporting.start', 'Start Timer')}\n </Button>\n </div>\n )\n}\n\nexport default TimeReportingWidget\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useActiveTimesheetTimer } from '../../../lib/timesheets-ui/useActiveTimesheetTimer'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'\nimport { resolveTimerActionError } from '../../../lib/timesheets-ui/timerErrors'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'\n\ntype ProjectOption = { id: string; name: string; code: string | null }\n\nconst TIMER_WIDGET_MUTATION_CONTEXT_ID = 'staff-timesheets-time-reporting-widget'\n\ntype TimerWidgetMutationContext = {\n formId: string\n resourceKind: string\n resourceId: string\n staffMemberId: string\n action: 'timer-create' | 'timer-start' | 'timer-stop'\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction formatElapsed(startedAt: string): string {\n const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime())\n const totalSeconds = Math.floor(elapsed / 1000)\n const hours = Math.floor(totalSeconds / 3600)\n const minutes = Math.floor((totalSeconds % 3600) / 60)\n const seconds = totalSeconds % 60\n return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`\n}\n\nconst TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const { runMutation, retryLastMutation } = useGuardedMutation<TimerWidgetMutationContext>({\n contextId: TIMER_WIDGET_MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n\n const [projects, setProjects] = React.useState<ProjectOption[]>([])\n const [selectedProjectId, setSelectedProjectId] = React.useState<string | null>(hydrated.lastProjectId)\n const [notes, setNotes] = React.useState('')\n const [elapsed, setElapsed] = React.useState('00:00:00')\n const [loading, setLoading] = React.useState(true)\n const [actionLoading, setActionLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const didLoadRef = React.useRef(false)\n const activeTimer = useActiveTimesheetTimer()\n const {\n staffMemberId,\n entryId: activeEntryId,\n running: timerRunning,\n startedAt: timerStartedAt,\n projectId: activeProjectId,\n projectName: activeProjectName,\n isLoading: activeTimerLoading,\n error: activeTimerError,\n refresh: refreshActiveTimer,\n } = activeTimer\n\n const loadState = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n // Load assigned projects\n const assignmentsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/staff/timesheets/my-projects?pageSize=100',\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const assignmentItems = Array.isArray(assignmentsRes.items) ? assignmentsRes.items : []\n const projectIds = assignmentItems\n .map((item) => String(item.time_project_id ?? item.timeProjectId ?? ''))\n .filter((id) => id.length > 0)\n\n if (projectIds.length > 0) {\n const projectsRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-projects?ids=${projectIds.join(',')}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const items = Array.isArray(projectsRes.items) ? projectsRes.items : []\n setProjects(items.map((item) => ({\n id: String(item.id ?? ''),\n name: String(item.name ?? ''),\n code: typeof item.code === 'string' ? item.code : null,\n })))\n } else {\n setProjects([])\n }\n\n } catch (err) {\n console.error('staff.timesheets.timeReporting.load', err)\n setError(t('staff.timesheets.widgets.timeReporting.error', 'Failed to load timer state'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [onRefreshStateChange, t])\n\n React.useEffect(() => {\n if (!didLoadRef.current) {\n didLoadRef.current = true\n void loadState()\n return\n }\n void Promise.allSettled([loadState(), refreshActiveTimer()])\n }, [loadState, refreshActiveTimer, refreshToken])\n\n // Tick elapsed time\n React.useEffect(() => {\n if (!timerRunning || !timerStartedAt) {\n setElapsed('00:00:00')\n return\n }\n const tick = () => setElapsed(formatElapsed(timerStartedAt))\n tick()\n const interval = setInterval(tick, 1000)\n return () => clearInterval(interval)\n }, [timerRunning, timerStartedAt])\n\n const handleStart = React.useCallback(async () => {\n if (!selectedProjectId || !staffMemberId) return\n if (timerRunning) return\n setActionLoading(true)\n try {\n const today = new Date().toISOString().slice(0, 10)\n // Single atomic create+start request (issue #3311): a partial failure can\n // no longer leave an orphaned, unstarted timer entry, and a rejected start\n // now surfaces here instead of being silently ignored. The call is routed\n // through the mutation guard (issue #3308) so global injection modules can\n // run onBeforeSave/onAfterSave and surface conflicts consistently.\n const startPayload = {\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: notes.trim() || null,\n }\n await runMutation({\n operation: () => startTimerEntry(startPayload),\n context: {\n formId: TIMER_WIDGET_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 onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })\n await refreshActiveTimer()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.start', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer')))\n } finally {\n setActionLoading(false)\n }\n }, [selectedProjectId, staffMemberId, timerRunning, notes, runMutation, retryLastMutation, hydrated, onSettingsChange, refreshActiveTimer, t])\n\n const handleStop = React.useCallback(async () => {\n if (!activeEntryId || !staffMemberId) return\n setActionLoading(true)\n try {\n const stopPayload = {\n id: activeEntryId,\n action: 'timer-stop',\n staffMemberId,\n }\n await runMutation({\n operation: () =>\n apiCall(`/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`, { method: 'POST' }),\n context: {\n formId: TIMER_WIDGET_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 await refreshActiveTimer()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.stop', err)\n setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer')))\n } finally {\n setActionLoading(false)\n }\n }, [activeEntryId, staffMemberId, runMutation, retryLastMutation, refreshActiveTimer, t])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-2 text-sm\">\n <p className=\"text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.settings.description', 'No additional settings. Select a project and start tracking from the widget.')}\n </p>\n </div>\n )\n }\n\n if (loading || activeTimerLoading) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">{t('staff.timesheets.widgets.timeReporting.loading', 'Loading...')}</p>\n </div>\n )\n }\n\n if (error || activeTimerError) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error ?? t('staff.timesheets.widgets.timeReporting.error', 'Failed to load timer state')}\n </p>\n </div>\n )\n }\n\n if (projects.length === 0) {\n return (\n <div className=\"flex h-full items-center justify-center py-8\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.noProjects', 'No projects assigned.')}\n </p>\n </div>\n )\n }\n\n // Timer is running\n if (timerRunning) {\n const runningProject = projects.find((p) => p.id === activeProjectId)\n return (\n <div className=\"space-y-3\">\n <div className=\"text-sm text-muted-foreground\">\n {runningProject?.name ?? activeProjectName ?? t('staff.timesheets.widgets.timeReporting.unknownProject', 'Unknown project')}\n </div>\n <div className=\"text-center\">\n <p className=\"font-mono text-3xl font-bold tabular-nums\">{elapsed}</p>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.running', 'Timer running')}\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"destructive\"\n className=\"w-full\"\n onClick={handleStop}\n disabled={actionLoading}\n >\n {t('staff.timesheets.widgets.timeReporting.stop', 'Stop Timer')}\n </Button>\n </div>\n )\n }\n\n // Timer not running \u2014 show start form\n return (\n <div className=\"space-y-3\">\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-project\">\n {t('staff.timesheets.widgets.timeReporting.project', 'Project')}\n </label>\n <select\n id=\"timer-project\"\n className=\"w-full rounded-md border bg-background px-3 py-2 text-sm\"\n value={selectedProjectId ?? ''}\n onChange={(e) => setSelectedProjectId(e.target.value || null)}\n >\n <option value=\"\">{t('staff.timesheets.widgets.timeReporting.selectProject', 'Select project...')}</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}{project.code ? ` (${project.code})` : ''}\n </option>\n ))}\n </select>\n </div>\n <div className=\"space-y-1.5\">\n <label className=\"text-xs font-medium text-muted-foreground\" htmlFor=\"timer-notes\">\n {t('staff.timesheets.widgets.timeReporting.taskNote', 'Task / Note')}\n </label>\n <Input\n id=\"timer-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={t('staff.timesheets.widgets.timeReporting.notesPlaceholder', 'What are you working on?')}\n />\n </div>\n <div className=\"text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('staff.timesheets.widgets.timeReporting.notRunning', 'Not running')}\n </p>\n </div>\n <Button\n type=\"button\"\n className=\"w-full\"\n onClick={handleStart}\n disabled={actionLoading || !selectedProjectId}\n >\n {t('staff.timesheets.widgets.timeReporting.start', 'Start Timer')}\n </Button>\n </div>\n )\n}\n\nexport default TimeReportingWidget\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,27 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
4
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
5
  import { ProjectColorDot } from "../../../lib/timesheets-ui/ProjectColorDot.js";
7
- const STORAGE_KEY = "om:timesheets:activeTimer";
8
- function saveToSession(state) {
9
- try {
10
- if (state) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
11
- else sessionStorage.removeItem(STORAGE_KEY);
12
- } catch {
13
- }
14
- }
15
- function loadFromSession() {
16
- try {
17
- const raw = sessionStorage.getItem(STORAGE_KEY);
18
- if (!raw) return null;
19
- const parsed = JSON.parse(raw);
20
- if (parsed?.startedAt && parsed?.entryId) return parsed;
21
- } catch {
22
- }
23
- return null;
24
- }
6
+ import { useActiveTimesheetTimer } from "../../../lib/timesheets-ui/useActiveTimesheetTimer.js";
25
7
  function formatElapsed(seconds) {
26
8
  const h = Math.floor(seconds / 3600);
27
9
  const m = Math.floor(seconds % 3600 / 60);
@@ -30,61 +12,11 @@ function formatElapsed(seconds) {
30
12
  }
31
13
  function TimerSidebarIndicator() {
32
14
  const t = useT();
33
- const [timer, setTimer] = React.useState(loadFromSession);
15
+ const timer = useActiveTimesheetTimer();
34
16
  const [elapsed, setElapsed] = React.useState(0);
35
17
  const intervalRef = React.useRef(null);
36
- const updateTimer = React.useCallback((next) => {
37
- setTimer(next);
38
- saveToSession(next);
39
- }, []);
40
- const pollTimer = React.useCallback(async () => {
41
- try {
42
- const selfRes = await apiCall("/api/staff/team-members/self");
43
- const memberId = selfRes.result?.member?.id;
44
- if (!memberId) {
45
- updateTimer(null);
46
- return;
47
- }
48
- const res = await apiCall(
49
- `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=50`
50
- );
51
- const items = res.result?.items ?? [];
52
- const active = items.find((e) => e.started_at && !e.ended_at);
53
- if (!active) {
54
- updateTimer(null);
55
- return;
56
- }
57
- const projectId = String(active.time_project_id ?? "");
58
- let projectName = "";
59
- let projectColor = null;
60
- if (projectId) {
61
- const projRes = await apiCall(
62
- `/api/staff/timesheets/time-projects?ids=${projectId}&pageSize=1`
63
- );
64
- const proj = (projRes.result?.items ?? [])[0];
65
- if (proj) {
66
- projectName = String(proj.name ?? "");
67
- projectColor = typeof proj.color === "string" ? proj.color : null;
68
- }
69
- }
70
- updateTimer({
71
- entryId: String(active.id ?? ""),
72
- projectName,
73
- projectColor,
74
- startedAt: String(active.started_at ?? "")
75
- });
76
- } catch {
77
- }
78
- }, [updateTimer]);
79
- React.useEffect(() => {
80
- void pollTimer();
81
- const poll = setInterval(() => {
82
- void pollTimer();
83
- }, 3e4);
84
- return () => clearInterval(poll);
85
- }, [pollTimer]);
86
18
  React.useEffect(() => {
87
- if (!timer) {
19
+ if (!timer.running || !timer.startedAt) {
88
20
  if (intervalRef.current) {
89
21
  clearInterval(intervalRef.current);
90
22
  intervalRef.current = null;
@@ -102,8 +34,8 @@ function TimerSidebarIndicator() {
102
34
  intervalRef.current = null;
103
35
  }
104
36
  };
105
- }, [timer]);
106
- if (!timer) return null;
37
+ }, [timer.running, timer.startedAt]);
38
+ if (!timer.running || !timer.entryId) return null;
107
39
  return /* @__PURE__ */ jsxs(
108
40
  "a",
109
41
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport { ProjectColorDot } from '../../../lib/timesheets-ui/ProjectColorDot'\n\ntype TimerState = {\n entryId: string\n projectName: string\n projectColor: string | null\n startedAt: string\n}\n\nconst STORAGE_KEY = 'om:timesheets:activeTimer'\n\nfunction saveToSession(state: TimerState | null) {\n try {\n if (state) sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state))\n else sessionStorage.removeItem(STORAGE_KEY)\n } catch { /* private browsing */ }\n}\n\nfunction loadFromSession(): TimerState | null {\n try {\n const raw = sessionStorage.getItem(STORAGE_KEY)\n if (!raw) return null\n const parsed = JSON.parse(raw)\n if (parsed?.startedAt && parsed?.entryId) return parsed as TimerState\n } catch { /* corrupt or private browsing */ }\n return null\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 TimerSidebarIndicator() {\n const t = useT()\n // Initialise from sessionStorage so the indicator doesn't flash away on navigation\n const [timer, setTimer] = React.useState<TimerState | null>(loadFromSession)\n const [elapsed, setElapsed] = React.useState(0)\n const intervalRef = React.useRef<ReturnType<typeof setInterval> | null>(null)\n\n // Wrap setTimer to also persist to sessionStorage\n const updateTimer = React.useCallback((next: TimerState | null) => {\n setTimer(next)\n saveToSession(next)\n }, [])\n\n const pollTimer = React.useCallback(async () => {\n try {\n const selfRes = await apiCall<{ member?: { id: string } | null }>('/api/staff/team-members/self')\n const memberId = selfRes.result?.member?.id\n if (!memberId) { updateTimer(null); return }\n\n const res = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=50`,\n )\n const items = (res.result?.items ?? []) as Array<Record<string, unknown>>\n const active = items.find((e) => e.started_at && !e.ended_at)\n\n if (!active) {\n updateTimer(null)\n return\n }\n\n const projectId = String(active.time_project_id ?? '')\n let projectName = ''\n let projectColor: string | null = null\n if (projectId) {\n const projRes = await apiCall<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-projects?ids=${projectId}&pageSize=1`,\n )\n const proj = (projRes.result?.items ?? [])[0]\n if (proj) {\n projectName = String(proj.name ?? '')\n projectColor = typeof proj.color === 'string' ? proj.color : null\n }\n }\n\n updateTimer({\n entryId: String(active.id ?? ''),\n projectName,\n projectColor,\n startedAt: String(active.started_at ?? ''),\n })\n } catch {\n // Silent \u2014 don't crash the sidebar\n }\n }, [updateTimer])\n\n // Poll for active timer on mount + every 30s\n React.useEffect(() => {\n void pollTimer()\n const poll = setInterval(() => { void pollTimer() }, 30000)\n return () => clearInterval(poll)\n }, [pollTimer])\n\n // Tick the elapsed counter locally every second\n React.useEffect(() => {\n if (!timer) {\n if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null }\n setElapsed(0)\n return\n }\n\n const startTime = new Date(timer.startedAt).getTime()\n const calcElapsed = () => Math.max(0, Math.floor((Date.now() - startTime) / 1000))\n setElapsed(calcElapsed())\n\n intervalRef.current = setInterval(() => setElapsed(calcElapsed()), 1000)\n return () => {\n if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null }\n }\n }, [timer])\n\n if (!timer) return null\n\n return (\n <a\n href=\"/backend/staff/timesheets\"\n className=\"flex items-center gap-2 rounded-md px-2 py-1.5 text-xs hover:bg-muted transition-colors cursor-pointer\"\n title={t('staff.timesheets.sidebar.timerRunning', 'Timer running \u2014 click to view')}\n >\n <span className=\"relative flex h-2.5 w-2.5 shrink-0\">\n <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75\" />\n <span className=\"relative inline-flex h-2.5 w-2.5 rounded-full bg-red-500\" />\n </span>\n {timer.projectName ? (\n <span className=\"inline-flex items-center gap-1 truncate\">\n <ProjectColorDot colorKey={timer.projectColor} projectName={timer.projectName} size=\"xs\" />\n <span className=\"truncate\">{timer.projectName}</span>\n </span>\n ) : null}\n <span className=\"ml-auto font-mono tabular-nums shrink-0\">{formatElapsed(elapsed)}</span>\n </a>\n )\n}\n\nconst widget: InjectionWidgetModule = {\n metadata: {\n id: 'staff.injection.timer-sidebar-indicator',\n title: 'Active timer indicator',\n description: 'Shows a pulsing indicator in the sidebar when a timesheet timer is running.',\n features: ['staff.timesheets.manage_own'],\n },\n Widget: TimerSidebarIndicator,\n}\n\nexport default widget\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport { ProjectColorDot } from '../../../lib/timesheets-ui/ProjectColorDot'\nimport { useActiveTimesheetTimer } from '../../../lib/timesheets-ui/useActiveTimesheetTimer'\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 TimerSidebarIndicator() {\n const t = useT()\n const timer = useActiveTimesheetTimer()\n const [elapsed, setElapsed] = React.useState(0)\n const intervalRef = React.useRef<ReturnType<typeof setInterval> | null>(null)\n\n // Tick the elapsed counter locally every second\n React.useEffect(() => {\n if (!timer.running || !timer.startedAt) {\n if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null }\n setElapsed(0)\n return\n }\n\n const startTime = new Date(timer.startedAt).getTime()\n const calcElapsed = () => Math.max(0, Math.floor((Date.now() - startTime) / 1000))\n setElapsed(calcElapsed())\n\n intervalRef.current = setInterval(() => setElapsed(calcElapsed()), 1000)\n return () => {\n if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null }\n }\n }, [timer.running, timer.startedAt])\n\n if (!timer.running || !timer.entryId) return null\n\n return (\n <a\n href=\"/backend/staff/timesheets\"\n className=\"flex items-center gap-2 rounded-md px-2 py-1.5 text-xs hover:bg-muted transition-colors cursor-pointer\"\n title={t('staff.timesheets.sidebar.timerRunning', 'Timer running \u2014 click to view')}\n >\n <span className=\"relative flex h-2.5 w-2.5 shrink-0\">\n <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75\" />\n <span className=\"relative inline-flex h-2.5 w-2.5 rounded-full bg-red-500\" />\n </span>\n {timer.projectName ? (\n <span className=\"inline-flex items-center gap-1 truncate\">\n <ProjectColorDot colorKey={timer.projectColor} projectName={timer.projectName} size=\"xs\" />\n <span className=\"truncate\">{timer.projectName}</span>\n </span>\n ) : null}\n <span className=\"ml-auto font-mono tabular-nums shrink-0\">{formatElapsed(elapsed)}</span>\n </a>\n )\n}\n\nconst widget: InjectionWidgetModule = {\n metadata: {\n id: 'staff.injection.timer-sidebar-indicator',\n title: 'Active timer indicator',\n description: 'Shows a pulsing indicator in the sidebar when a timesheet timer is running.',\n features: ['staff.timesheets.manage_own'],\n },\n Widget: TimerSidebarIndicator,\n}\n\nexport default widget\n"],
5
+ "mappings": ";AA+CM,SACE,KADF;AA7CN,YAAY,WAAW;AACvB,SAAS,YAAY;AAErB,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AAExC,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,wBAAwB;AAC/B,QAAM,IAAI,KAAK;AACf,QAAM,QAAQ,wBAAwB;AACtC,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,CAAC;AAC9C,QAAM,cAAc,MAAM,OAA8C,IAAI;AAG5E,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,MAAM,WAAW,CAAC,MAAM,WAAW;AACtC,UAAI,YAAY,SAAS;AAAE,sBAAc,YAAY,OAAO;AAAG,oBAAY,UAAU;AAAA,MAAK;AAC1F,iBAAW,CAAC;AACZ;AAAA,IACF;AAEA,UAAM,YAAY,IAAI,KAAK,MAAM,SAAS,EAAE,QAAQ;AACpD,UAAM,cAAc,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI,CAAC;AACjF,eAAW,YAAY,CAAC;AAExB,gBAAY,UAAU,YAAY,MAAM,WAAW,YAAY,CAAC,GAAG,GAAI;AACvE,WAAO,MAAM;AACX,UAAI,YAAY,SAAS;AAAE,sBAAc,YAAY,OAAO;AAAG,oBAAY,UAAU;AAAA,MAAK;AAAA,IAC5F;AAAA,EACF,GAAG,CAAC,MAAM,SAAS,MAAM,SAAS,CAAC;AAEnC,MAAI,CAAC,MAAM,WAAW,CAAC,MAAM,QAAS,QAAO;AAE7C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAU;AAAA,MACV,OAAO,EAAE,yCAAyC,oCAA+B;AAAA,MAEjF;AAAA,6BAAC,UAAK,WAAU,sCACd;AAAA,8BAAC,UAAK,WAAU,sFAAqF;AAAA,UACrG,oBAAC,UAAK,WAAU,4DAA2D;AAAA,WAC7E;AAAA,QACC,MAAM,cACL,qBAAC,UAAK,WAAU,2CACd;AAAA,8BAAC,mBAAgB,UAAU,MAAM,cAAc,aAAa,MAAM,aAAa,MAAK,MAAK;AAAA,UACzF,oBAAC,UAAK,WAAU,YAAY,gBAAM,aAAY;AAAA,WAChD,IACE;AAAA,QACJ,oBAAC,UAAK,WAAU,2CAA2C,wBAAc,OAAO,GAAE;AAAA;AAAA;AAAA,EACpF;AAEJ;AAEA,MAAM,SAAgC;AAAA,EACpC,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,6BAA6B;AAAA,EAC1C;AAAA,EACA,QAAQ;AACV;AAEA,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6451.1.5b071f47fc",
3
+ "version": "0.6.6-develop.6453.1.6cbc2d5d47",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -253,16 +253,16 @@
253
253
  "zod": "^4.4.3"
254
254
  },
255
255
  "peerDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6451.1.5b071f47fc",
257
- "@open-mercato/shared": "0.6.6-develop.6451.1.5b071f47fc",
258
- "@open-mercato/ui": "0.6.6-develop.6451.1.5b071f47fc",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6453.1.6cbc2d5d47",
257
+ "@open-mercato/shared": "0.6.6-develop.6453.1.6cbc2d5d47",
258
+ "@open-mercato/ui": "0.6.6-develop.6453.1.6cbc2d5d47",
259
259
  "react": "^19.0.0",
260
260
  "react-dom": "^19.0.0"
261
261
  },
262
262
  "devDependencies": {
263
- "@open-mercato/ai-assistant": "0.6.6-develop.6451.1.5b071f47fc",
264
- "@open-mercato/shared": "0.6.6-develop.6451.1.5b071f47fc",
265
- "@open-mercato/ui": "0.6.6-develop.6451.1.5b071f47fc",
263
+ "@open-mercato/ai-assistant": "0.6.6-develop.6453.1.6cbc2d5d47",
264
+ "@open-mercato/shared": "0.6.6-develop.6453.1.6cbc2d5d47",
265
+ "@open-mercato/ui": "0.6.6-develop.6453.1.6cbc2d5d47",
266
266
  "@testing-library/dom": "^10.4.1",
267
267
  "@testing-library/jest-dom": "^6.9.1",
268
268
  "@testing-library/react": "^16.3.1",
@@ -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'
12
+ import { useActiveTimesheetTimer } from './useActiveTimesheetTimer'
12
13
  import { startTimerEntry } from './startTimer'
13
14
  import { resolveTimerActionError } from './timerErrors'
14
15
 
@@ -58,8 +59,6 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
58
59
  blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
59
60
  })
60
61
 
61
- const [activeEntryId, setActiveEntryId] = useState<string | null>(null)
62
- const [activeProjectId, setActiveProjectId] = useState<string | null>(null)
63
62
  const [elapsedSeconds, setElapsedSeconds] = useState(0)
64
63
  const [description, setDescription] = useState('')
65
64
  const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
@@ -70,10 +69,15 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
70
69
 
71
70
  const dropdownRef = useRef<HTMLDivElement>(null)
72
71
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
72
+ const activeTimer = useActiveTimesheetTimer({ staffMemberId })
73
73
 
74
- const isRunning = activeEntryId !== null
74
+ const activeEntryId = activeTimer.entryId
75
+ const activeProjectId = activeTimer.projectId
76
+ const isRunning = activeTimer.running
75
77
 
76
78
  const activeProject = projects.find((p) => p.id === activeProjectId)
79
+ const activeProjectName = activeProject?.name ?? activeTimer.projectName
80
+ const activeProjectColor = activeProject?.color ?? activeTimer.projectColor
77
81
  const selectedProject = projects.find((p) => p.id === selectedProjectId)
78
82
 
79
83
  const filteredProjects = projects.filter((p) =>
@@ -100,32 +104,13 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
100
104
  }, [])
101
105
 
102
106
  useEffect(() => {
103
- if (!staffMemberId) return
104
-
105
- const checkActiveTimer = async () => {
106
- const response = await apiCall(
107
- `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&running=true&pageSize=50`,
108
- )
109
- if (!response.ok) return
110
-
111
- const data = response.result as Record<string, unknown> | null
112
- const items = (data?.items ?? data?.data ?? []) as Array<Record<string, unknown>>
113
-
114
- const activeEntry = items.find(
115
- (entry: Record<string, unknown>) =>
116
- entry.started_at && !entry.ended_at,
117
- )
118
-
119
- if (activeEntry) {
120
- setActiveEntryId(String(activeEntry.id ?? ''))
121
- setActiveProjectId(String(activeEntry.time_project_id ?? ''))
122
- setDescription(String(activeEntry.notes ?? ''))
123
- startElapsedCounter(String(activeEntry.started_at ?? ''))
124
- }
107
+ if (activeTimer.running && activeTimer.startedAt) {
108
+ startElapsedCounter(activeTimer.startedAt)
109
+ if (activeTimer.notes != null) setDescription(activeTimer.notes)
110
+ return
125
111
  }
126
-
127
- checkActiveTimer()
128
- }, [staffMemberId, startElapsedCounter])
112
+ stopElapsedCounter()
113
+ }, [activeTimer.running, activeTimer.startedAt, activeTimer.notes, startElapsedCounter, stopElapsedCounter])
129
114
 
130
115
  useEffect(() => {
131
116
  return () => {
@@ -162,7 +147,7 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
162
147
  date: today,
163
148
  notes: description || null,
164
149
  }
165
- const { id: entryId } = await runMutation({
150
+ await runMutation({
166
151
  operation: () => startTimerEntry(startPayload),
167
152
  context: {
168
153
  formId: TIMER_MUTATION_CONTEXT_ID,
@@ -175,9 +160,7 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
175
160
  mutationPayload: startPayload,
176
161
  })
177
162
 
178
- setActiveEntryId(entryId)
179
- setActiveProjectId(selectedProjectId)
180
- startElapsedCounter(new Date().toISOString())
163
+ await activeTimer.refresh()
181
164
  } catch (err) {
182
165
  flash(
183
166
  resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),
@@ -215,10 +198,8 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
215
198
  mutationPayload: stopPayload,
216
199
  })
217
200
 
218
- setActiveEntryId(null)
219
- setActiveProjectId(null)
220
201
  setDescription('')
221
- stopElapsedCounter()
202
+ await activeTimer.refresh()
222
203
  onTimerStopped()
223
204
  } catch (err) {
224
205
  flash(
@@ -246,10 +227,10 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
246
227
 
247
228
  <div className="relative" ref={dropdownRef}>
248
229
  {isRunning ? (
249
- activeProject ? (
230
+ activeProjectName ? (
250
231
  <span className="inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted">
251
- <ProjectColorDot colorKey={activeProject.color} projectName={activeProject.name} size="xs" />
252
- {activeProject.name}
232
+ <ProjectColorDot colorKey={activeProjectColor} projectName={activeProjectName} size="xs" />
233
+ {activeProjectName}
253
234
  </span>
254
235
  ) : null
255
236
  ) : (
@@ -0,0 +1,160 @@
1
+ "use client"
2
+
3
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
4
+ import * as React from 'react'
5
+ import { useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
6
+
7
+ export type ActiveTimesheetTimer = {
8
+ staffMemberId: string | null
9
+ entryId: string | null
10
+ running: boolean
11
+ startedAt: string | null
12
+ projectId: string | null
13
+ projectName: string | null
14
+ projectColor: string | null
15
+ notes: string | null
16
+ }
17
+
18
+ type ActiveTimesheetTimerResult = ActiveTimesheetTimer & {
19
+ isLoading: boolean
20
+ isFetching: boolean
21
+ error: Error | null
22
+ refresh: () => Promise<ActiveTimesheetTimer>
23
+ }
24
+
25
+ type ActiveTimesheetTimerOptions = {
26
+ staffMemberId?: string | null
27
+ }
28
+
29
+ const ACTIVE_TIMER_REVALIDATE_MS = 30_000
30
+ const BASE_ACTIVE_TIMER_KEY = ['staff', 'timesheets', 'activeTimer'] as const
31
+
32
+ const EMPTY_ACTIVE_TIMER: ActiveTimesheetTimer = {
33
+ staffMemberId: null,
34
+ entryId: null,
35
+ running: false,
36
+ startedAt: null,
37
+ projectId: null,
38
+ projectName: null,
39
+ projectColor: null,
40
+ notes: null,
41
+ }
42
+
43
+ export const activeTimesheetTimerQueryKey = () => BASE_ACTIVE_TIMER_KEY
44
+
45
+ function getString(value: unknown): string | null {
46
+ return typeof value === 'string' && value.length > 0 ? value : null
47
+ }
48
+
49
+ function getErrorMessage(result: unknown, fallback: string): string {
50
+ if (result && typeof result === 'object') {
51
+ const error = (result as Record<string, unknown>).error
52
+ if (typeof error === 'string' && error.length > 0) return error
53
+ }
54
+ return fallback
55
+ }
56
+
57
+ async function loadCurrentStaffMemberId(staffMemberId?: string | null): Promise<string | null> {
58
+ if (staffMemberId) return staffMemberId
59
+ const selfRes = await apiCall<{ member?: { id?: string | null } | null }>('/api/staff/team-members/self')
60
+ if (!selfRes.ok) {
61
+ throw new Error(getErrorMessage(selfRes.result, 'Failed to load current staff member.'))
62
+ }
63
+ return selfRes.result?.member?.id ?? null
64
+ }
65
+
66
+ async function loadProjectDisplay(projectId: string): Promise<Pick<ActiveTimesheetTimer, 'projectName' | 'projectColor'>> {
67
+ const projectRes = await apiCall<{ items?: Array<Record<string, unknown>> }>(
68
+ `/api/staff/timesheets/time-projects?ids=${encodeURIComponent(projectId)}&pageSize=1`,
69
+ )
70
+ if (!projectRes.ok) {
71
+ return { projectName: null, projectColor: null }
72
+ }
73
+ const project = projectRes.result?.items?.[0]
74
+ return {
75
+ projectName: getString(project?.name),
76
+ projectColor: getString(project?.color),
77
+ }
78
+ }
79
+
80
+ async function fetchActiveTimesheetTimer(staffMemberId?: string | null): Promise<ActiveTimesheetTimer> {
81
+ const memberId = await loadCurrentStaffMemberId(staffMemberId)
82
+ if (!memberId) {
83
+ return EMPTY_ACTIVE_TIMER
84
+ }
85
+
86
+ // Look up the active timer by running state rather than by today's date (issue #3717):
87
+ // a timer started before midnight is still running the next day, and a from=today&to=today
88
+ // lookup would hide it once the date rolls over, leaving an orphaned timer unstoppable.
89
+ const entriesRes = await apiCall<{ items?: Array<Record<string, unknown>> }>(
90
+ `/api/staff/timesheets/time-entries?staffMemberId=${encodeURIComponent(memberId)}&running=true&pageSize=50`,
91
+ )
92
+ if (!entriesRes.ok) {
93
+ throw new Error(getErrorMessage(entriesRes.result, 'Failed to load active timer.'))
94
+ }
95
+
96
+ const entries = Array.isArray(entriesRes.result?.items) ? entriesRes.result.items : []
97
+ const active = entries.find((entry) => {
98
+ const startedAt = entry.started_at ?? entry.startedAt
99
+ const endedAt = entry.ended_at ?? entry.endedAt
100
+ return startedAt != null && endedAt == null
101
+ })
102
+
103
+ if (!active) {
104
+ return { ...EMPTY_ACTIVE_TIMER, staffMemberId: memberId }
105
+ }
106
+
107
+ const projectId = getString(active.time_project_id ?? active.timeProjectId)
108
+ const projectDisplay = projectId
109
+ ? await loadProjectDisplay(projectId)
110
+ : { projectName: null, projectColor: null }
111
+
112
+ return {
113
+ staffMemberId: memberId,
114
+ entryId: getString(active.id),
115
+ running: true,
116
+ startedAt: getString(active.started_at ?? active.startedAt),
117
+ projectId,
118
+ projectName: projectDisplay.projectName,
119
+ projectColor: projectDisplay.projectColor,
120
+ notes: getString(active.notes),
121
+ }
122
+ }
123
+
124
+ export const activeTimesheetTimerQueryOptions = (staffMemberId?: string | null) => ({
125
+ queryKey: activeTimesheetTimerQueryKey(),
126
+ staleTime: ACTIVE_TIMER_REVALIDATE_MS,
127
+ gcTime: ACTIVE_TIMER_REVALIDATE_MS,
128
+ refetchInterval: ACTIVE_TIMER_REVALIDATE_MS,
129
+ queryFn: () => fetchActiveTimesheetTimer(staffMemberId),
130
+ })
131
+
132
+ export async function refreshActiveTimesheetTimer(
133
+ queryClient: QueryClient,
134
+ staffMemberId?: string | null,
135
+ ): Promise<ActiveTimesheetTimer> {
136
+ return queryClient.fetchQuery({
137
+ ...activeTimesheetTimerQueryOptions(staffMemberId),
138
+ staleTime: 0,
139
+ })
140
+ }
141
+
142
+ export function useActiveTimesheetTimer(
143
+ options: ActiveTimesheetTimerOptions = {},
144
+ ): ActiveTimesheetTimerResult {
145
+ const queryClient = useQueryClient()
146
+ const query = useQuery(activeTimesheetTimerQueryOptions(options.staffMemberId))
147
+ const data = query.data ?? EMPTY_ACTIVE_TIMER
148
+ const refresh = React.useCallback(
149
+ () => refreshActiveTimesheetTimer(queryClient, options.staffMemberId),
150
+ [queryClient, options.staffMemberId],
151
+ )
152
+
153
+ return {
154
+ ...data,
155
+ isLoading: query.isLoading,
156
+ isFetching: query.isFetching,
157
+ error: query.error instanceof Error ? query.error : null,
158
+ refresh,
159
+ }
160
+ }