@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +7 -8
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +4 -3
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +7 -8
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +4 -3
- 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: "
|
|
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: "
|
|
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": ";
|
|
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": ";
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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": ";
|
|
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.
|
|
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.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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
|