@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6241.1.47c01ad4ed

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 (125) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/commands/users.js +1 -0
  3. package/dist/modules/auth/commands/users.js.map +2 -2
  4. package/dist/modules/communication_channels/data/enrichers.js +9 -1
  5. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  6. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
  7. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
  8. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
  9. package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
  10. package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
  11. package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
  12. package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
  13. package/dist/modules/customers/api/pipelines/route.js +107 -12
  14. package/dist/modules/customers/api/pipelines/route.js.map +2 -2
  15. package/dist/modules/customers/api/settings/address-format/route.js +33 -1
  16. package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
  17. package/dist/modules/customers/api/tags/assign/route.js +37 -0
  18. package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
  19. package/dist/modules/customers/api/tags/unassign/route.js +37 -0
  20. package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
  21. package/dist/modules/entities/api/encryption.js +109 -38
  22. package/dist/modules/entities/api/encryption.js.map +2 -2
  23. package/dist/modules/entities/components/EncryptionManager.js +22 -9
  24. package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
  25. package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
  26. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  27. package/dist/modules/integrations/api/[id]/route.js +10 -3
  28. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  29. package/dist/modules/integrations/api/[id]/state/route.js +20 -5
  30. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  31. package/dist/modules/integrations/api/[id]/version/route.js +17 -1
  32. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  33. package/dist/modules/integrations/api/route.js +2 -0
  34. package/dist/modules/integrations/api/route.js.map +2 -2
  35. package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
  36. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  37. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
  38. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  39. package/dist/modules/integrations/backend/integrations/page.js +4 -3
  40. package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
  41. package/dist/modules/integrations/data/entities.js +2 -2
  42. package/dist/modules/integrations/data/entities.js.map +2 -2
  43. package/dist/modules/integrations/lib/credentials-service.js +32 -0
  44. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  45. package/dist/modules/integrations/lib/state-service.js +4 -2
  46. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  47. package/dist/modules/notifications/lib/notificationService.js +42 -10
  48. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  49. package/dist/modules/payment_gateways/api/status/route.js +91 -6
  50. package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
  51. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
  52. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  53. package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
  54. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  55. package/dist/modules/sales/backend/sales/channels/page.js +25 -7
  56. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  57. package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
  58. package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
  59. package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
  60. package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
  61. package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
  62. package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
  63. package/dist/modules/sales/components/StatusSettings.js +66 -31
  64. package/dist/modules/sales/components/StatusSettings.js.map +2 -2
  65. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
  66. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
  67. package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
  68. package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
  69. package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
  70. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  71. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +7 -8
  72. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  73. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
  74. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
  75. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +4 -3
  76. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
  77. package/dist/modules/workflows/backend/definitions/page.js +19 -10
  78. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  79. package/package.json +7 -7
  80. package/src/modules/auth/commands/users.ts +1 -0
  81. package/src/modules/communication_channels/data/enrichers.ts +25 -0
  82. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
  83. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
  84. package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
  85. package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
  86. package/src/modules/customers/api/pipelines/route.ts +112 -13
  87. package/src/modules/customers/api/settings/address-format/route.ts +36 -1
  88. package/src/modules/customers/api/tags/assign/route.ts +39 -0
  89. package/src/modules/customers/api/tags/unassign/route.ts +39 -0
  90. package/src/modules/customers/i18n/de.json +1 -0
  91. package/src/modules/customers/i18n/en.json +1 -0
  92. package/src/modules/customers/i18n/es.json +1 -0
  93. package/src/modules/customers/i18n/pl.json +1 -0
  94. package/src/modules/entities/api/encryption.ts +122 -39
  95. package/src/modules/entities/components/EncryptionManager.tsx +28 -9
  96. package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
  97. package/src/modules/integrations/api/[id]/route.ts +8 -1
  98. package/src/modules/integrations/api/[id]/state/route.ts +20 -4
  99. package/src/modules/integrations/api/[id]/version/route.ts +18 -1
  100. package/src/modules/integrations/api/route.ts +3 -0
  101. package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
  102. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
  103. package/src/modules/integrations/backend/integrations/page.tsx +8 -5
  104. package/src/modules/integrations/data/entities.ts +2 -2
  105. package/src/modules/integrations/lib/credentials-service.ts +35 -0
  106. package/src/modules/integrations/lib/state-service.ts +3 -0
  107. package/src/modules/notifications/lib/notificationService.ts +74 -11
  108. package/src/modules/payment_gateways/api/status/route.ts +97 -5
  109. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
  110. package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
  111. package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
  112. package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
  113. package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
  114. package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
  115. package/src/modules/sales/components/StatusSettings.tsx +74 -33
  116. package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
  117. package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
  118. package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
  119. package/src/modules/staff/i18n/de.json +5 -5
  120. package/src/modules/staff/i18n/es.json +5 -5
  121. package/src/modules/staff/i18n/pl.json +5 -5
  122. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +7 -8
  123. package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
  124. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +4 -3
  125. package/src/modules/workflows/backend/definitions/page.tsx +19 -10
