@open-mercato/core 0.6.6-develop.6416.1.888fffc007 → 0.6.6-develop.6422.1.b3d024f05c
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/messages/api/[id]/actions/[actionId]/route.js +5 -1
- package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +25 -2
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/openapi.js +1 -0
- package/dist/modules/messages/api/openapi.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +8 -0
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +50 -24
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js +1 -0
- package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js.map +2 -2
- package/dist/modules/messages/lib/constants.js +3 -1
- package/dist/modules/messages/lib/constants.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +56 -24
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/route.js +3 -28
- package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets/timeEntryListFilters.js +44 -0
- package/dist/modules/staff/lib/timesheets/timeEntryListFilters.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +1 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +1 -2
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +1 -6
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +6 -6
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +5 -1
- package/src/modules/messages/api/[id]/route.ts +30 -1
- package/src/modules/messages/api/openapi.ts +1 -0
- package/src/modules/messages/commands/actions.ts +14 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +53 -28
- package/src/modules/messages/components/message-detail/panels/composer-dialogs.tsx +1 -0
- package/src/modules/messages/components/message-detail/types.ts +1 -0
- package/src/modules/messages/lib/constants.ts +4 -0
- package/src/modules/progress/AGENTS.md +6 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +85 -29
- package/src/modules/staff/api/timesheets/time-entries/route.ts +3 -32
- package/src/modules/staff/lib/timesheets/timeEntryListFilters.ts +64 -0
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +1 -2
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +2 -4
- package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +1 -7
- package/src/modules/sync_excel/lib/adapters/customers.ts +6 -6
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
4
|
+
import { apiCall, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
|
|
5
|
+
import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
6
|
+
import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
|
|
4
7
|
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
5
|
-
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
6
8
|
import { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from "../utils.js";
|
|
7
9
|
function toStateChangeErrorMessage(payload, t) {
|
|
8
10
|
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
@@ -73,20 +75,33 @@ function useMessageDetailsActions({
|
|
|
73
75
|
setUpdatingState(true);
|
|
74
76
|
try {
|
|
75
77
|
await runMutation({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// Carry the OSS optimistic-lock header so a stale tab cannot delete a
|
|
79
|
+
// concurrently-changed message; throw a status-carrying error on failure
|
|
80
|
+
// so the 409 conflict surfaces on the shared RecordConflictBanner.
|
|
81
|
+
operation: () => withScopedApiRequestHeaders(
|
|
82
|
+
buildOptimisticLockHeader(detail?.updatedAt),
|
|
83
|
+
async () => {
|
|
84
|
+
const call = await apiCall(`/api/messages/${encodeURIComponent(id)}`, {
|
|
85
|
+
method: "DELETE"
|
|
86
|
+
});
|
|
87
|
+
if (!call.ok) {
|
|
88
|
+
throw Object.assign(
|
|
89
|
+
new Error(toErrorMessage(call.result) ?? t("messages.errors.deleteFailed", "Failed to delete message.")),
|
|
90
|
+
{
|
|
91
|
+
status: call.status,
|
|
92
|
+
...call.result && typeof call.result === "object" ? call.result : {}
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
82
96
|
}
|
|
83
|
-
|
|
97
|
+
),
|
|
84
98
|
context: { resourceKind: "message", messageId: id, action: "delete", retryLastMutation },
|
|
85
99
|
mutationPayload: { messageId: id, action: "delete" }
|
|
86
100
|
});
|
|
87
101
|
flash(t("messages.flash.deleted", "Message deleted."), "success");
|
|
88
102
|
onDeleted();
|
|
89
103
|
} catch (err) {
|
|
104
|
+
if (surfaceRecordConflict(err, t)) return;
|
|
90
105
|
flash(
|
|
91
106
|
err instanceof Error ? err.message : t("messages.errors.deleteFailed", "Failed to delete message."),
|
|
92
107
|
"error"
|
|
@@ -94,7 +109,7 @@ function useMessageDetailsActions({
|
|
|
94
109
|
} finally {
|
|
95
110
|
setUpdatingState(false);
|
|
96
111
|
}
|
|
97
|
-
}, [id, onDeleted, retryLastMutation, runMutation, t]);
|
|
112
|
+
}, [detail?.updatedAt, id, onDeleted, retryLastMutation, runMutation, t]);
|
|
98
113
|
const runConversationAction = React.useCallback(async (action, options) => {
|
|
99
114
|
setActiveConversationAction(action);
|
|
100
115
|
try {
|
|
@@ -187,22 +202,32 @@ function useMessageDetailsActions({
|
|
|
187
202
|
setExecutingActionId(action.id);
|
|
188
203
|
try {
|
|
189
204
|
const call = await runMutation({
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
// Carry the OSS optimistic-lock header so a stale tab cannot execute an
|
|
206
|
+
// action against a concurrently-changed message; throw a status-carrying
|
|
207
|
+
// error on failure so the 409 conflict surfaces on the shared banner.
|
|
208
|
+
operation: () => withScopedApiRequestHeaders(
|
|
209
|
+
buildOptimisticLockHeader(detail?.updatedAt),
|
|
210
|
+
async () => {
|
|
211
|
+
const result = await apiCall(
|
|
212
|
+
`/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
|
|
213
|
+
{
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify(payload ?? {})
|
|
217
|
+
}
|
|
202
218
|
);
|
|
219
|
+
if (!result.ok) {
|
|
220
|
+
throw Object.assign(
|
|
221
|
+
new Error(toErrorMessage(result.result) ?? t("messages.errors.actionFailed", "Failed to execute action.")),
|
|
222
|
+
{
|
|
223
|
+
status: result.status,
|
|
224
|
+
...result.result && typeof result.result === "object" ? result.result : {}
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
203
229
|
}
|
|
204
|
-
|
|
205
|
-
},
|
|
230
|
+
),
|
|
206
231
|
context: { resourceKind: "message", messageId: id, action: "execute-action", retryLastMutation },
|
|
207
232
|
mutationPayload: { messageId: id, action: "execute-action", actionId: action.id }
|
|
208
233
|
});
|
|
@@ -214,6 +239,7 @@ function useMessageDetailsActions({
|
|
|
214
239
|
}
|
|
215
240
|
await detailQuery.refetch();
|
|
216
241
|
} catch (err) {
|
|
242
|
+
if (surfaceRecordConflict(err, t)) return;
|
|
217
243
|
flash(
|
|
218
244
|
err instanceof Error ? err.message : t("messages.errors.actionFailed", "Failed to execute action."),
|
|
219
245
|
"error"
|
|
@@ -221,7 +247,7 @@ function useMessageDetailsActions({
|
|
|
221
247
|
} finally {
|
|
222
248
|
setExecutingActionId(null);
|
|
223
249
|
}
|
|
224
|
-
}, [detailQuery, id, retryLastMutation, runMutation, t]);
|
|
250
|
+
}, [detail?.updatedAt, detailQuery, id, retryLastMutation, runMutation, t]);
|
|
225
251
|
const handleExecuteAction = React.useCallback(async (action, payload) => {
|
|
226
252
|
if (executingActionId || detail?.actionTaken) return;
|
|
227
253
|
if (action.confirmRequired) {
|
package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { UseQueryResult } from '@tanstack/react-query'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport type {\n ActionResult,\n MessageAction,\n MessageAttachment,\n MessageDetail,\n PendingActionConfirmation,\n} from '../types'\nimport { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../utils'\n\ntype RequestAndRefreshOptions = {\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n}\n\ntype ConversationActionKind =\n | 'archiveConversation'\n | 'unarchiveConversation'\n | 'deleteConversation'\n | 'markAllUnread'\n | 'markAllRead'\n\ntype MessageDetailMutationContext = {\n resourceKind: 'message' | 'conversation'\n messageId: string\n action: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessageDetailsActionsInput = {\n id: string\n t: TranslateFn\n detail: MessageDetail | null\n detailQuery: UseQueryResult<MessageDetail | null, Error>\n attachments: MessageAttachment[] | undefined\n isArchived: boolean\n onDeleted: () => void\n onMarkedUnread: () => void\n refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n suppressAutoMarkRead: () => void\n}\n\nfunction toStateChangeErrorMessage(payload: unknown, t: TranslateFn): string | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n const code = (payload as Record<string, unknown>).code\n if (code === 'messages_sender_archive_unsupported') {\n return t(\n 'messages.errors.archiveSenderMessageUnsupported',\n 'You cannot archive messages you sent.',\n )\n }\n }\n return toErrorMessage(payload)\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n onMarkedUnread,\n refreshDetailWithoutAutoMarkRead,\n suppressAutoMarkRead,\n}: UseMessageDetailsActionsInput) {\n const [replyOpen, setReplyOpen] = React.useState(false)\n const [forwardOpen, setForwardOpen] = React.useState(false)\n const [editOpen, setEditOpen] = React.useState(false)\n const [updatingState, setUpdatingState] = React.useState(false)\n const [executingActionId, setExecutingActionId] = React.useState<string | null>(null)\n const [pendingActionConfirmation, setPendingActionConfirmation] = React.useState<PendingActionConfirmation | null>(null)\n const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)\n const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({\n contextId: 'messages-detail-actions',\n })\n\n const requestAndRefresh = React.useCallback(async (\n url: string,\n method: 'PUT' | 'DELETE',\n options?: RequestAndRefreshOptions,\n ) => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(url, { method })\n if (!call.ok) {\n throw new Error(\n toStateChangeErrorMessage(call.result, t)\n ?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n )\n }\n if (options?.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },\n mutationPayload: { messageId: id, action: 'state-change', url, method },\n })\n options?.onSuccess?.()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const handleDelete = React.useCallback(async () => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n })\n if (!call.ok) {\n throw new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.'))\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },\n mutationPayload: { messageId: id, action: 'delete' },\n })\n flash(t('messages.flash.deleted', 'Message deleted.'), 'success')\n onDeleted()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.deleteFailed', 'Failed to delete message.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [id, onDeleted, retryLastMutation, runMutation, t])\n\n const runConversationAction = React.useCallback(async (\n action: ConversationActionKind,\n options: {\n url: string\n method: 'PUT' | 'DELETE'\n successMessage: string\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n },\n ) => {\n setActiveConversationAction(action)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n )\n }\n },\n context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },\n mutationPayload: { messageId: id, action, url: options.url, method: options.method },\n })\n\n flash(options.successMessage, 'success')\n\n if (options.onSuccess) {\n options.onSuccess()\n return\n }\n\n if (options.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n 'error',\n )\n } finally {\n setActiveConversationAction(null)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const archiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('archiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationArchived', 'Conversation archived.'),\n })\n }, [id, runConversationAction, t])\n\n const unarchiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('unarchiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationUnarchived', 'Conversation restored from archive.'),\n })\n }, [id, runConversationAction, t])\n\n const markConversationUnread = React.useCallback(async (messageId?: string) => {\n suppressAutoMarkRead()\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllUnread', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),\n onSuccess: onMarkedUnread,\n })\n }, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])\n\n const markConversationRead = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllRead', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationMarkedRead', 'Conversation marked read.'),\n })\n }, [id, runConversationAction, t])\n\n const deleteConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('deleteConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationDeleted', 'Conversation deleted.'),\n onSuccess: onDeleted,\n })\n }, [id, onDeleted, runConversationAction, t])\n\n const handleDeleteDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleDelete()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setDeleteConfirmationOpen(false)\n }\n }, [handleDelete])\n\n const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n setExecutingActionId(action.id)\n try {\n const call = await runMutation({\n operation: async () => {\n const result = await apiCall<ActionResult>(\n `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload ?? {}),\n },\n )\n\n if (!result.ok) {\n throw new Error(\n toErrorMessage(result.result)\n ?? t('messages.errors.actionFailed', 'Failed to execute action.'),\n )\n }\n\n return result\n },\n context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },\n mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },\n })\n\n flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')\n\n const redirectTarget = call.result?.result?.redirect\n if (\n typeof redirectTarget === 'string'\n && redirectTarget.trim().length > 0\n && isSafeNavigationHref(redirectTarget)\n ) {\n window.location.href = redirectTarget\n return\n }\n\n await detailQuery.refetch()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.actionFailed', 'Failed to execute action.'),\n 'error',\n )\n } finally {\n setExecutingActionId(null)\n }\n }, [detailQuery, id, retryLastMutation, runMutation, t])\n\n const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n if (executingActionId || detail?.actionTaken) return\n\n if (action.confirmRequired) {\n setPendingActionConfirmation({ action, payload })\n return\n }\n\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId])\n\n const handleConfirmPendingAction = React.useCallback(async () => {\n if (!pendingActionConfirmation || executingActionId || detail?.actionTaken) return\n const { action, payload } = pendingActionConfirmation\n setPendingActionConfirmation(null)\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId, pendingActionConfirmation])\n\n const handleActionConfirmDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleConfirmPendingAction()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setPendingActionConfirmation(null)\n }\n }, [handleConfirmPendingAction])\n\n const contentProps = React.useMemo<MessageContentProps>(() => ({\n message: {\n id: detail?.id ?? '',\n type: detail?.type ?? 'default',\n subject: detail?.subject ?? '',\n body: detail?.body ?? '',\n bodyFormat: detail?.bodyFormat ?? 'text',\n priority: (detail?.priority ?? 'normal') as 'low' | 'normal' | 'high' | 'urgent',\n sentAt: detail?.sentAt ? new Date(detail.sentAt) : null,\n senderName: detail?.senderName ?? undefined,\n senderUserId: detail?.senderUserId ?? '',\n actionData: detail?.actionData ?? null,\n actionTaken: detail?.actionTaken ?? null,\n actionTakenAt: detail?.actionTakenAt ? new Date(detail.actionTakenAt) : null,\n },\n objects: (detail?.objects ?? []).map((item) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n snapshot: item.snapshot ?? undefined,\n })),\n attachments: (attachments ?? []).map((item) => ({\n id: item.id,\n fileName: item.fileName,\n fileSize: item.fileSize,\n mimeType: item.mimeType,\n url: item.url,\n })),\n }), [attachments, detail])\n\n const handleExecuteActionById = React.useCallback<MessageActionsProps['onExecuteAction']>(async (actionId, payload) => {\n const action = detail?.actionData?.actions?.find((entry) => {\n if (parseObjectActionId(entry.id)) return false\n return entry.id === actionId\n })\n if (!action) return\n await handleExecuteAction(action, payload)\n }, [detail?.actionData?.actions, handleExecuteAction])\n\n const messageActions = React.useMemo(() => {\n return (detail?.actionData?.actions ?? []).filter((action) => !parseObjectActionId(action.id))\n }, [detail?.actionData?.actions])\n\n const objectActionsByObjectId = React.useMemo(() => {\n const byObjectId = new Map<string, Map<string, MessageAction>>()\n for (const action of detail?.actionData?.actions ?? []) {\n const parsed = parseObjectActionId(action.id)\n if (!parsed) continue\n const current = byObjectId.get(parsed.objectId) ?? new Map<string, MessageAction>()\n current.set(parsed.actionId, action)\n byObjectId.set(parsed.objectId, current)\n }\n return byObjectId\n }, [detail?.actionData?.actions])\n\n const toggleRead = React.useCallback(async () => {\n if (detail?.isRead) {\n suppressAutoMarkRead()\n }\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,\n )\n }, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])\n\n const toggleArchive = React.useCallback(async () => {\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/archive`,\n isArchived ? 'DELETE' : 'PUT',\n )\n }, [id, isArchived, requestAndRefresh])\n\n return {\n replyOpen,\n setReplyOpen,\n forwardOpen,\n setForwardOpen,\n editOpen,\n setEditOpen,\n updatingState,\n executingActionId,\n pendingActionConfirmation,\n setPendingActionConfirmation,\n deleteConfirmationOpen,\n setDeleteConfirmationOpen,\n activeConversationAction,\n toggleRead,\n toggleArchive,\n archiveConversation,\n unarchiveConversation,\n deleteConversation,\n markConversationUnread,\n markConversationRead,\n requestAndRefresh,\n handleDelete,\n handleDeleteDialogKeyDown,\n handleExecuteAction,\n handleConfirmPendingAction,\n handleActionConfirmDialogKeyDown,\n contentProps,\n handleExecuteActionById,\n messageActions,\n objectActionsByObjectId,\n }\n}\n"],
|
|
5
|
-
"mappings": ";AAEA,YAAY,WAAW;AAIvB,SAAS,aAAa;AACtB,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { UseQueryResult } from '@tanstack/react-query'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\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 { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport type {\n ActionResult,\n MessageAction,\n MessageAttachment,\n MessageDetail,\n PendingActionConfirmation,\n} from '../types'\nimport { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../utils'\n\ntype RequestAndRefreshOptions = {\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n}\n\ntype ConversationActionKind =\n | 'archiveConversation'\n | 'unarchiveConversation'\n | 'deleteConversation'\n | 'markAllUnread'\n | 'markAllRead'\n\ntype MessageDetailMutationContext = {\n resourceKind: 'message' | 'conversation'\n messageId: string\n action: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessageDetailsActionsInput = {\n id: string\n t: TranslateFn\n detail: MessageDetail | null\n detailQuery: UseQueryResult<MessageDetail | null, Error>\n attachments: MessageAttachment[] | undefined\n isArchived: boolean\n onDeleted: () => void\n onMarkedUnread: () => void\n refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n suppressAutoMarkRead: () => void\n}\n\nfunction toStateChangeErrorMessage(payload: unknown, t: TranslateFn): string | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n const code = (payload as Record<string, unknown>).code\n if (code === 'messages_sender_archive_unsupported') {\n return t(\n 'messages.errors.archiveSenderMessageUnsupported',\n 'You cannot archive messages you sent.',\n )\n }\n }\n return toErrorMessage(payload)\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n onMarkedUnread,\n refreshDetailWithoutAutoMarkRead,\n suppressAutoMarkRead,\n}: UseMessageDetailsActionsInput) {\n const [replyOpen, setReplyOpen] = React.useState(false)\n const [forwardOpen, setForwardOpen] = React.useState(false)\n const [editOpen, setEditOpen] = React.useState(false)\n const [updatingState, setUpdatingState] = React.useState(false)\n const [executingActionId, setExecutingActionId] = React.useState<string | null>(null)\n const [pendingActionConfirmation, setPendingActionConfirmation] = React.useState<PendingActionConfirmation | null>(null)\n const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)\n const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({\n contextId: 'messages-detail-actions',\n })\n\n const requestAndRefresh = React.useCallback(async (\n url: string,\n method: 'PUT' | 'DELETE',\n options?: RequestAndRefreshOptions,\n ) => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(url, { method })\n if (!call.ok) {\n throw new Error(\n toStateChangeErrorMessage(call.result, t)\n ?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n )\n }\n if (options?.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },\n mutationPayload: { messageId: id, action: 'state-change', url, method },\n })\n options?.onSuccess?.()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const handleDelete = React.useCallback(async () => {\n setUpdatingState(true)\n try {\n await runMutation({\n // Carry the OSS optimistic-lock header so a stale tab cannot delete a\n // concurrently-changed message; throw a status-carrying error on failure\n // so the 409 conflict surfaces on the shared RecordConflictBanner.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(detail?.updatedAt),\n async () => {\n const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n })\n if (!call.ok) {\n throw Object.assign(\n new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.')),\n {\n status: call.status,\n ...(call.result && typeof call.result === 'object' ? call.result : {}),\n },\n )\n }\n },\n ),\n context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },\n mutationPayload: { messageId: id, action: 'delete' },\n })\n flash(t('messages.flash.deleted', 'Message deleted.'), 'success')\n onDeleted()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.deleteFailed', 'Failed to delete message.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detail?.updatedAt, id, onDeleted, retryLastMutation, runMutation, t])\n\n const runConversationAction = React.useCallback(async (\n action: ConversationActionKind,\n options: {\n url: string\n method: 'PUT' | 'DELETE'\n successMessage: string\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n },\n ) => {\n setActiveConversationAction(action)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n )\n }\n },\n context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },\n mutationPayload: { messageId: id, action, url: options.url, method: options.method },\n })\n\n flash(options.successMessage, 'success')\n\n if (options.onSuccess) {\n options.onSuccess()\n return\n }\n\n if (options.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n 'error',\n )\n } finally {\n setActiveConversationAction(null)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const archiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('archiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationArchived', 'Conversation archived.'),\n })\n }, [id, runConversationAction, t])\n\n const unarchiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('unarchiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationUnarchived', 'Conversation restored from archive.'),\n })\n }, [id, runConversationAction, t])\n\n const markConversationUnread = React.useCallback(async (messageId?: string) => {\n suppressAutoMarkRead()\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllUnread', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),\n onSuccess: onMarkedUnread,\n })\n }, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])\n\n const markConversationRead = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllRead', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationMarkedRead', 'Conversation marked read.'),\n })\n }, [id, runConversationAction, t])\n\n const deleteConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('deleteConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationDeleted', 'Conversation deleted.'),\n onSuccess: onDeleted,\n })\n }, [id, onDeleted, runConversationAction, t])\n\n const handleDeleteDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleDelete()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setDeleteConfirmationOpen(false)\n }\n }, [handleDelete])\n\n const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n setExecutingActionId(action.id)\n try {\n const call = await runMutation({\n // Carry the OSS optimistic-lock header so a stale tab cannot execute an\n // action against a concurrently-changed message; throw a status-carrying\n // error on failure so the 409 conflict surfaces on the shared banner.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(detail?.updatedAt),\n async () => {\n const result = await apiCall<ActionResult>(\n `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload ?? {}),\n },\n )\n\n if (!result.ok) {\n throw Object.assign(\n new Error(toErrorMessage(result.result) ?? t('messages.errors.actionFailed', 'Failed to execute action.')),\n {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n },\n )\n }\n\n return result\n },\n ),\n context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },\n mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },\n })\n\n flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')\n\n const redirectTarget = call.result?.result?.redirect\n if (\n typeof redirectTarget === 'string'\n && redirectTarget.trim().length > 0\n && isSafeNavigationHref(redirectTarget)\n ) {\n window.location.href = redirectTarget\n return\n }\n\n await detailQuery.refetch()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.actionFailed', 'Failed to execute action.'),\n 'error',\n )\n } finally {\n setExecutingActionId(null)\n }\n }, [detail?.updatedAt, detailQuery, id, retryLastMutation, runMutation, t])\n\n const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n if (executingActionId || detail?.actionTaken) return\n\n if (action.confirmRequired) {\n setPendingActionConfirmation({ action, payload })\n return\n }\n\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId])\n\n const handleConfirmPendingAction = React.useCallback(async () => {\n if (!pendingActionConfirmation || executingActionId || detail?.actionTaken) return\n const { action, payload } = pendingActionConfirmation\n setPendingActionConfirmation(null)\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId, pendingActionConfirmation])\n\n const handleActionConfirmDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleConfirmPendingAction()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setPendingActionConfirmation(null)\n }\n }, [handleConfirmPendingAction])\n\n const contentProps = React.useMemo<MessageContentProps>(() => ({\n message: {\n id: detail?.id ?? '',\n type: detail?.type ?? 'default',\n subject: detail?.subject ?? '',\n body: detail?.body ?? '',\n bodyFormat: detail?.bodyFormat ?? 'text',\n priority: (detail?.priority ?? 'normal') as 'low' | 'normal' | 'high' | 'urgent',\n sentAt: detail?.sentAt ? new Date(detail.sentAt) : null,\n senderName: detail?.senderName ?? undefined,\n senderUserId: detail?.senderUserId ?? '',\n actionData: detail?.actionData ?? null,\n actionTaken: detail?.actionTaken ?? null,\n actionTakenAt: detail?.actionTakenAt ? new Date(detail.actionTakenAt) : null,\n },\n objects: (detail?.objects ?? []).map((item) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n snapshot: item.snapshot ?? undefined,\n })),\n attachments: (attachments ?? []).map((item) => ({\n id: item.id,\n fileName: item.fileName,\n fileSize: item.fileSize,\n mimeType: item.mimeType,\n url: item.url,\n })),\n }), [attachments, detail])\n\n const handleExecuteActionById = React.useCallback<MessageActionsProps['onExecuteAction']>(async (actionId, payload) => {\n const action = detail?.actionData?.actions?.find((entry) => {\n if (parseObjectActionId(entry.id)) return false\n return entry.id === actionId\n })\n if (!action) return\n await handleExecuteAction(action, payload)\n }, [detail?.actionData?.actions, handleExecuteAction])\n\n const messageActions = React.useMemo(() => {\n return (detail?.actionData?.actions ?? []).filter((action) => !parseObjectActionId(action.id))\n }, [detail?.actionData?.actions])\n\n const objectActionsByObjectId = React.useMemo(() => {\n const byObjectId = new Map<string, Map<string, MessageAction>>()\n for (const action of detail?.actionData?.actions ?? []) {\n const parsed = parseObjectActionId(action.id)\n if (!parsed) continue\n const current = byObjectId.get(parsed.objectId) ?? new Map<string, MessageAction>()\n current.set(parsed.actionId, action)\n byObjectId.set(parsed.objectId, current)\n }\n return byObjectId\n }, [detail?.actionData?.actions])\n\n const toggleRead = React.useCallback(async () => {\n if (detail?.isRead) {\n suppressAutoMarkRead()\n }\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,\n )\n }, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])\n\n const toggleArchive = React.useCallback(async () => {\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/archive`,\n isArchived ? 'DELETE' : 'PUT',\n )\n }, [id, isArchived, requestAndRefresh])\n\n return {\n replyOpen,\n setReplyOpen,\n forwardOpen,\n setForwardOpen,\n editOpen,\n setEditOpen,\n updatingState,\n executingActionId,\n pendingActionConfirmation,\n setPendingActionConfirmation,\n deleteConfirmationOpen,\n setDeleteConfirmationOpen,\n activeConversationAction,\n toggleRead,\n toggleArchive,\n archiveConversation,\n unarchiveConversation,\n deleteConversation,\n markConversationUnread,\n markConversationRead,\n requestAndRefresh,\n handleDelete,\n handleDeleteDialogKeyDown,\n handleExecuteAction,\n handleConfirmPendingAction,\n handleActionConfirmDialogKeyDown,\n contentProps,\n handleExecuteActionById,\n messageActions,\n objectActionsByObjectId,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,6 +7,7 @@ function MessageDetailComposerDialogs(props) {
|
|
|
7
7
|
{
|
|
8
8
|
variant: "compose",
|
|
9
9
|
messageId: props.id,
|
|
10
|
+
expectedUpdatedAt: props.detail.updatedAt,
|
|
10
11
|
open: props.editOpen && props.detail.isDraft && props.detail.canEditDraft,
|
|
11
12
|
onOpenChange: props.setEditOpen,
|
|
12
13
|
defaultValues: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/messages/components/message-detail/panels/composer-dialogs.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport type { MessageAttachment, MessageDetail } from '../types'\n\ntype ComposerDialogsProps = {\n id: string\n detail: MessageDetail\n attachments: MessageAttachment[] | undefined\n editOpen: boolean\n setEditOpen: (value: boolean) => void\n onRefresh: () => Promise<unknown>\n}\n\nexport function MessageDetailComposerDialogs(props: ComposerDialogsProps) {\n return (\n <MessageComposer\n variant=\"compose\"\n messageId={props.id}\n open={props.editOpen && props.detail.isDraft && props.detail.canEditDraft}\n onOpenChange={props.setEditOpen}\n defaultValues={{\n type: props.detail.type,\n recipients: props.detail.recipients.map((recipient) => recipient.userId),\n subject: props.detail.subject,\n body: props.detail.body,\n bodyFormat: props.detail.bodyFormat,\n priority: props.detail.priority as 'low' | 'normal' | 'high' | 'urgent',\n visibility: props.detail.visibility ?? 'internal',\n sourceEntityType: props.detail.sourceEntityType ?? null,\n sourceEntityId: props.detail.sourceEntityId ?? null,\n externalEmail: props.detail.externalEmail ?? null,\n externalName: props.detail.externalName ?? null,\n attachmentIds: props.attachments?.map((attachment) => attachment.id) ?? [],\n }}\n onSuccess={() => {\n props.setEditOpen(false)\n void props.onRefresh()\n }}\n />\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAgBI;AAdJ,SAAS,uBAAuB;AAYzB,SAAS,6BAA6B,OAA6B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,OAAO;AAAA,MAC7D,cAAc,MAAM;AAAA,MACpB,eAAe;AAAA,QACb,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO,WAAW,IAAI,CAAC,cAAc,UAAU,MAAM;AAAA,QACvE,SAAS,MAAM,OAAO;AAAA,QACtB,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO;AAAA,QACzB,UAAU,MAAM,OAAO;AAAA,QACvB,YAAY,MAAM,OAAO,cAAc;AAAA,QACvC,kBAAkB,MAAM,OAAO,oBAAoB;AAAA,QACnD,gBAAgB,MAAM,OAAO,kBAAkB;AAAA,QAC/C,eAAe,MAAM,OAAO,iBAAiB;AAAA,QAC7C,cAAc,MAAM,OAAO,gBAAgB;AAAA,QAC3C,eAAe,MAAM,aAAa,IAAI,CAAC,eAAe,WAAW,EAAE,KAAK,CAAC;AAAA,MAC3E;AAAA,MACA,WAAW,MAAM;AACf,cAAM,YAAY,KAAK;AACvB,aAAK,MAAM,UAAU;AAAA,MACvB;AAAA;AAAA,EACF;AAEJ;",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport type { MessageAttachment, MessageDetail } from '../types'\n\ntype ComposerDialogsProps = {\n id: string\n detail: MessageDetail\n attachments: MessageAttachment[] | undefined\n editOpen: boolean\n setEditOpen: (value: boolean) => void\n onRefresh: () => Promise<unknown>\n}\n\nexport function MessageDetailComposerDialogs(props: ComposerDialogsProps) {\n return (\n <MessageComposer\n variant=\"compose\"\n messageId={props.id}\n expectedUpdatedAt={props.detail.updatedAt}\n open={props.editOpen && props.detail.isDraft && props.detail.canEditDraft}\n onOpenChange={props.setEditOpen}\n defaultValues={{\n type: props.detail.type,\n recipients: props.detail.recipients.map((recipient) => recipient.userId),\n subject: props.detail.subject,\n body: props.detail.body,\n bodyFormat: props.detail.bodyFormat,\n priority: props.detail.priority as 'low' | 'normal' | 'high' | 'urgent',\n visibility: props.detail.visibility ?? 'internal',\n sourceEntityType: props.detail.sourceEntityType ?? null,\n sourceEntityId: props.detail.sourceEntityId ?? null,\n externalEmail: props.detail.externalEmail ?? null,\n externalName: props.detail.externalName ?? null,\n attachmentIds: props.attachments?.map((attachment) => attachment.id) ?? [],\n }}\n onSuccess={() => {\n props.setEditOpen(false)\n void props.onRefresh()\n }}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAgBI;AAdJ,SAAS,uBAAuB;AAYzB,SAAS,6BAA6B,OAA6B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,mBAAmB,MAAM,OAAO;AAAA,MAChC,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,OAAO;AAAA,MAC7D,cAAc,MAAM;AAAA,MACpB,eAAe;AAAA,QACb,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO,WAAW,IAAI,CAAC,cAAc,UAAU,MAAM;AAAA,QACvE,SAAS,MAAM,OAAO;AAAA,QACtB,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO;AAAA,QACzB,UAAU,MAAM,OAAO;AAAA,QACvB,YAAY,MAAM,OAAO,cAAc;AAAA,QACvC,kBAAkB,MAAM,OAAO,oBAAoB;AAAA,QACnD,gBAAgB,MAAM,OAAO,kBAAkB;AAAA,QAC/C,eAAe,MAAM,OAAO,iBAAiB;AAAA,QAC7C,cAAc,MAAM,OAAO,gBAAgB;AAAA,QAC3C,eAAe,MAAM,aAAa,IAAI,CAAC,eAAe,WAAW,EAAE,KAAK,CAAC;AAAA,MAC3E;AAAA,MACA,WAAW,MAAM;AACf,cAAM,YAAY,KAAK;AACvB,aAAK,MAAM,UAAU;AAAA,MACvB;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const MESSAGE_ENTITY_ID = "messages:message";
|
|
2
2
|
const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID;
|
|
3
3
|
const MESSAGE_ATTACHMENT_PARTITION = "messages";
|
|
4
|
+
const MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND = "messages.message";
|
|
4
5
|
export {
|
|
5
6
|
MESSAGE_ATTACHMENT_ENTITY_ID,
|
|
6
7
|
MESSAGE_ATTACHMENT_PARTITION,
|
|
7
|
-
MESSAGE_ENTITY_ID
|
|
8
|
+
MESSAGE_ENTITY_ID,
|
|
9
|
+
MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND
|
|
8
10
|
};
|
|
9
11
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/lib/constants.ts"],
|
|
4
|
-
"sourcesContent": ["export const MESSAGE_ENTITY_ID = 'messages:message'\nexport const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID\nexport const MESSAGE_ATTACHMENT_PARTITION = 'messages'\n"],
|
|
5
|
-
"mappings": "AAAO,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,+BAA+B;",
|
|
4
|
+
"sourcesContent": ["export const MESSAGE_ENTITY_ID = 'messages:message'\nexport const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID\nexport const MESSAGE_ATTACHMENT_PARTITION = 'messages'\n\n// Resource kind used by the command-level OSS optimistic-lock guard so stale\n// draft edits and message actions fail with the structured 409 conflict.\nexport const MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND = 'messages.message'\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,+BAA+B;AAIrC,MAAM,wCAAwC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,14 @@ import { ProgressJob } from "../data/entities.js";
|
|
|
2
2
|
import { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from "./progressService.js";
|
|
3
3
|
import { PROGRESS_EVENTS } from "./events.js";
|
|
4
4
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
|
+
const DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250;
|
|
6
|
+
function resolveBroadcastMinIntervalMs() {
|
|
7
|
+
const raw = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS;
|
|
8
|
+
if (raw == null || raw.trim() === "") return DEFAULT_BROADCAST_MIN_INTERVAL_MS;
|
|
9
|
+
const parsed = Number.parseInt(raw, 10);
|
|
10
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS;
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
5
13
|
function buildJobPayload(job) {
|
|
6
14
|
return {
|
|
7
15
|
jobId: job.id,
|
|
@@ -20,6 +28,44 @@ function buildJobPayload(job) {
|
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
function createProgressService(em, eventBus) {
|
|
31
|
+
const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs();
|
|
32
|
+
const jobUpdateThrottle = /* @__PURE__ */ new Map();
|
|
33
|
+
function jobScopeFilter(jobId, ctx) {
|
|
34
|
+
return {
|
|
35
|
+
id: jobId,
|
|
36
|
+
tenantId: ctx.tenantId,
|
|
37
|
+
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function loadUpdatableJob(jobId, ctx) {
|
|
41
|
+
const cached = jobUpdateThrottle.get(jobId);
|
|
42
|
+
if (cached) return cached.job;
|
|
43
|
+
return em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx));
|
|
44
|
+
}
|
|
45
|
+
async function commitJobUpdate(job, ctx) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const cached = jobUpdateThrottle.get(job.id);
|
|
48
|
+
const shouldBroadcast = broadcastMinIntervalMs <= 0 || cached == null || now - cached.lastBroadcastAt >= broadcastMinIntervalMs || Math.abs(job.progressPercent - cached.lastBroadcastPercent) >= 1;
|
|
49
|
+
if (shouldBroadcast) {
|
|
50
|
+
await em.flush();
|
|
51
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
52
|
+
...buildJobPayload(job),
|
|
53
|
+
tenantId: ctx.tenantId,
|
|
54
|
+
organizationId: job.organizationId ?? null
|
|
55
|
+
});
|
|
56
|
+
jobUpdateThrottle.set(job.id, { job, lastBroadcastAt: now, lastBroadcastPercent: job.progressPercent });
|
|
57
|
+
} else {
|
|
58
|
+
jobUpdateThrottle.set(job.id, {
|
|
59
|
+
job,
|
|
60
|
+
lastBroadcastAt: cached.lastBroadcastAt,
|
|
61
|
+
lastBroadcastPercent: cached.lastBroadcastPercent
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return job;
|
|
65
|
+
}
|
|
66
|
+
function forgetJobThrottle(jobId) {
|
|
67
|
+
jobUpdateThrottle.delete(jobId);
|
|
68
|
+
}
|
|
23
69
|
return {
|
|
24
70
|
async createJob(input, ctx) {
|
|
25
71
|
const job = em.create(ProgressJob, {
|
|
@@ -66,12 +112,9 @@ function createProgressService(em, eventBus) {
|
|
|
66
112
|
return job;
|
|
67
113
|
},
|
|
68
114
|
async updateProgress(jobId, input, ctx) {
|
|
69
|
-
const job = await
|
|
70
|
-
id: jobId,
|
|
71
|
-
tenantId: ctx.tenantId,
|
|
72
|
-
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
73
|
-
});
|
|
115
|
+
const job = await loadUpdatableJob(jobId, ctx);
|
|
74
116
|
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
117
|
+
forgetJobThrottle(jobId);
|
|
75
118
|
return job;
|
|
76
119
|
}
|
|
77
120
|
if (input.processedCount !== void 0) {
|
|
@@ -94,21 +137,12 @@ function createProgressService(em, eventBus) {
|
|
|
94
137
|
job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt);
|
|
95
138
|
}
|
|
96
139
|
job.heartbeatAt = /* @__PURE__ */ new Date();
|
|
97
|
-
|
|
98
|
-
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
99
|
-
...buildJobPayload(job),
|
|
100
|
-
tenantId: ctx.tenantId,
|
|
101
|
-
organizationId: job.organizationId ?? null
|
|
102
|
-
});
|
|
103
|
-
return job;
|
|
140
|
+
return commitJobUpdate(job, ctx);
|
|
104
141
|
},
|
|
105
142
|
async incrementProgress(jobId, delta, ctx) {
|
|
106
|
-
const job = await
|
|
107
|
-
id: jobId,
|
|
108
|
-
tenantId: ctx.tenantId,
|
|
109
|
-
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
110
|
-
});
|
|
143
|
+
const job = await loadUpdatableJob(jobId, ctx);
|
|
111
144
|
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
145
|
+
forgetJobThrottle(jobId);
|
|
112
146
|
return job;
|
|
113
147
|
}
|
|
114
148
|
job.processedCount += delta;
|
|
@@ -119,13 +153,7 @@ function createProgressService(em, eventBus) {
|
|
|
119
153
|
job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt);
|
|
120
154
|
}
|
|
121
155
|
}
|
|
122
|
-
|
|
123
|
-
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
124
|
-
...buildJobPayload(job),
|
|
125
|
-
tenantId: ctx.tenantId,
|
|
126
|
-
organizationId: job.organizationId ?? null
|
|
127
|
-
});
|
|
128
|
-
return job;
|
|
156
|
+
return commitJobUpdate(job, ctx);
|
|
129
157
|
},
|
|
130
158
|
async completeJob(jobId, input, ctx) {
|
|
131
159
|
const job = await em.findOne(ProgressJob, {
|
|
@@ -151,6 +179,7 @@ function createProgressService(em, eventBus) {
|
|
|
151
179
|
tenantId: ctx.tenantId,
|
|
152
180
|
organizationId: job.organizationId ?? null
|
|
153
181
|
});
|
|
182
|
+
forgetJobThrottle(jobId);
|
|
154
183
|
return job;
|
|
155
184
|
},
|
|
156
185
|
async failJob(jobId, input, ctx) {
|
|
@@ -174,6 +203,7 @@ function createProgressService(em, eventBus) {
|
|
|
174
203
|
tenantId: ctx.tenantId,
|
|
175
204
|
organizationId: job.organizationId ?? null
|
|
176
205
|
});
|
|
206
|
+
forgetJobThrottle(jobId);
|
|
177
207
|
return job;
|
|
178
208
|
},
|
|
179
209
|
async cancelJob(jobId, ctx) {
|
|
@@ -196,6 +226,7 @@ function createProgressService(em, eventBus) {
|
|
|
196
226
|
tenantId: ctx.tenantId,
|
|
197
227
|
organizationId: job.organizationId ?? null
|
|
198
228
|
});
|
|
229
|
+
forgetJobThrottle(jobId);
|
|
199
230
|
return job;
|
|
200
231
|
},
|
|
201
232
|
async markCancelled(jobId, ctx) {
|
|
@@ -219,6 +250,7 @@ function createProgressService(em, eventBus) {
|
|
|
219
250
|
tenantId: ctx.tenantId,
|
|
220
251
|
organizationId: job.organizationId ?? null
|
|
221
252
|
});
|
|
253
|
+
forgetJobThrottle(jobId);
|
|
222
254
|
return job;
|
|
223
255
|
},
|
|
224
256
|
async isCancellationRequested(jobId, tenantId, organizationId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/progress/lib/progressServiceImpl.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { ProgressJob } from '../data/entities'\nimport type { ProgressService } from './progressService'\nimport { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from './progressService'\nimport { PROGRESS_EVENTS } from './events'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nfunction buildJobPayload(job: ProgressJob): Record<string, unknown> {\n return {\n jobId: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description ?? null,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n cancellable: job.cancellable,\n meta: job.meta ?? null,\n startedAt: job.startedAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n }\n}\n\nexport function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {\n return {\n async createJob(input, ctx) {\n const job = em.create(ProgressJob, {\n jobType: input.jobType,\n name: input.name,\n description: input.description,\n totalCount: input.totalCount,\n cancellable: input.cancellable ?? false,\n meta: input.meta,\n parentJobId: input.parentJobId,\n partitionIndex: input.partitionIndex,\n partitionCount: input.partitionCount,\n startedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: 'pending',\n })\n\n await em.persist(job).flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return job\n },\n\n async startJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (job.status === 'cancelled') {\n return job\n }\n\n job.status = 'running'\n job.startedAt = new Date()\n job.heartbeatAt = new Date()\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async updateProgress(jobId, input, ctx) {\n const job = await em.findOneOrFail(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {\n return job\n }\n\n if (input.processedCount !== undefined) {\n job.processedCount = input.processedCount\n }\n if (input.totalCount !== undefined) {\n job.totalCount = input.totalCount\n }\n if (input.meta !== undefined) {\n job.meta = { ...job.meta, ...input.meta }\n }\n\n if (input.progressPercent !== undefined) {\n job.progressPercent = input.progressPercent\n } else if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n }\n\n if (input.etaSeconds !== undefined) {\n job.etaSeconds = input.etaSeconds\n } else if (job.startedAt && job.totalCount) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n\n job.heartbeatAt = new Date()\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async incrementProgress(jobId, delta, ctx) {\n const job = await em.findOneOrFail(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {\n return job\n }\n\n job.processedCount += delta\n job.heartbeatAt = new Date()\n\n if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n if (job.startedAt) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async completeJob(jobId, input, ctx) {\n const job = await em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled') {\n return job\n }\n\n job.status = 'completed'\n job.finishedAt = new Date()\n job.progressPercent = 100\n job.etaSeconds = 0\n if (input?.resultSummary) {\n job.resultSummary = input.resultSummary\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {\n ...buildJobPayload(job),\n resultSummary: job.resultSummary,\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async failJob(jobId, input, ctx) {\n const job = await em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled') {\n return job\n }\n\n job.status = 'failed'\n job.finishedAt = new Date()\n job.errorMessage = input.errorMessage\n job.errorStack = input.errorStack\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async cancelJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n cancellable: true,\n status: { $in: ['pending', 'running'] },\n })\n\n job.cancelRequestedAt = new Date()\n job.cancelledByUserId = ctx.userId\n\n if (job.status === 'pending') {\n job.status = 'cancelled'\n job.finishedAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async markCancelled(jobId, ctx) {\n const job = await em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled') {\n return job\n }\n\n job.cancelRequestedAt = job.cancelRequestedAt ?? new Date()\n job.cancelledByUserId = ctx.userId\n job.status = 'cancelled'\n job.finishedAt = job.finishedAt ?? new Date()\n job.etaSeconds = 0\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async isCancellationRequested(jobId, tenantId, organizationId) {\n const job = await findOneWithDecryption(em, ProgressJob, {\n id: jobId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n })\n return job?.cancelRequestedAt != null\n },\n\n async getActiveJobs(ctx) {\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['pending', 'running'] },\n parentJobId: null,\n }, {\n orderBy: { createdAt: 'DESC' },\n limit: 50,\n })\n },\n\n async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {\n const cutoff = new Date(Date.now() - sinceSeconds * 1000)\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['completed', 'failed'] },\n finishedAt: { $gte: cutoff },\n parentJobId: null,\n }, {\n orderBy: { finishedAt: 'DESC' },\n limit: 10,\n })\n },\n\n async getJob(jobId, ctx) {\n return em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n },\n\n async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId?: string | null) {\n const cutoff = new Date(Date.now() - timeoutSeconds * 1000)\n\n const staleJobs = await em.find(ProgressJob, {\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n status: 'running',\n $or: [\n { heartbeatAt: { $lt: cutoff } },\n {\n heartbeatAt: null,\n startedAt: { $lt: cutoff },\n },\n ],\n })\n\n for (const job of staleJobs) {\n job.status = 'failed'\n job.finishedAt = new Date()\n job.errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n await em.flush()\n return staleJobs.length\n },\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,mBAAmB;AAE5B,SAAS,cAAc,0BAA0B,iCAAiC;AAClF,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AAEtC,SAAS,gBAAgB,KAA2C;AAClE,SAAO;AAAA,IACL,OAAO,IAAI;AAAA,IACX,SAAS,IAAI;AAAA,IACb,MAAM,IAAI;AAAA,IACV,aAAa,IAAI,eAAe;AAAA,IAChC,QAAQ,IAAI;AAAA,IACZ,iBAAiB,IAAI;AAAA,IACrB,gBAAgB,IAAI;AAAA,IACpB,YAAY,IAAI,cAAc;AAAA,IAC9B,YAAY,IAAI,cAAc;AAAA,IAC9B,aAAa,IAAI;AAAA,IACjB,MAAM,IAAI,QAAQ;AAAA,IAClB,WAAW,IAAI,WAAW,YAAY,KAAK;AAAA,IAC3C,YAAY,IAAI,YAAY,YAAY,KAAK;AAAA,EAC/C;AACF;AAEO,SAAS,sBAAsB,IAAmB,UAAyG;AAChK,SAAO;AAAA,IACL,MAAM,UAAU,OAAO,KAAK;AAC1B,YAAM,MAAM,GAAG,OAAO,aAAa;AAAA,QACjC,SAAS,MAAM;AAAA,QACf,MAAM,MAAM;AAAA,QACZ,aAAa,MAAM;AAAA,QACnB,YAAY,MAAM;AAAA,QAClB,aAAa,MAAM,eAAe;AAAA,QAClC,MAAM,MAAM;AAAA,QACZ,aAAa,MAAM;AAAA,QACnB,gBAAgB,MAAM;AAAA,QACtB,gBAAgB,MAAM;AAAA,QACtB,iBAAiB,IAAI;AAAA,QACrB,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,QACpB,QAAQ;AAAA,MACV,CAAC;AAED,YAAM,GAAG,QAAQ,GAAG,EAAE,MAAM;AAE5B,YAAM,SAAS,KAAK,gBAAgB,aAAa;AAAA,QAC/C,GAAG,gBAAgB,GAAG;AAAA,QACtB,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,MACtB,CAAC;AAED,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,SAAS,OAAO,KAAK;AACzB,YAAM,MAAM,MAAM,GAAG,cAAc,aAAa;AAAA,QAC9C,IAAI;AAAA,QACJ,UAAU,IAAI;AAAA,QACd,GAAI,IAAI,iBAAiB,EAAE,gBAAgB,IAAI,eAAe,IAAI,CAAC;AAAA,MACrE,CAAC;AACD,UAAI,IAAI,WAAW,aAAa;AAC9B,eAAO;AAAA,MACT;AAEA,UAAI,SAAS;AACb,UAAI,YAAY,oBAAI,KAAK;AACzB,UAAI,cAAc,oBAAI,KAAK;AAE3B,YAAM,GAAG,MAAM;AAEf,YAAM,SAAS,KAAK,gBAAgB,aAAa;AAAA,QAC/C,GAAG,gBAAgB,GAAG;AAAA,QACtB,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI,kBAAkB;AAAA,MACxC,CAAC;AAED,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,eAAe,OAAO,OAAO,KAAK;AACtC,YAAM,MAAM,MAAM,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { ProgressJob } from '../data/entities'\nimport type { ProgressService, ProgressServiceContext } from './progressService'\nimport { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from './progressService'\nimport { PROGRESS_EVENTS } from './events'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250\n\n// Minimum elapsed time between coalesced `progress.job.updated` flush+broadcasts for a\n// single job. Bulk workers call updateProgress/incrementProgress once per record, and\n// every emit of the `clientBroadcast: true` event pays a serialized pg_notify roundtrip\n// plus a tenant-wide SSE fan-out. Setting the knob to 0 restores per-record emission.\nfunction resolveBroadcastMinIntervalMs(): number {\n const raw = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS\n if (raw == null || raw.trim() === '') return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n const parsed = Number.parseInt(raw, 10)\n if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n return parsed\n}\n\ntype JobUpdateThrottleEntry = {\n job: ProgressJob\n lastBroadcastAt: number\n lastBroadcastPercent: number\n}\n\nfunction buildJobPayload(job: ProgressJob): Record<string, unknown> {\n return {\n jobId: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description ?? null,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n cancellable: job.cancellable,\n meta: job.meta ?? null,\n startedAt: job.startedAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n }\n}\n\nexport function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {\n const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs()\n // Per-job coalescing state, scoped to this service instance (request/worker scope).\n // The cached managed entity doubles as the in-memory buffer: intermediate updates mutate\n // it without flushing, and a single flush+emit at the throttle boundary persists the\n // accumulated change. Domain commands fork their own EntityManager, so no other operation\n // queries this job on `em` between calls, keeping the buffered changes safe until flush.\n const jobUpdateThrottle = new Map<string, JobUpdateThrottleEntry>()\n\n function jobScopeFilter(jobId: string, ctx: ProgressServiceContext) {\n return {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n }\n\n async function loadUpdatableJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob> {\n const cached = jobUpdateThrottle.get(jobId)\n if (cached) return cached.job\n return em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx))\n }\n\n async function commitJobUpdate(job: ProgressJob, ctx: ProgressServiceContext): Promise<ProgressJob> {\n const now = Date.now()\n const cached = jobUpdateThrottle.get(job.id)\n const shouldBroadcast =\n broadcastMinIntervalMs <= 0 ||\n cached == null ||\n now - cached.lastBroadcastAt >= broadcastMinIntervalMs ||\n Math.abs(job.progressPercent - cached.lastBroadcastPercent) >= 1\n\n if (shouldBroadcast) {\n await em.flush()\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n jobUpdateThrottle.set(job.id, { job, lastBroadcastAt: now, lastBroadcastPercent: job.progressPercent })\n } else {\n jobUpdateThrottle.set(job.id, {\n job,\n lastBroadcastAt: cached.lastBroadcastAt,\n lastBroadcastPercent: cached.lastBroadcastPercent,\n })\n }\n\n return job\n }\n\n function forgetJobThrottle(jobId: string) {\n jobUpdateThrottle.delete(jobId)\n }\n\n return {\n async createJob(input, ctx) {\n const job = em.create(ProgressJob, {\n jobType: input.jobType,\n name: input.name,\n description: input.description,\n totalCount: input.totalCount,\n cancellable: input.cancellable ?? false,\n meta: input.meta,\n parentJobId: input.parentJobId,\n partitionIndex: input.partitionIndex,\n partitionCount: input.partitionCount,\n startedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: 'pending',\n })\n\n await em.persist(job).flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return job\n },\n\n async startJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (job.status === 'cancelled') {\n return job\n }\n\n job.status = 'running'\n job.startedAt = new Date()\n job.heartbeatAt = new Date()\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async updateProgress(jobId, input, ctx) {\n const job = await loadUpdatableJob(jobId, ctx)\n if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {\n forgetJobThrottle(jobId)\n return job\n }\n\n if (input.processedCount !== undefined) {\n job.processedCount = input.processedCount\n }\n if (input.totalCount !== undefined) {\n job.totalCount = input.totalCount\n }\n if (input.meta !== undefined) {\n job.meta = { ...job.meta, ...input.meta }\n }\n\n if (input.progressPercent !== undefined) {\n job.progressPercent = input.progressPercent\n } else if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n }\n\n if (input.etaSeconds !== undefined) {\n job.etaSeconds = input.etaSeconds\n } else if (job.startedAt && job.totalCount) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n\n job.heartbeatAt = new Date()\n\n return commitJobUpdate(job, ctx)\n },\n\n async incrementProgress(jobId, delta, ctx) {\n const job = await loadUpdatableJob(jobId, ctx)\n if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {\n forgetJobThrottle(jobId)\n return job\n }\n\n job.processedCount += delta\n job.heartbeatAt = new Date()\n\n if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n if (job.startedAt) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n }\n\n return commitJobUpdate(job, ctx)\n },\n\n async completeJob(jobId, input, ctx) {\n const job = await em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled') {\n return job\n }\n\n job.status = 'completed'\n job.finishedAt = new Date()\n job.progressPercent = 100\n job.etaSeconds = 0\n if (input?.resultSummary) {\n job.resultSummary = input.resultSummary\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {\n ...buildJobPayload(job),\n resultSummary: job.resultSummary,\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n forgetJobThrottle(jobId)\n return job\n },\n\n async failJob(jobId, input, ctx) {\n const job = await em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled') {\n return job\n }\n\n job.status = 'failed'\n job.finishedAt = new Date()\n job.errorMessage = input.errorMessage\n job.errorStack = input.errorStack\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n forgetJobThrottle(jobId)\n return job\n },\n\n async cancelJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n cancellable: true,\n status: { $in: ['pending', 'running'] },\n })\n\n job.cancelRequestedAt = new Date()\n job.cancelledByUserId = ctx.userId\n\n if (job.status === 'pending') {\n job.status = 'cancelled'\n job.finishedAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n forgetJobThrottle(jobId)\n return job\n },\n\n async markCancelled(jobId, ctx) {\n const job = await em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled') {\n return job\n }\n\n job.cancelRequestedAt = job.cancelRequestedAt ?? new Date()\n job.cancelledByUserId = ctx.userId\n job.status = 'cancelled'\n job.finishedAt = job.finishedAt ?? new Date()\n job.etaSeconds = 0\n\n await em.flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n forgetJobThrottle(jobId)\n return job\n },\n\n async isCancellationRequested(jobId, tenantId, organizationId) {\n const job = await findOneWithDecryption(em, ProgressJob, {\n id: jobId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n })\n return job?.cancelRequestedAt != null\n },\n\n async getActiveJobs(ctx) {\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['pending', 'running'] },\n parentJobId: null,\n }, {\n orderBy: { createdAt: 'DESC' },\n limit: 50,\n })\n },\n\n async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {\n const cutoff = new Date(Date.now() - sinceSeconds * 1000)\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['completed', 'failed'] },\n finishedAt: { $gte: cutoff },\n parentJobId: null,\n }, {\n orderBy: { finishedAt: 'DESC' },\n limit: 10,\n })\n },\n\n async getJob(jobId, ctx) {\n return em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n },\n\n async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId?: string | null) {\n const cutoff = new Date(Date.now() - timeoutSeconds * 1000)\n\n const staleJobs = await em.find(ProgressJob, {\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n status: 'running',\n $or: [\n { heartbeatAt: { $lt: cutoff } },\n {\n heartbeatAt: null,\n startedAt: { $lt: cutoff },\n },\n ],\n })\n\n for (const job of staleJobs) {\n job.status = 'failed'\n job.finishedAt = new Date()\n job.errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n await em.flush()\n return staleJobs.length\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,7 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
|
4
4
|
import { resolveCrudRecordId, parseScopedCommandInput } from "@open-mercato/shared/lib/api/scoped";
|
|
5
5
|
import { StaffTimeEntry } from "../../../data/entities.js";
|
|
6
6
|
import { staffTimeEntryCreateSchema, staffTimeEntryUpdateSchema } from "../../../data/validators.js";
|
|
7
|
+
import { buildTimeEntryListFilters, isParseableDateFilter } from "../../../lib/timesheets/timeEntryListFilters.js";
|
|
7
8
|
import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from "../../openapi.js";
|
|
8
9
|
const F = {
|
|
9
10
|
id: "id",
|
|
@@ -32,12 +33,6 @@ const routeMetadata = {
|
|
|
32
33
|
};
|
|
33
34
|
const metadata = routeMetadata;
|
|
34
35
|
const rawBodySchema = z.object({}).passthrough();
|
|
35
|
-
const isParseableDateFilter = (value) => {
|
|
36
|
-
const trimmed = value.trim();
|
|
37
|
-
if (trimmed.length === 0) return true;
|
|
38
|
-
if (!/^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(trimmed)) return false;
|
|
39
|
-
return !Number.isNaN(new Date(trimmed).getTime());
|
|
40
|
-
};
|
|
41
36
|
const dateFilterSchema = z.string().refine(isParseableDateFilter, { message: "Invalid date" }).optional();
|
|
42
37
|
const listSchema = z.object({
|
|
43
38
|
page: z.coerce.number().min(1).default(1),
|
|
@@ -47,6 +42,7 @@ const listSchema = z.object({
|
|
|
47
42
|
to: dateFilterSchema,
|
|
48
43
|
projectId: z.string().uuid().optional(),
|
|
49
44
|
ids: z.string().optional(),
|
|
45
|
+
running: z.string().optional(),
|
|
50
46
|
sortField: z.string().optional(),
|
|
51
47
|
sortDir: z.enum(["asc", "desc"]).optional()
|
|
52
48
|
}).passthrough();
|
|
@@ -87,28 +83,7 @@ const crud = makeCrudRoute({
|
|
|
87
83
|
updatedAt: F.updated_at,
|
|
88
84
|
durationMinutes: F.duration_minutes
|
|
89
85
|
},
|
|
90
|
-
buildFilters: async (query) =>
|
|
91
|
-
const filters = {};
|
|
92
|
-
if (typeof query.ids === "string" && query.ids.trim().length > 0) {
|
|
93
|
-
const ids = query.ids.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
94
|
-
if (ids.length > 0) {
|
|
95
|
-
filters[F.id] = { $in: ids };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (typeof query.staffMemberId === "string" && query.staffMemberId.length > 0) {
|
|
99
|
-
filters[F.staff_member_id] = query.staffMemberId;
|
|
100
|
-
}
|
|
101
|
-
if (typeof query.from === "string" && query.from.length > 0 && isParseableDateFilter(query.from)) {
|
|
102
|
-
filters[F.date] = { ...filters[F.date] ?? {}, $gte: query.from };
|
|
103
|
-
}
|
|
104
|
-
if (typeof query.to === "string" && query.to.length > 0 && isParseableDateFilter(query.to)) {
|
|
105
|
-
filters[F.date] = { ...filters[F.date] ?? {}, $lte: query.to };
|
|
106
|
-
}
|
|
107
|
-
if (typeof query.projectId === "string" && query.projectId.length > 0) {
|
|
108
|
-
filters[F.time_project_id] = query.projectId;
|
|
109
|
-
}
|
|
110
|
-
return filters;
|
|
111
|
-
}
|
|
86
|
+
buildFilters: async (query) => buildTimeEntryListFilters(query)
|
|
112
87
|
},
|
|
113
88
|
actions: {
|
|
114
89
|
create: {
|