@open-mercato/core 0.6.6-develop.6153.1.b4c555b820 → 0.6.6-develop.6156.1.5ac693a69a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js +32 -0
  3. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
  4. package/dist/modules/messages/api/[id]/archive/route.js +63 -0
  5. package/dist/modules/messages/api/[id]/archive/route.js.map +2 -2
  6. package/dist/modules/messages/api/[id]/attachments/route.js +63 -0
  7. package/dist/modules/messages/api/[id]/attachments/route.js.map +2 -2
  8. package/dist/modules/messages/api/[id]/conversation/archive/route.js +32 -0
  9. package/dist/modules/messages/api/[id]/conversation/archive/route.js.map +2 -2
  10. package/dist/modules/messages/api/[id]/conversation/read/route.js +32 -0
  11. package/dist/modules/messages/api/[id]/conversation/read/route.js.map +2 -2
  12. package/dist/modules/messages/api/[id]/conversation/route.js +32 -0
  13. package/dist/modules/messages/api/[id]/conversation/route.js.map +2 -2
  14. package/dist/modules/messages/api/[id]/forward/route.js +32 -0
  15. package/dist/modules/messages/api/[id]/forward/route.js.map +2 -2
  16. package/dist/modules/messages/api/[id]/read/route.js +63 -0
  17. package/dist/modules/messages/api/[id]/read/route.js.map +2 -2
  18. package/dist/modules/messages/api/[id]/reply/route.js +32 -0
  19. package/dist/modules/messages/api/[id]/reply/route.js.map +2 -2
  20. package/dist/modules/messages/api/[id]/route.js +63 -0
  21. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  22. package/dist/modules/messages/api/guards.js +31 -0
  23. package/dist/modules/messages/api/guards.js.map +7 -0
  24. package/dist/modules/messages/api/route.js +32 -0
  25. package/dist/modules/messages/api/route.js.map +2 -2
  26. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +107 -0
  27. package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +7 -0
  28. package/dist/modules/staff/commands/timesheets-entries.js +142 -1
  29. package/dist/modules/staff/commands/timesheets-entries.js.map +3 -3
  30. package/dist/modules/staff/data/validators.js +8 -0
  31. package/dist/modules/staff/data/validators.js.map +2 -2
  32. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +8 -22
  33. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  34. package/dist/modules/staff/lib/timesheets-ui/startTimer.js +22 -0
  35. package/dist/modules/staff/lib/timesheets-ui/startTimer.js.map +7 -0
  36. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +6 -16
  37. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
  38. package/package.json +7 -7
  39. package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +34 -0
  40. package/src/modules/messages/api/[id]/archive/route.ts +63 -0
  41. package/src/modules/messages/api/[id]/attachments/route.ts +63 -0
  42. package/src/modules/messages/api/[id]/conversation/archive/route.ts +33 -0
  43. package/src/modules/messages/api/[id]/conversation/read/route.ts +33 -0
  44. package/src/modules/messages/api/[id]/conversation/route.ts +33 -0
  45. package/src/modules/messages/api/[id]/forward/route.ts +33 -0
  46. package/src/modules/messages/api/[id]/read/route.ts +63 -0
  47. package/src/modules/messages/api/[id]/reply/route.ts +33 -0
  48. package/src/modules/messages/api/[id]/route.ts +65 -0
  49. package/src/modules/messages/api/guards.ts +59 -0
  50. package/src/modules/messages/api/route.ts +33 -0
  51. package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +110 -0
  52. package/src/modules/staff/commands/timesheets-entries.ts +166 -1
  53. package/src/modules/staff/data/validators.ts +9 -0
  54. package/src/modules/staff/i18n/de.json +1 -0
  55. package/src/modules/staff/i18n/en.json +1 -0
  56. package/src/modules/staff/i18n/es.json +1 -0
  57. package/src/modules/staff/i18n/pl.json +1 -0
  58. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +9 -25
  59. package/src/modules/staff/lib/timesheets-ui/startTimer.ts +40 -0
  60. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +9 -18