@@ -9,6 +9,7 @@ import { apiCall, apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall"
9
9
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
10
10
  import { ProjectColorDot } from "./ProjectColorDot.js";
11
11
  import { startTimerEntry } from "./startTimer.js";
12
+ import { resolveTimerActionError } from "./timerErrors.js";
12
13
  function formatElapsed(seconds) {
13
14
  const h = Math.floor(seconds / 3600);
14
15
  const m = Math.floor(seconds % 3600 / 60);
@@ -111,9 +112,9 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
111
112
  setActiveEntryId(entryId);
112
113
  setActiveProjectId(selectedProjectId);
113
114
  startElapsedCounter((/* @__PURE__ */ new Date()).toISOString());
114
- } catch {
115
+ } catch (err) {
115
116
  flash(
116
- t("staff.timesheets.my.timer.startError", "Failed to start timer"),
117
+ resolveTimerActionError(err, t("staff.timesheets.my.timer.startError", "Failed to start timer")),
117
118
  "error"
118
119
  );
119
120
  } finally {
@@ -133,9 +134,9 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
133
134
  setDescription("");
134
135
  stopElapsedCounter();
135
136
  onTimerStopped();
136
- } catch {
137
+ } catch (err) {
137
138
  flash(
138
- t("staff.timesheets.my.timer.stopError", "Failed to stop timer"),
139
+ resolveTimerActionError(err, t("staff.timesheets.my.timer.stopError", "Failed to stop timer")),
139
140
  "error"
140
141
  );
141
142
  } finally {
@@ -227,11 +228,10 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
227
228
  IconButton,
228
229
  {
229
230
  type: "button",
230
- variant: "outline",
231
+ variant: "destructive",
231
232
  size: "default",
232
233
  onClick: handleStop,
233
234
  disabled: isStopping,
234
- className: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
235
235
  "aria-label": t("staff.timesheets.my.timer.stop", "Stop timer"),
236
236
  children: /* @__PURE__ */ jsx(Square, { className: "size-4" })
237
237
  }
@@ -239,11 +239,10 @@ function TimerBar({ projects, staffMemberId, onTimerStopped }) {
239
239
  IconButton,
240
240
  {
241
241
  type: "button",
242
- variant: "outline",
242
+ variant: "primary",
243
243
  size: "default",
244
244
  onClick: handleStart,
245
245
  disabled: isStarting || !selectedProjectId,
246
- className: "bg-primary text-primary-foreground hover:bg-primary/90",
247
246
  "aria-label": t("staff.timesheets.my.timer.start", "Start timer"),
248
247
  children: /* @__PURE__ */ jsx(Play, { className: "size-4" })
249
248
  }
@@ -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'\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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",
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'\nimport { resolveTimerActionError } from './timerErrors'\n\ntype ProjectOption = {\n id: string\n name: string\n code: string | null\n color?: string | null\n}\n\ntype TimerBarProps = {\n projects: ProjectOption[]\n staffMemberId: string | null\n onTimerStopped: () => void\n}\n\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 (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),\n 'error',\n )\n } finally {\n setIsStarting(false)\n }\n }\n\n const handleStop = async () => {\n if (!activeEntryId) return\n\n setIsStopping(true)\n try {\n 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 (err) {\n flash(\n resolveTimerActionError(err, t('staff.timesheets.my.timer.stopError', 'Failed to stop timer')),\n 'error',\n )\n } finally {\n setIsStopping(false)\n }\n }\n\n return (\n <div className=\"flex items-center gap-3 rounded-lg border bg-card p-3 mb-4\">\n <input\n type=\"text\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n readOnly={isRunning}\n placeholder={t(\n 'staff.timesheets.my.timer.placeholder',\n 'What are you working on?',\n )}\n className=\"flex-1 bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground\"\n />\n\n <div className=\"relative\" ref={dropdownRef}>\n {isRunning ? (\n activeProject ? (\n <span className=\"inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded border bg-muted\">\n <ProjectColorDot colorKey={activeProject.color} projectName={activeProject.name} size=\"xs\" />\n {activeProject.name}\n </span>\n ) : null\n ) : (\n <>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setShowProjectDropdown(!showProjectDropdown)\n setProjectFilter('')\n }}\n className=\"text-xs font-medium\"\n >\n {selectedProject\n ? (<><ProjectColorDot colorKey={selectedProject.color} projectName={selectedProject.name} size=\"xs\" /><span className=\"ml-1\">{selectedProject.name}</span></>)\n : t('staff.timesheets.my.timer.selectProject', 'Project')}\n </Button>\n\n {showProjectDropdown && (\n <div className=\"absolute right-0 top-full mt-1 z-50 min-w-[200px] rounded-md border bg-popover p-1 shadow-md\">\n <input\n type=\"text\"\n value={projectFilter}\n onChange={(e) => setProjectFilter(e.target.value)}\n placeholder={t(\n 'staff.timesheets.my.timer.searchProject',\n 'Search projects...',\n )}\n className=\"w-full bg-transparent border-b px-2 py-1.5 text-xs outline-none placeholder:text-muted-foreground mb-1\"\n autoFocus\n />\n <div className=\"max-h-[200px] overflow-y-auto\">\n {filteredProjects.length === 0 ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {t(\n 'staff.timesheets.my.timer.noProjects',\n 'No projects found',\n )}\n </div>\n ) : (\n filteredProjects.map((project) => (\n <Button\n key={project.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs h-auto py-1.5\"\n onClick={() => {\n setSelectedProjectId(project.id)\n setShowProjectDropdown(false)\n setProjectFilter('')\n }}\n >\n <ProjectColorDot colorKey={project.color} projectName={project.name} size=\"xs\" />\n <span className=\"ml-1\">{project.name}</span>\n {project.code ? (\n <span className=\"ml-1 text-muted-foreground\">\n ({project.code})\n </span>\n ) : null}\n </Button>\n ))\n )}\n </div>\n </div>\n )}\n </>\n )}\n </div>\n\n <span className=\"font-mono text-sm tabular-nums min-w-[64px] text-right\">\n {formatElapsed(elapsedSeconds)}\n </span>\n\n {isRunning ? (\n <IconButton\n type=\"button\"\n variant=\"destructive\"\n size=\"default\"\n onClick={handleStop}\n disabled={isStopping}\n aria-label={t('staff.timesheets.my.timer.stop', 'Stop timer')}\n >\n <Square className=\"size-4\" />\n </IconButton>\n ) : (\n <IconButton\n type=\"button\"\n variant=\"primary\"\n size=\"default\"\n onClick={handleStart}\n disabled={isStarting || !selectedProjectId}\n aria-label={t('staff.timesheets.my.timer.start', 'Start timer')}\n >\n <Play className=\"size-4\" />\n </IconButton>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,12 @@
1
+ import { normalizeCrudServerError } from "@open-mercato/ui/backend/utils/serverErrors";
2
+ function resolveTimerActionError(err, fallback) {
3
+ const status = err?.status;
4
+ if (typeof status !== "number") return fallback;
5
+ const message = normalizeCrudServerError(err).message;
6
+ const trimmed = typeof message === "string" ? message.trim() : "";
7
+ return trimmed.length > 0 ? trimmed : fallback;
8
+ }
9
+ export {
10
+ resolveTimerActionError
11
+ };
12
+ //# sourceMappingURL=timerErrors.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/timesheets-ui/timerErrors.ts"],
4
+ "sourcesContent": ["import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\n\n/**\n * Resolves the message to show when a timesheet timer start/stop request\n * fails. Prefers the localized message the server already returned (e.g. the\n * 409 \"another timer is already running\" reason, surfaced by the atomic\n * start-timer route) over a generic client-side fallback. Both the TimerBar\n * and the dashboard time-reporting widget use this so a rejected timer action\n * shows the specific server reason in the active locale instead of a hardcoded\n * English string (issue #3507).\n *\n * The server message is only trusted when the error originated from an HTTP\n * response (`raiseCrudError` attaches a numeric `status`); transport-level\n * failures (no response) fall back to the localized fallback so the user never\n * sees an untranslated network error string.\n */\nexport function resolveTimerActionError(err: unknown, fallback: string): string {\n const status = (err as { status?: unknown } | null)?.status\n if (typeof status !== 'number') return fallback\n const message = normalizeCrudServerError(err).message\n const trimmed = typeof message === 'string' ? message.trim() : ''\n return trimmed.length > 0 ? trimmed : fallback\n}\n"],
5
+ "mappings": "AAAA,SAAS,gCAAgC;AAgBlC,SAAS,wBAAwB,KAAc,UAA0B;AAC9E,QAAM,SAAU,KAAqC;AACrD,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,QAAM,UAAU,yBAAyB,GAAG,EAAE;AAC9C,QAAM,UAAU,OAAO,YAAY,WAAW,QAAQ,KAAK,IAAI;AAC/D,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;",
6
+ "names": []
7
+ }
@@ -6,6 +6,7 @@ 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
8
  import { startTimerEntry } from "../../../lib/timesheets-ui/startTimer.js";
9
+ import { resolveTimerActionError } from "../../../lib/timesheets-ui/timerErrors.js";
9
10
  import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
10
11
  function formatElapsed(startedAt) {
11
12
  const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime());
@@ -125,7 +126,7 @@ const TimeReportingWidget = ({
125
126
  await loadState();
126
127
  } catch (err) {
127
128
  console.error("staff.timesheets.timeReporting.start", err);
128
- setError(t("staff.timesheets.widgets.timeReporting.startError", "Failed to start timer"));
129
+ setError(resolveTimerActionError(err, t("staff.timesheets.widgets.timeReporting.startError", "Failed to start timer")));
129
130
  } finally {
130
131
  setActionLoading(false);
131
132
  }
@@ -138,7 +139,7 @@ const TimeReportingWidget = ({
138
139
  await loadState();
139
140
  } catch (err) {
140
141
  console.error("staff.timesheets.timeReporting.stop", err);
141
- setError(t("staff.timesheets.widgets.timeReporting.stopError", "Failed to stop timer"));
142
+ setError(resolveTimerActionError(err, t("staff.timesheets.widgets.timeReporting.stopError", "Failed to stop timer")));
142
143
  } finally {
143
144
  setActionLoading(false);
144
145
  }
@@ -150,7 +151,7 @@ const TimeReportingWidget = ({
150
151
  return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.timesheets.widgets.timeReporting.loading", "Loading...") }) });
151
152
  }
152
153
  if (error) {
153
- return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) });
154
+ return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", children: error }) });
154
155
  }
155
156
  if (projects.length === 0) {
156
157
  return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("staff.timesheets.widgets.timeReporting.noProjects", "No projects assigned.") }) });
@@ -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 { 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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",
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 { 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\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(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, 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(resolveTimerActionError(err, 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 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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",
6
6
  "names": []
7
7
  }
@@ -94,19 +94,28 @@ function WorkflowDefinitionsListPage() {
94
94
  }
95
95
  setDeleteTarget(null);
96
96
  };
97
- const handleToggleEnabled = async (id, currentEnabled) => {
98
- const result = await apiCall(`/api/workflows/definitions/${id}`, {
99
- method: "PUT",
100
- headers: { "Content-Type": "application/json" },
101
- body: JSON.stringify({
102
- enabled: !currentEnabled
97
+ const handleToggleEnabled = async (id, currentEnabled, updatedAt) => {
98
+ const result = await withScopedApiRequestHeaders(
99
+ buildOptimisticLockHeader(updatedAt),
100
+ () => apiCall(`/api/workflows/definitions/${id}`, {
101
+ method: "PUT",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({
104
+ enabled: !currentEnabled
105
+ })
103
106
  })
104
- });
107
+ );
105
108
  if (result.ok) {
106
109
  flash(t("workflows.messages.updated"), "success");
107
110
  queryClient.invalidateQueries({ queryKey: ["workflow-definitions"] });
108
111
  } else {
109
- flash(t("workflows.messages.updateFailed"), "error");
112
+ const conflictError = Object.assign(new Error(t("workflows.messages.updateFailed")), {
113
+ status: result.status,
114
+ ...result.result && typeof result.result === "object" ? result.result : {}
115
+ });
116
+ if (!surfaceRecordConflict(conflictError, t)) {
117
+ flash(t("workflows.messages.updateFailed"), "error");
118
+ }
110
119
  }
111
120
  };
112
121
  const handleDuplicate = async (definition) => {
@@ -212,7 +221,7 @@ function WorkflowDefinitionsListPage() {
212
221
  cell: ({ row }) => /* @__PURE__ */ jsx(
213
222
  "button",
214
223
  {
215
- onClick: () => handleToggleEnabled(row.original.id, row.original.enabled),
224
+ onClick: () => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt),
216
225
  className: `inline-flex items-center px-2 py-1 rounded text-xs font-medium cursor-pointer ${row.original.enabled ? "bg-status-success-bg text-status-success-text hover:bg-status-success-border" : "bg-status-neutral-bg text-status-neutral-text hover:bg-status-neutral-border"}`,
217
226
  title: t("workflows.actions.toggleEnabled"),
218
227
  children: row.original.enabled ? t("common.yes") : t("common.no")
@@ -262,7 +271,7 @@ function WorkflowDefinitionsListPage() {
262
271
  ...!isCodeOnly ? [{
263
272
  id: row.original.enabled ? "disable" : "enable",
264
273
  label: row.original.enabled ? t("common.disable") : t("common.enable"),
265
- onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled)
274
+ onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt)
266
275
  }] : [],
267
276
  ...!isCodeOnly ? [{
268
277
  id: "duplicate",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/workflows/backend/definitions/page.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { Trash2 } from 'lucide-react'\n\ntype WorkflowDefinitionSource = 'code' | 'code_override' | 'user'\n\ntype WorkflowDefinition = {\n id: string\n workflowId: string\n workflowName: string\n description: string | null\n version: number\n definition: Record<string, unknown>\n enabled: boolean\n effectiveFrom: string | null\n effectiveTo: string | null\n metadata: {\n tags?: string[]\n category?: string\n icon?: string\n } | null\n tenantId: string\n organizationId: string\n createdAt: string\n updatedAt: string\n createdBy: string | null\n source?: WorkflowDefinitionSource\n isCodeBased?: boolean\n}\n\ntype DefinitionsResponse = {\n data: WorkflowDefinition[]\n pagination: {\n total: number\n limit: number\n offset: number\n hasMore: boolean\n }\n}\n\ntype CreateDefinitionResponse = {\n data?: {\n id?: string\n }\n error?: string\n}\n\nconst WORKFLOW_ID_MAX_LENGTH = 100\n\nfunction buildDuplicateWorkflowId(sourceWorkflowId: string, attempt: number): string {\n const suffix = attempt === 0 ? '_copy' : `_copy_${attempt + 1}`\n const maxBaseLength = Math.max(1, WORKFLOW_ID_MAX_LENGTH - suffix.length)\n const base = sourceWorkflowId.slice(0, maxBaseLength)\n return `${base}${suffix}`\n}\n\nexport default function WorkflowDefinitionsListPage() {\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const t = useT()\n const router = useRouter()\n const queryClient = useQueryClient()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; name: string; updatedAt: string | null } | null>(null)\n\n const { data, isLoading, error } = useQuery({\n queryKey: ['workflow-definitions', 'list', filterValues, page],\n queryFn: async () => {\n const params = new URLSearchParams()\n const offset = (page - 1) * pageSize\n params.set('limit', pageSize.toString())\n params.set('offset', offset.toString())\n\n if (filterValues.enabled !== undefined && filterValues.enabled !== '') {\n params.set('enabled', filterValues.enabled as string)\n }\n if (filterValues.workflowId) params.set('workflowId', filterValues.workflowId as string)\n if (filterValues.search) params.set('search', filterValues.search as string)\n\n const result = await apiCall<DefinitionsResponse>(\n `/api/workflows/definitions?${params.toString()}`\n )\n\n if (!result.ok) {\n throw new Error('Failed to fetch workflow definitions')\n }\n\n const response = result.result\n if (response?.pagination) {\n setTotal(response.pagination.total || 0)\n const calculatedPages = Math.ceil((response.pagination.total || 0) / pageSize)\n setTotalPages(calculatedPages || 1)\n }\n\n return response?.data || []\n },\n })\n\n const handleDelete = (id: string, workflowName: string, updatedAt: string | null) => {\n setDeleteTarget({ id, name: workflowName, updatedAt })\n }\n\n const confirmDelete = async () => {\n if (!deleteTarget) return\n\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(deleteTarget.updatedAt),\n () => apiCall(`/api/workflows/definitions/${deleteTarget.id}`, {\n method: 'DELETE',\n }),\n )\n\n if (result.ok) {\n flash(t('workflows.messages.deleted'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n const conflictError = Object.assign(new Error(t('workflows.messages.deleteFailed')), {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n })\n if (!surfaceRecordConflict(conflictError, t)) {\n flash(t('workflows.messages.deleteFailed'), 'error')\n }\n }\n setDeleteTarget(null)\n }\n\n const handleToggleEnabled = async (id: string, currentEnabled: boolean) => {\n const result = await apiCall(`/api/workflows/definitions/${id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n enabled: !currentEnabled,\n }),\n })\n\n if (result.ok) {\n flash(t('workflows.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n flash(t('workflows.messages.updateFailed'), 'error')\n }\n }\n\n const handleDuplicate = async (definition: WorkflowDefinition) => {\n for (let attempt = 0; attempt < 10; attempt += 1) {\n const duplicateWorkflowId = buildDuplicateWorkflowId(definition.workflowId, attempt)\n const result = await apiCall<CreateDefinitionResponse>('/api/workflows/definitions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n workflowId: duplicateWorkflowId,\n workflowName: definition.workflowName,\n description: definition.description,\n version: definition.version,\n definition: definition.definition,\n metadata: definition.metadata,\n enabled: definition.enabled,\n }),\n })\n\n if (result.ok) {\n flash(t('workflows.messages.workflowDuplicated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n return\n }\n\n if (result.status !== 409) {\n break\n }\n }\n\n flash(t('workflows.errors.createFailed'), 'error')\n }\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n const next: FilterValues = {}\n Object.entries(values).forEach(([key, value]) => {\n if (value !== undefined && value !== '') next[key] = value\n })\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const filters: FilterDef[] = [\n {\n id: 'search',\n type: 'text',\n label: t('workflows.filters.search'),\n placeholder: t('workflows.filters.searchPlaceholder'),\n },\n {\n id: 'enabled',\n type: 'select',\n label: t('workflows.filters.status'),\n options: [\n { label: t('common.all'), value: '' },\n { label: t('common.enabled'), value: 'true' },\n { label: t('common.disabled'), value: 'false' },\n ],\n },\n {\n id: 'workflowId',\n type: 'text',\n label: t('workflows.filters.workflowId'),\n placeholder: t('workflows.filters.workflowIdPlaceholder'),\n },\n ]\n\n const columns: ColumnDef<WorkflowDefinition>[] = [\n {\n id: 'workflowId',\n header: t('workflows.fields.workflowId'),\n accessorKey: 'workflowId',\n meta: { truncate: false },\n cell: ({ row }) => (\n <span className=\"font-mono text-sm\">{row.original.workflowId}</span>\n ),\n },\n {\n id: 'workflowName',\n header: t('workflows.fields.workflowName'),\n accessorKey: 'workflowName',\n meta: { truncate: false },\n cell: ({ row }) => (\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{row.original.workflowName}</span>\n {row.original.source === 'code' && (\n <Badge variant=\"secondary\">{t('workflows.source.code')}</Badge>\n )}\n {row.original.source === 'code_override' && (\n <Badge variant=\"outline\">{t('workflows.source.code_override')}</Badge>\n )}\n </div>\n {row.original.description && (\n <div className=\"text-xs text-muted-foreground\">\n {row.original.description}\n </div>\n )}\n {row.original.metadata?.category && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {row.original.metadata.category}\n </div>\n )}\n </div>\n ),\n },\n {\n id: 'version',\n header: t('workflows.fields.version'),\n accessorKey: 'version',\n meta: { truncate: false },\n cell: ({ row }) => (\n <Badge variant=\"secondary\" className=\"font-mono\">\n v{row.original.version}\n </Badge>\n ),\n },\n {\n id: 'enabled',\n header: t('workflows.fields.enabled'),\n accessorKey: 'enabled',\n cell: ({ row }) => (\n <button\n onClick={() => handleToggleEnabled(row.original.id, row.original.enabled)}\n className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium cursor-pointer ${\n row.original.enabled\n ? 'bg-status-success-bg text-status-success-text hover:bg-status-success-border'\n : 'bg-status-neutral-bg text-status-neutral-text hover:bg-status-neutral-border'\n }`}\n title={t('workflows.actions.toggleEnabled')}\n >\n {row.original.enabled ? t('common.yes') : t('common.no')}\n </button>\n ),\n },\n {\n id: 'tags',\n header: t('workflows.fields.tags'),\n cell: ({ row }) => {\n const tags = row.original.metadata?.tags || []\n if (tags.length === 0) return <span className=\"text-muted-foreground\">-</span>\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.slice(0, 2).map((tag, idx) => (\n <Badge key={idx} variant=\"secondary\">\n {tag}\n </Badge>\n ))}\n {tags.length > 2 && (\n <Badge variant=\"outline\">+{tags.length - 2}</Badge>\n )}\n </div>\n )\n },\n },\n {\n id: 'createdAt',\n header: t('workflows.fields.createdAt'),\n accessorKey: 'createdAt',\n cell: ({ row }) => {\n const date = new Date(row.original.createdAt)\n return <span className=\"text-sm text-muted-foreground\">{date.toLocaleDateString()}</span>\n },\n },\n {\n id: 'actions',\n header: '',\n cell: ({ row }) => {\n const isCodeOnly = row.original.source === 'code'\n const items = [\n {\n id: 'edit',\n label: isCodeOnly ? t('common.view') : t('common.edit'),\n href: `/backend/definitions/${row.original.id}`,\n },\n ...(!isCodeOnly ? [{\n id: 'edit-visual',\n label: t('workflows.actions.editVisually'),\n href: `/backend/definitions/visual-editor?id=${row.original.id}`,\n }] : []),\n ...(!isCodeOnly ? [{\n id: row.original.enabled ? 'disable' : 'enable',\n label: row.original.enabled ? t('common.disable') : t('common.enable'),\n onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'duplicate',\n label: t('common.duplicate'),\n onSelect: () => handleDuplicate(row.original),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'delete',\n label: t('common.delete'),\n onSelect: () => handleDelete(row.original.id, row.original.workflowName, row.original.updatedAt),\n destructive: true,\n }] : []),\n ]\n return <RowActions items={items} />\n },\n },\n ]\n\n if (error) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={t('workflows.messages.loadFailed')}\n description={error.message}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })}>\n {t('common.retry', 'Retry')}\n </Button>\n )}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('workflows.list.title')}\n actions={(\n <div className=\"flex items-center gap-2\">\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/definitions/visual-editor\">\n {t('workflows.actions.createVisual')}\n </Link>\n </Button>\n <Button asChild>\n <Link href=\"/backend/definitions/create\">\n {t('workflows.actions.create')}\n </Link>\n </Button>\n </div>\n )}\n columns={columns}\n data={data || []}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n onRowClick={(row) => router.push(`/backend/definitions/visual-editor?id=${row.id}`)}\n perspective={{\n tableId: 'workflows.definitions.list',\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('workflows.list.title')}\n createHref=\"/backend/definitions/create\"\n createLabel={t('workflows.actions.create')}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n />\n <Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>{t('workflows.confirm.deleteTitle')}</DialogTitle>\n <DialogDescription>\n {t('workflows.confirm.delete', { name: deleteTarget?.name ?? '' })}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setDeleteTarget(null)}>\n {t('common.cancel')}\n </Button>\n <Button variant=\"destructive\" onClick={confirmDelete}>\n <Trash2/>\n {t('common.delete')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { Trash2 } from 'lucide-react'\n\ntype WorkflowDefinitionSource = 'code' | 'code_override' | 'user'\n\ntype WorkflowDefinition = {\n id: string\n workflowId: string\n workflowName: string\n description: string | null\n version: number\n definition: Record<string, unknown>\n enabled: boolean\n effectiveFrom: string | null\n effectiveTo: string | null\n metadata: {\n tags?: string[]\n category?: string\n icon?: string\n } | null\n tenantId: string\n organizationId: string\n createdAt: string\n updatedAt: string\n createdBy: string | null\n source?: WorkflowDefinitionSource\n isCodeBased?: boolean\n}\n\ntype DefinitionsResponse = {\n data: WorkflowDefinition[]\n pagination: {\n total: number\n limit: number\n offset: number\n hasMore: boolean\n }\n}\n\ntype CreateDefinitionResponse = {\n data?: {\n id?: string\n }\n error?: string\n}\n\nconst WORKFLOW_ID_MAX_LENGTH = 100\n\nfunction buildDuplicateWorkflowId(sourceWorkflowId: string, attempt: number): string {\n const suffix = attempt === 0 ? '_copy' : `_copy_${attempt + 1}`\n const maxBaseLength = Math.max(1, WORKFLOW_ID_MAX_LENGTH - suffix.length)\n const base = sourceWorkflowId.slice(0, maxBaseLength)\n return `${base}${suffix}`\n}\n\nexport default function WorkflowDefinitionsListPage() {\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const t = useT()\n const router = useRouter()\n const queryClient = useQueryClient()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; name: string; updatedAt: string | null } | null>(null)\n\n const { data, isLoading, error } = useQuery({\n queryKey: ['workflow-definitions', 'list', filterValues, page],\n queryFn: async () => {\n const params = new URLSearchParams()\n const offset = (page - 1) * pageSize\n params.set('limit', pageSize.toString())\n params.set('offset', offset.toString())\n\n if (filterValues.enabled !== undefined && filterValues.enabled !== '') {\n params.set('enabled', filterValues.enabled as string)\n }\n if (filterValues.workflowId) params.set('workflowId', filterValues.workflowId as string)\n if (filterValues.search) params.set('search', filterValues.search as string)\n\n const result = await apiCall<DefinitionsResponse>(\n `/api/workflows/definitions?${params.toString()}`\n )\n\n if (!result.ok) {\n throw new Error('Failed to fetch workflow definitions')\n }\n\n const response = result.result\n if (response?.pagination) {\n setTotal(response.pagination.total || 0)\n const calculatedPages = Math.ceil((response.pagination.total || 0) / pageSize)\n setTotalPages(calculatedPages || 1)\n }\n\n return response?.data || []\n },\n })\n\n const handleDelete = (id: string, workflowName: string, updatedAt: string | null) => {\n setDeleteTarget({ id, name: workflowName, updatedAt })\n }\n\n const confirmDelete = async () => {\n if (!deleteTarget) return\n\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(deleteTarget.updatedAt),\n () => apiCall(`/api/workflows/definitions/${deleteTarget.id}`, {\n method: 'DELETE',\n }),\n )\n\n if (result.ok) {\n flash(t('workflows.messages.deleted'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n const conflictError = Object.assign(new Error(t('workflows.messages.deleteFailed')), {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n })\n if (!surfaceRecordConflict(conflictError, t)) {\n flash(t('workflows.messages.deleteFailed'), 'error')\n }\n }\n setDeleteTarget(null)\n }\n\n const handleToggleEnabled = async (id: string, currentEnabled: boolean, updatedAt: string | null) => {\n const result = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () => apiCall(`/api/workflows/definitions/${id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n enabled: !currentEnabled,\n }),\n }),\n )\n\n if (result.ok) {\n flash(t('workflows.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n } else {\n const conflictError = Object.assign(new Error(t('workflows.messages.updateFailed')), {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n })\n if (!surfaceRecordConflict(conflictError, t)) {\n flash(t('workflows.messages.updateFailed'), 'error')\n }\n }\n }\n\n const handleDuplicate = async (definition: WorkflowDefinition) => {\n for (let attempt = 0; attempt < 10; attempt += 1) {\n const duplicateWorkflowId = buildDuplicateWorkflowId(definition.workflowId, attempt)\n const result = await apiCall<CreateDefinitionResponse>('/api/workflows/definitions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n workflowId: duplicateWorkflowId,\n workflowName: definition.workflowName,\n description: definition.description,\n version: definition.version,\n definition: definition.definition,\n metadata: definition.metadata,\n enabled: definition.enabled,\n }),\n })\n\n if (result.ok) {\n flash(t('workflows.messages.workflowDuplicated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })\n return\n }\n\n if (result.status !== 409) {\n break\n }\n }\n\n flash(t('workflows.errors.createFailed'), 'error')\n }\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n const next: FilterValues = {}\n Object.entries(values).forEach(([key, value]) => {\n if (value !== undefined && value !== '') next[key] = value\n })\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const filters: FilterDef[] = [\n {\n id: 'search',\n type: 'text',\n label: t('workflows.filters.search'),\n placeholder: t('workflows.filters.searchPlaceholder'),\n },\n {\n id: 'enabled',\n type: 'select',\n label: t('workflows.filters.status'),\n options: [\n { label: t('common.all'), value: '' },\n { label: t('common.enabled'), value: 'true' },\n { label: t('common.disabled'), value: 'false' },\n ],\n },\n {\n id: 'workflowId',\n type: 'text',\n label: t('workflows.filters.workflowId'),\n placeholder: t('workflows.filters.workflowIdPlaceholder'),\n },\n ]\n\n const columns: ColumnDef<WorkflowDefinition>[] = [\n {\n id: 'workflowId',\n header: t('workflows.fields.workflowId'),\n accessorKey: 'workflowId',\n meta: { truncate: false },\n cell: ({ row }) => (\n <span className=\"font-mono text-sm\">{row.original.workflowId}</span>\n ),\n },\n {\n id: 'workflowName',\n header: t('workflows.fields.workflowName'),\n accessorKey: 'workflowName',\n meta: { truncate: false },\n cell: ({ row }) => (\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{row.original.workflowName}</span>\n {row.original.source === 'code' && (\n <Badge variant=\"secondary\">{t('workflows.source.code')}</Badge>\n )}\n {row.original.source === 'code_override' && (\n <Badge variant=\"outline\">{t('workflows.source.code_override')}</Badge>\n )}\n </div>\n {row.original.description && (\n <div className=\"text-xs text-muted-foreground\">\n {row.original.description}\n </div>\n )}\n {row.original.metadata?.category && (\n <div className=\"text-xs text-muted-foreground mt-0.5\">\n {row.original.metadata.category}\n </div>\n )}\n </div>\n ),\n },\n {\n id: 'version',\n header: t('workflows.fields.version'),\n accessorKey: 'version',\n meta: { truncate: false },\n cell: ({ row }) => (\n <Badge variant=\"secondary\" className=\"font-mono\">\n v{row.original.version}\n </Badge>\n ),\n },\n {\n id: 'enabled',\n header: t('workflows.fields.enabled'),\n accessorKey: 'enabled',\n cell: ({ row }) => (\n <button\n onClick={() => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt)}\n className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium cursor-pointer ${\n row.original.enabled\n ? 'bg-status-success-bg text-status-success-text hover:bg-status-success-border'\n : 'bg-status-neutral-bg text-status-neutral-text hover:bg-status-neutral-border'\n }`}\n title={t('workflows.actions.toggleEnabled')}\n >\n {row.original.enabled ? t('common.yes') : t('common.no')}\n </button>\n ),\n },\n {\n id: 'tags',\n header: t('workflows.fields.tags'),\n cell: ({ row }) => {\n const tags = row.original.metadata?.tags || []\n if (tags.length === 0) return <span className=\"text-muted-foreground\">-</span>\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.slice(0, 2).map((tag, idx) => (\n <Badge key={idx} variant=\"secondary\">\n {tag}\n </Badge>\n ))}\n {tags.length > 2 && (\n <Badge variant=\"outline\">+{tags.length - 2}</Badge>\n )}\n </div>\n )\n },\n },\n {\n id: 'createdAt',\n header: t('workflows.fields.createdAt'),\n accessorKey: 'createdAt',\n cell: ({ row }) => {\n const date = new Date(row.original.createdAt)\n return <span className=\"text-sm text-muted-foreground\">{date.toLocaleDateString()}</span>\n },\n },\n {\n id: 'actions',\n header: '',\n cell: ({ row }) => {\n const isCodeOnly = row.original.source === 'code'\n const items = [\n {\n id: 'edit',\n label: isCodeOnly ? t('common.view') : t('common.edit'),\n href: `/backend/definitions/${row.original.id}`,\n },\n ...(!isCodeOnly ? [{\n id: 'edit-visual',\n label: t('workflows.actions.editVisually'),\n href: `/backend/definitions/visual-editor?id=${row.original.id}`,\n }] : []),\n ...(!isCodeOnly ? [{\n id: row.original.enabled ? 'disable' : 'enable',\n label: row.original.enabled ? t('common.disable') : t('common.enable'),\n onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'duplicate',\n label: t('common.duplicate'),\n onSelect: () => handleDuplicate(row.original),\n }] : []),\n ...(!isCodeOnly ? [{\n id: 'delete',\n label: t('common.delete'),\n onSelect: () => handleDelete(row.original.id, row.original.workflowName, row.original.updatedAt),\n destructive: true,\n }] : []),\n ]\n return <RowActions items={items} />\n },\n },\n ]\n\n if (error) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={t('workflows.messages.loadFailed')}\n description={error.message}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })}>\n {t('common.retry', 'Retry')}\n </Button>\n )}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('workflows.list.title')}\n actions={(\n <div className=\"flex items-center gap-2\">\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/definitions/visual-editor\">\n {t('workflows.actions.createVisual')}\n </Link>\n </Button>\n <Button asChild>\n <Link href=\"/backend/definitions/create\">\n {t('workflows.actions.create')}\n </Link>\n </Button>\n </div>\n )}\n columns={columns}\n data={data || []}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n onRowClick={(row) => router.push(`/backend/definitions/visual-editor?id=${row.id}`)}\n perspective={{\n tableId: 'workflows.definitions.list',\n }}\n emptyState={(\n <ListEmptyState\n entityName={t('workflows.list.title')}\n createHref=\"/backend/definitions/create\"\n createLabel={t('workflows.actions.create')}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n />\n <Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>{t('workflows.confirm.deleteTitle')}</DialogTitle>\n <DialogDescription>\n {t('workflows.confirm.delete', { name: deleteTarget?.name ?? '' })}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setDeleteTarget(null)}>\n {t('common.cancel')}\n </Button>\n <Button variant=\"destructive\" onClick={confirmDelete}>\n <Trash2/>\n {t('common.delete')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\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.6229.1.ebe6706732",
3
+ "version": "0.6.6-develop.6241.1.47c01ad4ed",
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.6229.1.ebe6706732",
250
- "@open-mercato/shared": "0.6.6-develop.6229.1.ebe6706732",
251
- "@open-mercato/ui": "0.6.6-develop.6229.1.ebe6706732",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6241.1.47c01ad4ed",
250
+ "@open-mercato/shared": "0.6.6-develop.6241.1.47c01ad4ed",
251
+ "@open-mercato/ui": "0.6.6-develop.6241.1.47c01ad4ed",
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.6229.1.ebe6706732",
257
- "@open-mercato/shared": "0.6.6-develop.6229.1.ebe6706732",
258
- "@open-mercato/ui": "0.6.6-develop.6229.1.ebe6706732",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6241.1.47c01ad4ed",
257
+ "@open-mercato/shared": "0.6.6-develop.6241.1.47c01ad4ed",
258
+ "@open-mercato/ui": "0.6.6-develop.6241.1.47c01ad4ed",
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",
@@ -205,6 +205,7 @@ const createUserCommand: CommandHandler<Record<string, unknown>, CreateUserResul
205
205
  )
206
206
  if (!organization) throw new CrudHttpError(400, { error: 'Organization not found' })
207
207
  const tenantId = organization.tenant?.id ? String(organization.tenant.id) : null
208
+ assertTargetTenantInScope(resolveActorTenantScope(ctx), tenantId, 'Organization not found')
208
209
 
209
210
  const emailHash = computeEmailHash(parsed.email)
210
211
  // Email is unique per-tenant, not globally (see Migration20260610120000:
@@ -1,6 +1,7 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import type { EnricherContext, ResponseEnricher } from '@open-mercato/shared/lib/crud/response-enricher'
3
3
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
+ import { sanitizeChannelHtml } from '../lib/sanitize-channel-html'
4
5
  import {
5
6
  CommunicationChannel,
6
7
  ExternalConversation,
@@ -63,6 +64,12 @@ export type ChannelPayloadEnrichment = {
63
64
  channelPayload: Record<string, unknown> | null
64
65
  interactiveState: Record<string, unknown> | null
65
66
  channelMetadata: Record<string, unknown> | null
67
+ /**
68
+ * Server-sanitized email HTML, safe for `dangerouslySetInnerHTML`. Populated
69
+ * for `email/*` payloads that carry an html body, `null` otherwise. Sanitizing
70
+ * here keeps `sanitize-html` off the client render path.
71
+ */
72
+ sanitizedHtml: string | null
66
73
  }
67
74
 
68
75
  export type ChannelContactEnrichment = {
@@ -71,6 +78,23 @@ export type ChannelContactEnrichment = {
71
78
  subject: string | null
72
79
  }
73
80
 
81
+ /**
82
+ * Sanitize channel-supplied email HTML on the server so the client widget never
83
+ * has to pull `sanitize-html` into the browser bundle or run sanitization on the
84
+ * render path. Returns the sanitized HTML for `email/*` payloads that carry an
85
+ * html body, and `null` otherwise. The raw `channelPayload` is left untouched so
86
+ * provider-package widget overrides keep their existing data contract.
87
+ */
88
+ function sanitizeEmailPayloadHtml(
89
+ contentType: string | null | undefined,
90
+ payload: Record<string, unknown> | null | undefined,
91
+ ): string | null {
92
+ if (!contentType || !contentType.startsWith('email/')) return null
93
+ const html = payload?.html
94
+ if (typeof html !== 'string' || html.length === 0) return null
95
+ return sanitizeChannelHtml(html)
96
+ }
97
+
74
98
  // ── _channel + _channelPayload + _channelContact ──────────────────────────────
75
99
 
76
100
  const messageChannelEnricher: ResponseEnricher<
@@ -196,6 +220,7 @@ const messageChannelEnricher: ResponseEnricher<
196
220
  channelPayload: link.channelPayload ?? null,
197
221
  interactiveState: link.interactiveState ?? null,
198
222
  channelMetadata: link.channelMetadata ?? null,
223
+ sanitizedHtml: sanitizeEmailPayloadHtml(link.channelContentType, link.channelPayload),
199
224
  }
200
225
 
201
226
  const conversation = link.externalConversationId