@@ -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 { ProjectColorDot } from './ProjectColorDot'\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\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\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 now = Date.now()\n const initialElapsed = Math.max(0, Math.floor((now - startTime) / 1000))\n setElapsedSeconds(initialElapsed)\n\n if (intervalRef.current) clearInterval(intervalRef.current)\n intervalRef.current = setInterval(() => {\n setElapsedSeconds((prev) => prev + 1)\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 today = getToday()\n const checkActiveTimer = async () => {\n const response = await apiCall(\n `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&from=${today}&to=${today}&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 createResponse = await apiCallOrThrow(\n '/api/staff/timesheets/time-entries',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n durationMinutes: 0,\n source: 'timer',\n notes: description || null,\n }),\n },\n )\n\n const created = createResponse.result as Record<string, unknown> | null\n const entryId = created?.id as string | undefined\n\n await apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${entryId}/timer-start`,\n { method: 'POST' },\n )\n\n setActiveEntryId(entryId ?? null)\n setActiveProjectId(selectedProjectId)\n startElapsedCounter(new Date().toISOString())\n } catch {\n flash(\n 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 await apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,\n { method: 'POST' },\n )\n\n setActiveEntryId(null)\n setActiveProjectId(null)\n setDescription('')\n stopElapsedCounter()\n onTimerStopped()\n } catch {\n flash(\n 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=\"outline\"\n size=\"default\"\n onClick={handleStop}\n disabled={isStopping}\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\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=\"outline\"\n size=\"default\"\n onClick={handleStart}\n disabled={isStarting || !selectedProjectId}\n className=\"bg-primary text-primary-foreground hover:bg-primary/90\"\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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",
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 { ProjectColorDot } from './ProjectColorDot'\nimport { startTimerEntry } from './startTimer'\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\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\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 now = Date.now()\n const initialElapsed = Math.max(0, Math.floor((now - startTime) / 1000))\n setElapsedSeconds(initialElapsed)\n\n if (intervalRef.current) clearInterval(intervalRef.current)\n intervalRef.current = setInterval(() => {\n setElapsedSeconds((prev) => prev + 1)\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 today = getToday()\n const checkActiveTimer = async () => {\n const response = await apiCall(\n `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&from=${today}&to=${today}&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 { id: entryId } = await startTimerEntry({\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: description || null,\n })\n\n setActiveEntryId(entryId)\n setActiveProjectId(selectedProjectId)\n startElapsedCounter(new Date().toISOString())\n } catch {\n flash(\n 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 await apiCallOrThrow(\n `/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,\n { method: 'POST' },\n )\n\n setActiveEntryId(null)\n setActiveProjectId(null)\n setDescription('')\n stopElapsedCounter()\n onTimerStopped()\n } catch {\n flash(\n 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=\"outline\"\n size=\"default\"\n onClick={handleStop}\n disabled={isStopping}\n className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\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=\"outline\"\n size=\"default\"\n onClick={handleStart}\n disabled={isStarting || !selectedProjectId}\n className=\"bg-primary text-primary-foreground hover:bg-primary/90\"\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,22 @@
1
+ import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
2
+ async function startTimerEntry(input) {
3
+ const response = await apiCallOrThrow(
4
+ "/api/staff/timesheets/time-entries/start-timer",
5
+ {
6
+ method: "POST",
7
+ headers: { "Content-Type": "application/json" },
8
+ body: JSON.stringify({
9
+ staffMemberId: input.staffMemberId,
10
+ timeProjectId: input.timeProjectId,
11
+ date: input.date,
12
+ notes: input.notes ?? null
13
+ })
14
+ }
15
+ );
16
+ const result = response.result;
17
+ return { id: result?.id ?? null };
18
+ }
19
+ export {
20
+ startTimerEntry
21
+ };
22
+ //# sourceMappingURL=startTimer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/timesheets-ui/startTimer.ts"],
4
+ "sourcesContent": ["import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nexport type StartTimerEntryInput = {\n staffMemberId: string\n timeProjectId: string\n date: string\n notes?: string | null\n}\n\nexport type StartedTimerEntry = {\n id: string | null\n}\n\n/**\n * Starts a timesheet timer through the single atomic server endpoint\n * (`/api/staff/timesheets/time-entries/start-timer`), which creates the\n * timer-sourced entry AND starts it in one transaction. This replaces the\n * legacy create-then-start two-request sequence so a partial failure (a failed\n * second request, or the browser navigating between the two calls) can no\n * longer leave an orphaned, never-started timer entry (issue #3311). Both the\n * TimerBar and the dashboard time-reporting widget call this helper so they\n * share error handling and refresh semantics.\n */\nexport async function startTimerEntry(input: StartTimerEntryInput): Promise<StartedTimerEntry> {\n const response = await apiCallOrThrow<Record<string, unknown>>(\n '/api/staff/timesheets/time-entries/start-timer',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n staffMemberId: input.staffMemberId,\n timeProjectId: input.timeProjectId,\n date: input.date,\n notes: input.notes ?? null,\n }),\n },\n )\n const result = response.result as Record<string, unknown> | null\n return { id: (result?.id as string | undefined) ?? null }\n}\n"],
5
+ "mappings": "AAAA,SAAS,sBAAsB;AAuB/B,eAAsB,gBAAgB,OAAyD;AAC7F,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU;AAAA,QACnB,eAAe,MAAM;AAAA,QACrB,eAAe,MAAM;AAAA,QACrB,MAAM,MAAM;AAAA,QACZ,OAAO,MAAM,SAAS;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,EACF;AACA,QAAM,SAAS,SAAS;AACxB,SAAO,EAAE,IAAK,QAAQ,MAA6B,KAAK;AAC1D;",
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 { startTimerEntry } from "../../../lib/timesheets-ui/startTimer.js";
8
9
  import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
9
10
  function formatElapsed(startedAt) {
10
11
  const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime());
@@ -114,23 +115,12 @@ const TimeReportingWidget = ({
114
115
  setActionLoading(true);
115
116
  try {
116
117
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
117
- const createRes = await apiCall("/api/staff/timesheets/time-entries", {
118
- method: "POST",
119
- headers: { "Content-Type": "application/json" },
120
- body: JSON.stringify({
121
- staffMemberId,
122
- date: today,
123
- timeProjectId: selectedProjectId,
124
- durationMinutes: 0,
125
- notes: notes.trim() || null,
126
- source: "timer"
127
- })
118
+ await startTimerEntry({
119
+ staffMemberId,
120
+ timeProjectId: selectedProjectId,
121
+ date: today,
122
+ notes: notes.trim() || null
128
123
  });
129
- if (!createRes.ok) throw new Error("Failed to create entry");
130
- const body = createRes.result;
131
- const entryId = String(body?.id ?? body?.item?.id ?? "");
132
- if (!entryId) throw new Error("Failed to extract entry ID");
133
- await apiCall(`/api/staff/timesheets/time-entries/${entryId}/timer-start`, { method: "POST" });
134
124
  onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId });
135
125
  await loadState();
136
126
  } catch (err) {
@@ -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 { 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\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 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 // 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 // Check for active timer \u2014 look for today's entries with startedAt set and endedAt null\n const selfRes = await readApiResultOrThrow<{ member?: { id: string } | null }>(\n '/api/staff/team-members/self',\n undefined,\n { errorMessage: '', fallback: { member: null } },\n )\n const memberId = selfRes.member?.id ?? null\n setStaffMemberId(memberId)\n if (memberId) {\n const today = new Date().toISOString().slice(0, 10)\n const entriesRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const entries = Array.isArray(entriesRes.items) ? entriesRes.items : []\n const running = entries.find((e) => {\n const startedAt = e.started_at ?? e.startedAt\n const endedAt = e.ended_at ?? e.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 // Create entry + start timer\n const createRes = await apiCall<Record<string, unknown>>('/api/staff/timesheets/time-entries', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n staffMemberId,\n date: today,\n timeProjectId: selectedProjectId,\n durationMinutes: 0,\n notes: notes.trim() || null,\n source: 'timer',\n }),\n })\n if (!createRes.ok) throw new Error('Failed to create entry')\n const body = createRes.result as Record<string, unknown> | null\n const entryId = String(body?.id ?? (body?.item as Record<string, unknown> | undefined)?.id ?? '')\n if (!entryId) throw new Error('Failed to extract entry ID')\n\n await apiCall(`/api/staff/timesheets/time-entries/${entryId}/timer-start`, { method: 'POST' })\n\n onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.start', err)\n setError(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, t])\n\n const handleStop = React.useCallback(async () => {\n if (!timer.entryId) return\n setActionLoading(true)\n try {\n await apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.stop', err)\n setError(t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer'))\n } finally {\n setActionLoading(false)\n }\n }, [timer.entryId, loadState, 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 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": ";AA2LQ,cAyCA,YAzCA;AAzLR,YAAY,WAAW;AAEvB,SAAS,SAAS,4BAA4B;AAC9C,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,kBAAkB,uBAAmD;AAW9E,SAAS,cAAc,WAA2B;AAChD,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,QAAQ,CAAC;AACtE,QAAM,eAAe,KAAK,MAAM,UAAU,GAAI;AAC9C,QAAM,QAAQ,KAAK,MAAM,eAAe,IAAI;AAC5C,QAAM,UAAU,KAAK,MAAO,eAAe,OAAQ,EAAE;AACrD,QAAM,UAAU,eAAe;AAC/B,SAAO,GAAG,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC;AAClH;AAEA,MAAM,sBAAsF,CAAC;AAAA,EAC3F;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,IAAI,KAAK;AACf,QAAM,WAAW,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,GAAG,CAAC,QAAQ,CAAC;AAE1E,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAA0B,CAAC,CAAC;AAClE,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAwB,SAAS,aAAa;AACtG,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAqB,EAAE,SAAS,MAAM,SAAS,OAAO,WAAW,MAAM,WAAW,KAAK,CAAC;AACxH,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,UAAU;AACvD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAwB,IAAI;AAC5E,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAS,KAAK;AAC9D,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAE5D,QAAM,YAAY,MAAM,YAAY,YAAY;AAC9C,2BAAuB,IAAI;AAC3B,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AAEF,YAAM,iBAAiB,MAAM;AAAA,QAC3B;AAAA,QACA;AAAA,QACA,EAAE,cAAc,IAAI,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,MAC9C;AACA,YAAM,kBAAkB,MAAM,QAAQ,eAAe,KAAK,IAAI,eAAe,QAAQ,CAAC;AACtF,YAAM,aAAa,gBAChB,IAAI,CAAC,SAAS,OAAO,KAAK,mBAAmB,KAAK,iBAAiB,EAAE,CAAC,EACtE,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;AAE/B,UAAI,WAAW,SAAS,GAAG;AACzB,cAAM,cAAc,MAAM;AAAA,UACxB,2CAA2C,WAAW,KAAK,GAAG,CAAC;AAAA,UAC/D;AAAA,UACA,EAAE,cAAc,IAAI,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,QAC9C;AACA,cAAM,QAAQ,MAAM,QAAQ,YAAY,KAAK,IAAI,YAAY,QAAQ,CAAC;AACtE,oBAAY,MAAM,IAAI,CAAC,UAAU;AAAA,UAC/B,IAAI,OAAO,KAAK,MAAM,EAAE;AAAA,UACxB,MAAM,OAAO,KAAK,QAAQ,EAAE;AAAA,UAC5B,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAAA,QACpD,EAAE,CAAC;AAAA,MACL,OAAO;AACL,oBAAY,CAAC,CAAC;AAAA,MAChB;AAGA,YAAM,UAAU,MAAM;AAAA,QACpB;AAAA,QACA;AAAA,QACA,EAAE,cAAc,IAAI,UAAU,EAAE,QAAQ,KAAK,EAAE;AAAA,MACjD;AACA,YAAM,WAAW,QAAQ,QAAQ,MAAM;AACvC,uBAAiB,QAAQ;AACzB,UAAI,UAAU;AACZ,cAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAClD,cAAM,aAAa,MAAM;AAAA,UACvB,oDAAoD,QAAQ,SAAS,KAAK,OAAO,KAAK;AAAA,UACtF;AAAA,UACA,EAAE,cAAc,IAAI,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,QAC9C;AACA,cAAM,UAAU,MAAM,QAAQ,WAAW,KAAK,IAAI,WAAW,QAAQ,CAAC;AACtE,cAAM,UAAU,QAAQ,KAAK,CAAC,MAAM;AAClC,gBAAM,YAAY,EAAE,cAAc,EAAE;AACpC,gBAAM,UAAU,EAAE,YAAY,EAAE;AAChC,iBAAO,aAAa,QAAQ,WAAW;AAAA,QACzC,CAAC;AACD,YAAI,SAAS;AACX,mBAAS;AAAA,YACP,SAAS,OAAO,QAAQ,MAAM,EAAE;AAAA,YAChC,SAAS;AAAA,YACT,WAAW,OAAO,QAAQ,cAAc,QAAQ,aAAa,EAAE;AAAA,YAC/D,WAAW,OAAO,QAAQ,mBAAmB,QAAQ,iBAAiB,EAAE;AAAA,UAC1E,CAAC;AAAA,QACH,OAAO;AACL,mBAAS,EAAE,SAAS,MAAM,SAAS,OAAO,WAAW,MAAM,WAAW,KAAK,CAAC;AAAA,QAC9E;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC,GAAG;AACxD,eAAS,EAAE,gDAAgD,4BAA4B,CAAC;AAAA,IAC1F,UAAE;AACA,iBAAW,KAAK;AAChB,6BAAuB,KAAK;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,sBAAsB,CAAC,CAAC;AAE5B,QAAM,UAAU,MAAM;AACpB,SAAK,UAAU;AAAA,EACjB,GAAG,CAAC,WAAW,YAAY,CAAC;AAG5B,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,MAAM,WAAW,CAAC,MAAM,UAAW;AACxC,UAAM,OAAO,MAAM,WAAW,cAAc,MAAM,SAAU,CAAC;AAC7D,SAAK;AACL,UAAM,WAAW,YAAY,MAAM,GAAI;AACvC,WAAO,MAAM,cAAc,QAAQ;AAAA,EACrC,GAAG,CAAC,MAAM,SAAS,MAAM,SAAS,CAAC;AAEnC,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,QAAI,CAAC,qBAAqB,CAAC,cAAe;AAC1C,QAAI,MAAM,QAAS;AACnB,qBAAiB,IAAI;AACrB,QAAI;AACF,YAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAElD,YAAM,YAAY,MAAM,QAAiC,sCAAsC;AAAA,QAC7F,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB;AAAA,UACA,MAAM;AAAA,UACN,eAAe;AAAA,UACf,iBAAiB;AAAA,UACjB,OAAO,MAAM,KAAK,KAAK;AAAA,UACvB,QAAQ;AAAA,QACV,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,UAAU,GAAI,OAAM,IAAI,MAAM,wBAAwB;AAC3D,YAAM,OAAO,UAAU;AACvB,YAAM,UAAU,OAAO,MAAM,MAAO,MAAM,MAA8C,MAAM,EAAE;AAChG,UAAI,CAAC,QAAS,OAAM,IAAI,MAAM,4BAA4B;AAE1D,YAAM,QAAQ,sCAAsC,OAAO,gBAAgB,EAAE,QAAQ,OAAO,CAAC;AAE7F,uBAAiB,EAAE,GAAG,UAAU,eAAe,kBAAkB,CAAC;AAClE,YAAM,UAAU;AAAA,IAClB,SAAS,KAAK;AACZ,cAAQ,MAAM,wCAAwC,GAAG;AACzD,eAAS,EAAE,qDAAqD,uBAAuB,CAAC;AAAA,IAC1F,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,mBAAmB,eAAe,MAAM,SAAS,OAAO,UAAU,kBAAkB,WAAW,CAAC,CAAC;AAErG,QAAM,aAAa,MAAM,YAAY,YAAY;AAC/C,QAAI,CAAC,MAAM,QAAS;AACpB,qBAAiB,IAAI;AACrB,QAAI;AACF,YAAM,QAAQ,sCAAsC,MAAM,OAAO,eAAe,EAAE,QAAQ,OAAO,CAAC;AAClG,YAAM,UAAU;AAAA,IAClB,SAAS,KAAK;AACZ,cAAQ,MAAM,uCAAuC,GAAG;AACxD,eAAS,EAAE,oDAAoD,sBAAsB,CAAC;AAAA,IACxF,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,MAAM,SAAS,WAAW,CAAC,CAAC;AAEhC,MAAI,SAAS,YAAY;AACvB,WACE,oBAAC,SAAI,WAAU,qBACb,8BAAC,OAAE,WAAU,yBACV,YAAE,+DAA+D,8EAA8E,GAClJ,GACF;AAAA,EAEJ;AAEA,MAAI,SAAS;AACX,WACE,oBAAC,SAAI,WAAU,gDACb,8BAAC,OAAE,WAAU,iCAAiC,YAAE,kDAAkD,YAAY,GAAE,GAClH;AAAA,EAEJ;AAEA,MAAI,OAAO;AACT,WACE,oBAAC,SAAI,WAAU,gDACb,8BAAC,OAAE,WAAU,4BAA4B,iBAAM,GACjD;AAAA,EAEJ;AAEA,MAAI,SAAS,WAAW,GAAG;AACzB,WACE,oBAAC,SAAI,WAAU,gDACb,8BAAC,OAAE,WAAU,iCACV,YAAE,qDAAqD,uBAAuB,GACjF,GACF;AAAA,EAEJ;AAGA,MAAI,MAAM,SAAS;AACjB,UAAM,iBAAiB,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,SAAS;AACpE,WACE,qBAAC,SAAI,WAAU,aACb;AAAA,0BAAC,SAAI,WAAU,iCACZ,0BAAgB,QAAQ,EAAE,yDAAyD,iBAAiB,GACvG;AAAA,MACA,qBAAC,SAAI,WAAU,eACb;AAAA,4BAAC,OAAE,WAAU,6CAA6C,mBAAQ;AAAA,QAClE,oBAAC,OAAE,WAAU,sCACV,YAAE,kDAAkD,eAAe,GACtE;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,WAAU;AAAA,UACV,SAAS;AAAA,UACT,UAAU;AAAA,UAET,YAAE,+CAA+C,YAAY;AAAA;AAAA,MAChE;AAAA,OACF;AAAA,EAEJ;AAGA,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,eACb;AAAA,0BAAC,WAAM,WAAU,6CAA4C,SAAQ,iBAClE,YAAE,kDAAkD,SAAS,GAChE;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,WAAU;AAAA,UACV,OAAO,qBAAqB;AAAA,UAC5B,UAAU,CAAC,MAAM,qBAAqB,EAAE,OAAO,SAAS,IAAI;AAAA,UAE5D;AAAA,gCAAC,YAAO,OAAM,IAAI,YAAE,wDAAwD,mBAAmB,GAAE;AAAA,YAChG,SAAS,IAAI,CAAC,YACb,qBAAC,YAAwB,OAAO,QAAQ,IACrC;AAAA,sBAAQ;AAAA,cAAM,QAAQ,OAAO,KAAK,QAAQ,IAAI,MAAM;AAAA,iBAD1C,QAAQ,EAErB,CACD;AAAA;AAAA;AAAA,MACH;AAAA,OACF;AAAA,IACA,qBAAC,SAAI,WAAU,eACb;AAAA,0BAAC,WAAM,WAAU,6CAA4C,SAAQ,eAClE,YAAE,mDAAmD,aAAa,GACrE;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,aAAa,EAAE,2DAA2D,0BAA0B;AAAA;AAAA,MACtG;AAAA,OACF;AAAA,IACA,oBAAC,SAAI,WAAU,eACb,8BAAC,OAAE,WAAU,iCACV,YAAE,qDAAqD,aAAa,GACvE,GACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS;AAAA,QACT,UAAU,iBAAiB,CAAC;AAAA,QAE3B,YAAE,gDAAgD,aAAa;AAAA;AAAA,IAClE;AAAA,KACF;AAEJ;AAEA,IAAO,wBAAQ;",
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 { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'\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\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 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 // 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 // Check for active timer \u2014 look for today's entries with startedAt set and endedAt null\n const selfRes = await readApiResultOrThrow<{ member?: { id: string } | null }>(\n '/api/staff/team-members/self',\n undefined,\n { errorMessage: '', fallback: { member: null } },\n )\n const memberId = selfRes.member?.id ?? null\n setStaffMemberId(memberId)\n if (memberId) {\n const today = new Date().toISOString().slice(0, 10)\n const entriesRes = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=100`,\n undefined,\n { errorMessage: '', fallback: { items: [] } },\n )\n const entries = Array.isArray(entriesRes.items) ? entriesRes.items : []\n const running = entries.find((e) => {\n const startedAt = e.started_at ?? e.startedAt\n const endedAt = e.ended_at ?? e.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.\n await startTimerEntry({\n staffMemberId,\n timeProjectId: selectedProjectId,\n date: today,\n notes: notes.trim() || null,\n })\n\n onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.start', err)\n setError(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, t])\n\n const handleStop = React.useCallback(async () => {\n if (!timer.entryId) return\n setActionLoading(true)\n try {\n await apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' })\n await loadState()\n } catch (err) {\n console.error('staff.timesheets.timeReporting.stop', err)\n setError(t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer'))\n } finally {\n setActionLoading(false)\n }\n }, [timer.entryId, loadState, 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 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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",
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.6153.1.b4c555b820",
3
+ "version": "0.6.6-develop.6156.1.5ac693a69a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6153.1.b4c555b820",
250
- "@open-mercato/shared": "0.6.6-develop.6153.1.b4c555b820",
251
- "@open-mercato/ui": "0.6.6-develop.6153.1.b4c555b820",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6156.1.5ac693a69a",
250
+ "@open-mercato/shared": "0.6.6-develop.6156.1.5ac693a69a",
251
+ "@open-mercato/ui": "0.6.6-develop.6156.1.5ac693a69a",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6153.1.b4c555b820",
257
- "@open-mercato/shared": "0.6.6-develop.6153.1.b4c555b820",
258
- "@open-mercato/ui": "0.6.6-develop.6153.1.b4c555b820",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6156.1.5ac693a69a",
257
+ "@open-mercato/shared": "0.6.6-develop.6156.1.5ac693a69a",
258
+ "@open-mercato/ui": "0.6.6-develop.6156.1.5ac693a69a",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -2,6 +2,7 @@ import { z } from 'zod'
2
2
  import type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'
3
3
  import { attachOperationMetadataHeader, type OperationLogEntryLike } from '../../../../lib/operationMetadata'
4
4
  import { resolveMessageContext } from '../../../../lib/routeHelpers'
5
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../../guards'
5
6
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
6
7
  import { actionResultResponseSchema } from '../../../openapi'
7
8
 
@@ -20,6 +21,29 @@ export async function POST(
20
21
  const body = (typeof rawBody === 'object' && rawBody && !Array.isArray(rawBody)
21
22
  ? rawBody
22
23
  : {}) as Record<string, unknown>
24
+
25
+ const guardResult = await runMessageMutationGuards(
26
+ ctx.container,
27
+ {
28
+ tenantId: scope.tenantId,
29
+ organizationId: scope.organizationId,
30
+ userId: scope.userId,
31
+ resourceKind: 'messages.message',
32
+ resourceId: params.id,
33
+ operation: 'update',
34
+ requestMethod: req.method,
35
+ requestHeaders: req.headers,
36
+ mutationPayload: body,
37
+ },
38
+ resolveUserFeatures(ctx.auth),
39
+ )
40
+ if (!guardResult.ok) {
41
+ return Response.json(
42
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
43
+ { status: guardResult.errorStatus ?? 422 },
44
+ )
45
+ }
46
+
23
47
  try {
24
48
  const commandResult = await commandBus.execute('messages.actions.execute', {
25
49
  input: {
@@ -55,6 +79,16 @@ export async function POST(
55
79
  resourceKind: 'messages.message',
56
80
  resourceId: params.id,
57
81
  })
82
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
83
+ tenantId: scope.tenantId,
84
+ organizationId: scope.organizationId,
85
+ userId: scope.userId,
86
+ resourceKind: 'messages.message',
87
+ resourceId: params.id,
88
+ operation: 'update',
89
+ requestMethod: req.method,
90
+ requestHeaders: req.headers,
91
+ })
58
92
  return response
59
93
  } catch (error) {
60
94
  if (error instanceof Error) {
@@ -5,6 +5,7 @@ import { Message, MessageRecipient } from '../../../data/entities'
5
5
  import { attachOperationMetadataHeader } from '../../../lib/operationMetadata'
6
6
  import { hasOrganizationAccess, resolveMessageContext } from '../../../lib/routeHelpers'
7
7
  import type { MessageScope } from '../../../lib/routeHelpers'
8
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../guards'
8
9
  import { errorResponseSchema, okResponseSchema } from '../../openapi'
9
10
 
10
11
  export const metadata = {
@@ -56,6 +57,27 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
56
57
  if ('response' in context) return context.response
57
58
  const { ctx, scope } = context
58
59
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
60
+ const guardResult = await runMessageMutationGuards(
61
+ ctx.container,
62
+ {
63
+ tenantId: scope.tenantId,
64
+ organizationId: scope.organizationId,
65
+ userId: scope.userId,
66
+ resourceKind: 'messages.message',
67
+ resourceId: params.id,
68
+ operation: 'update',
69
+ requestMethod: req.method,
70
+ requestHeaders: req.headers,
71
+ mutationPayload: null,
72
+ },
73
+ resolveUserFeatures(ctx.auth),
74
+ )
75
+ if (!guardResult.ok) {
76
+ return Response.json(
77
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
78
+ { status: guardResult.errorStatus ?? 422 },
79
+ )
80
+ }
59
81
  const { logEntry } = await commandBus.execute('messages.recipients.archive', {
60
82
  input: {
61
83
  messageId: params.id,
@@ -78,6 +100,16 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
78
100
  resourceKind: 'messages.message',
79
101
  resourceId: params.id,
80
102
  })
103
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
104
+ tenantId: scope.tenantId,
105
+ organizationId: scope.organizationId,
106
+ userId: scope.userId,
107
+ resourceKind: 'messages.message',
108
+ resourceId: params.id,
109
+ operation: 'update',
110
+ requestMethod: req.method,
111
+ requestHeaders: req.headers,
112
+ })
81
113
  return response
82
114
  }
83
115
 
@@ -86,6 +118,27 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
86
118
  if ('response' in context) return context.response
87
119
  const { ctx, scope } = context
88
120
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
121
+ const guardResult = await runMessageMutationGuards(
122
+ ctx.container,
123
+ {
124
+ tenantId: scope.tenantId,
125
+ organizationId: scope.organizationId,
126
+ userId: scope.userId,
127
+ resourceKind: 'messages.message',
128
+ resourceId: params.id,
129
+ operation: 'update',
130
+ requestMethod: req.method,
131
+ requestHeaders: req.headers,
132
+ mutationPayload: null,
133
+ },
134
+ resolveUserFeatures(ctx.auth),
135
+ )
136
+ if (!guardResult.ok) {
137
+ return Response.json(
138
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
139
+ { status: guardResult.errorStatus ?? 422 },
140
+ )
141
+ }
89
142
  const { logEntry } = await commandBus.execute('messages.recipients.unarchive', {
90
143
  input: {
91
144
  messageId: params.id,
@@ -108,6 +161,16 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
108
161
  resourceKind: 'messages.message',
109
162
  resourceId: params.id,
110
163
  })
164
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
165
+ tenantId: scope.tenantId,
166
+ organizationId: scope.organizationId,
167
+ userId: scope.userId,
168
+ resourceKind: 'messages.message',
169
+ resourceId: params.id,
170
+ operation: 'update',
171
+ requestMethod: req.method,
172
+ requestHeaders: req.headers,
173
+ })
111
174
  return response
112
175
  }
113
176
 
@@ -7,6 +7,7 @@ import { attachmentIdsPayloadSchema, unlinkAttachmentPayloadSchema } from '../..
7
7
  import { getMessageAttachments, linkAttachmentsToMessage } from '../../../lib/attachments'
8
8
  import { attachOperationMetadataHeader } from '../../../lib/operationMetadata'
9
9
  import { resolveMessageContext } from '../../../lib/routeHelpers'
10
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../guards'
10
11
  import {
11
12
  attachmentIdsPayloadSchema as attachmentIdsOpenApiSchema,
12
13
  errorResponseSchema,
@@ -95,6 +96,27 @@ export async function POST(req: Request, { params }: { params: { id: string } })
95
96
  }
96
97
 
97
98
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
99
+ const guardResult = await runMessageMutationGuards(
100
+ ctx.container,
101
+ {
102
+ tenantId: scope.tenantId,
103
+ organizationId: scope.organizationId,
104
+ userId: scope.userId,
105
+ resourceKind: 'messages.message',
106
+ resourceId: message.id,
107
+ operation: 'update',
108
+ requestMethod: req.method,
109
+ requestHeaders: req.headers,
110
+ mutationPayload: input as Record<string, unknown>,
111
+ },
112
+ resolveUserFeatures(ctx.auth),
113
+ )
114
+ if (!guardResult.ok) {
115
+ return Response.json(
116
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
117
+ { status: guardResult.errorStatus ?? 422 },
118
+ )
119
+ }
98
120
  const { logEntry } = await commandBus.execute('messages.attachments.link_to_draft', {
99
121
  input: {
100
122
  messageId: message.id,
@@ -118,6 +140,16 @@ export async function POST(req: Request, { params }: { params: { id: string } })
118
140
  resourceKind: 'messages.message',
119
141
  resourceId: params.id,
120
142
  })
143
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
144
+ tenantId: scope.tenantId,
145
+ organizationId: scope.organizationId,
146
+ userId: scope.userId,
147
+ resourceKind: 'messages.message',
148
+ resourceId: message.id,
149
+ operation: 'update',
150
+ requestMethod: req.method,
151
+ requestHeaders: req.headers,
152
+ })
121
153
  return response
122
154
  }
123
155
 
@@ -150,6 +182,27 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
150
182
  }
151
183
 
152
184
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
185
+ const guardResult = await runMessageMutationGuards(
186
+ ctx.container,
187
+ {
188
+ tenantId: scope.tenantId,
189
+ organizationId: scope.organizationId,
190
+ userId: scope.userId,
191
+ resourceKind: 'messages.message',
192
+ resourceId: message.id,
193
+ operation: 'update',
194
+ requestMethod: req.method,
195
+ requestHeaders: req.headers,
196
+ mutationPayload: input as Record<string, unknown>,
197
+ },
198
+ resolveUserFeatures(ctx.auth),
199
+ )
200
+ if (!guardResult.ok) {
201
+ return Response.json(
202
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
203
+ { status: guardResult.errorStatus ?? 422 },
204
+ )
205
+ }
153
206
  const { logEntry } = await commandBus.execute('messages.attachments.unlink_from_draft', {
154
207
  input: {
155
208
  messageId: message.id,
@@ -173,6 +226,16 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
173
226
  resourceKind: 'messages.message',
174
227
  resourceId: params.id,
175
228
  })
229
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
230
+ tenantId: scope.tenantId,
231
+ organizationId: scope.organizationId,
232
+ userId: scope.userId,
233
+ resourceKind: 'messages.message',
234
+ resourceId: message.id,
235
+ operation: 'update',
236
+ requestMethod: req.method,
237
+ requestHeaders: req.headers,
238
+ })
176
239
  return response
177
240
  }
178
241
 
@@ -2,6 +2,7 @@ import type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'
2
2
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
3
3
  import { attachOperationMetadataHeader } from '../../../../lib/operationMetadata'
4
4
  import { resolveMessageContext } from '../../../../lib/routeHelpers'
5
+ import { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from '../../../guards'
5
6
  import {
6
7
  conversationMutationResponseSchema,
7
8
  errorResponseSchema,
@@ -15,6 +16,28 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
15
16
  const { ctx, scope } = await resolveMessageContext(req)
16
17
  const commandBus = ctx.container.resolve('commandBus') as CommandBus
17
18
 
19
+ const guardResult = await runMessageMutationGuards(
20
+ ctx.container,
21
+ {
22
+ tenantId: scope.tenantId,
23
+ organizationId: scope.organizationId,
24
+ userId: scope.userId,
25
+ resourceKind: 'messages.conversation',
26
+ resourceId: params.id,
27
+ operation: 'update',
28
+ requestMethod: req.method,
29
+ requestHeaders: req.headers,
30
+ mutationPayload: null,
31
+ },
32
+ resolveUserFeatures(ctx.auth),
33
+ )
34
+ if (!guardResult.ok) {
35
+ return Response.json(
36
+ guardResult.errorBody ?? { error: 'Operation blocked by guard' },
37
+ { status: guardResult.errorStatus ?? 422 },
38
+ )
39
+ }
40
+
18
41
  try {
19
42
  const { result, logEntry } = await commandBus.execute('messages.conversation.archive_for_actor', {
20
43
  input: {
@@ -38,6 +61,16 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
38
61
  resourceKind: 'messages.conversation',
39
62
  resourceId: params.id,
40
63
  })
64
+ await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
65
+ tenantId: scope.tenantId,
66
+ organizationId: scope.organizationId,
67
+ userId: scope.userId,
68
+ resourceKind: 'messages.conversation',
69
+ resourceId: params.id,
70
+ operation: 'update',
71
+ requestMethod: req.method,
72
+ requestHeaders: req.headers,
73
+ })
41
74
  return response
42
75
  } catch (error) {
43
76
  if (error instanceof Error) {