@open-mercato/core 0.10.1-develop.7728.1.6b52eb2849 → 0.10.1-develop.7741.1.61a2652671
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/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/lib/ocrService.js +8 -0
- package/dist/modules/attachments/lib/ocrService.js.map +2 -2
- package/dist/modules/auth/api/logout.js +3 -1
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1190 -1408
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +34 -11
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +5 -0
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +9 -0
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +6 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- package/dist/modules/workflows/lib/task-handler.js +16 -8
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- package/src/modules/attachments/lib/ocrService.ts +8 -0
- package/src/modules/auth/api/logout.ts +3 -1
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/sales/commands/documents.ts +1501 -1626
- package/src/modules/sales/commands/returns.ts +41 -14
- package/src/modules/sales/commands/shared.ts +40 -0
- package/src/modules/sales/commands/shipments.ts +10 -0
- package/src/modules/staff/api/timesheets/time-projects/route.ts +6 -2
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/workflows/api/portal/tasks/%5Bid%5D/complete/route.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Portal User Task completion API\n *\n * - POST /api/workflows/portal/tasks/[id]/complete\n *\n * `actable` is the gate, and on the portal branch `actable` is true only for the\n * assignee themselves: a portal admin who can READ a company member's task gets\n * `visible` without `actable` and is refused here. The refusal is a 404, same as\n * every other portal refusal \u2014 telling an admin \"you may see this but not\n * finish it\" would disclose more than the read already did.\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n getCustomerAuthFromRequest,\n requireCustomerFeature,\n} from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport type { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { UserTask } from '../../../../../data/entities'\nimport { serializeUserTask } from '../../../../tasks/serialize'\nimport type { TaskHandlerService } from '../../../../../lib/task-handler'\nimport {\n decidePortalTaskAccess,\n PORTAL_TASK_REFUSAL,\n PORTAL_TASKS_COMPLETE_FEATURE,\n resolvePortalTaskPrincipal,\n} from '../../../../../lib/portal-task-access'\nimport {\n workflowsTag,\n portalTaskCompleteRequestSchema,\n portalTaskCompleteResponseSchema,\n portalTaskErrorSchema,\n} from '../../../../openapi'\n\nconst logger = createLogger('workflows')\n\nexport const metadata: { requireAuth?: boolean } = { requireAuth: false }\n\nconst completeSchema = z.object({\n formData: z.record(z.string(), z.any()),\n comments: z.string().optional(),\n decisionId: z.string().min(1).optional(),\n})\n\nexport async function POST(request: NextRequest, { params }: { params: { id: string } }) {\n const auth = await getCustomerAuthFromRequest(request)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbac = container.resolve('customerRbacService') as CustomerRbacService\n\n try {\n await requireCustomerFeature(auth, [PORTAL_TASKS_COMPLETE_FEATURE], rbac)\n } catch (response) {\n return response as NextResponse\n }\n\n try {\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const acl = await rbac.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const resolved = await resolvePortalTaskPrincipal({ auth, em, isPortalAdmin: acl.isPortalAdmin })\n if (!resolved.ok) return NextResponse.json(resolved.body, { status: resolved.status })\n\n const parsed = completeSchema.safeParse(await request.json())\n if (!parsed.success) {\n return NextResponse.json(\n { ok: false, error: 'Invalid request body', details: parsed.error.format() },\n { status: 400 },\n )\n }\n\n const task = await em.findOne(UserTask, {\n id: params.id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!task) {\n return NextResponse.json(PORTAL_TASK_REFUSAL.body, { status: PORTAL_TASK_REFUSAL.status })\n }\n\n const decision = decidePortalTaskAccess(resolved.principal, task)\n if (!decision.actable) {\n return NextResponse.json(PORTAL_TASK_REFUSAL.body, { status: PORTAL_TASK_REFUSAL.status })\n }\n\n const taskHandler = container.resolve('taskHandler') as TaskHandlerService\n await taskHandler.completeUserTask(em, container, {\n taskId: params.id,\n formData: parsed.data.formData,\n // The portal principal's own id. `completeUserTask` compares it against\n // `claimedBy ?? assignedTo`, which for a portal task is the same id space,\n // so its own ownership check agrees with the predicate rather than\n // fighting it.\n userId: auth.sub,\n comments: parsed.data.comments,\n decisionId: parsed.data.decisionId,\n scope: { tenantId: auth.tenantId, organizationId: auth.orgId },\n })\n\n const updated = await em.findOne(UserTask, {\n id: params.id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n return NextResponse.json({\n ok: true,\n task: updated ? serializeUserTask(updated) : null,\n })\n } catch (error) {\n const code = (error as { code?: unknown }).code\n if (code === 'UNKNOWN_DECISION' || code === 'FORM_VALIDATION_FAILED') {\n return NextResponse.json(\n { ok: false, error: error instanceof Error ? error.message : 'Invalid submission' },\n { status: 400 },\n )\n }\n // Anything the handler refuses on ownership or status grounds collapses into\n // the same 404 the predicate would have produced, so the act path discloses\n // no more than the read path.\n if (code === 'TASK_NOT_FOUND'
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Portal User Task completion API\n *\n * - POST /api/workflows/portal/tasks/[id]/complete\n *\n * `actable` is the gate, and on the portal branch `actable` is true only for the\n * assignee themselves: a portal admin who can READ a company member's task gets\n * `visible` without `actable` and is refused here. The refusal is a 404, same as\n * every other portal refusal \u2014 telling an admin \"you may see this but not\n * finish it\" would disclose more than the read already did.\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n getCustomerAuthFromRequest,\n requireCustomerFeature,\n} from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport type { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { UserTask } from '../../../../../data/entities'\nimport { serializeUserTask } from '../../../../tasks/serialize'\nimport type { TaskHandlerService } from '../../../../../lib/task-handler'\nimport {\n decidePortalTaskAccess,\n PORTAL_TASK_REFUSAL,\n PORTAL_TASKS_COMPLETE_FEATURE,\n resolvePortalTaskPrincipal,\n} from '../../../../../lib/portal-task-access'\nimport {\n workflowsTag,\n portalTaskCompleteRequestSchema,\n portalTaskCompleteResponseSchema,\n portalTaskErrorSchema,\n} from '../../../../openapi'\n\nconst logger = createLogger('workflows')\n\nexport const metadata: { requireAuth?: boolean } = { requireAuth: false }\n\nconst completeSchema = z.object({\n formData: z.record(z.string(), z.any()),\n comments: z.string().optional(),\n decisionId: z.string().min(1).optional(),\n})\n\nexport async function POST(request: NextRequest, { params }: { params: { id: string } }) {\n const auth = await getCustomerAuthFromRequest(request)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbac = container.resolve('customerRbacService') as CustomerRbacService\n\n try {\n await requireCustomerFeature(auth, [PORTAL_TASKS_COMPLETE_FEATURE], rbac)\n } catch (response) {\n return response as NextResponse\n }\n\n try {\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const acl = await rbac.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const resolved = await resolvePortalTaskPrincipal({ auth, em, isPortalAdmin: acl.isPortalAdmin })\n if (!resolved.ok) return NextResponse.json(resolved.body, { status: resolved.status })\n\n const parsed = completeSchema.safeParse(await request.json())\n if (!parsed.success) {\n return NextResponse.json(\n { ok: false, error: 'Invalid request body', details: parsed.error.format() },\n { status: 400 },\n )\n }\n\n const task = await em.findOne(UserTask, {\n id: params.id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!task) {\n return NextResponse.json(PORTAL_TASK_REFUSAL.body, { status: PORTAL_TASK_REFUSAL.status })\n }\n\n const decision = decidePortalTaskAccess(resolved.principal, task)\n if (!decision.actable) {\n return NextResponse.json(PORTAL_TASK_REFUSAL.body, { status: PORTAL_TASK_REFUSAL.status })\n }\n\n const taskHandler = container.resolve('taskHandler') as TaskHandlerService\n await taskHandler.completeUserTask(em, container, {\n taskId: params.id,\n formData: parsed.data.formData,\n // The portal principal's own id. `completeUserTask` compares it against\n // `claimedBy ?? assignedTo`, which for a portal task is the same id space,\n // so its own ownership check agrees with the predicate rather than\n // fighting it.\n userId: auth.sub,\n comments: parsed.data.comments,\n decisionId: parsed.data.decisionId,\n scope: { tenantId: auth.tenantId, organizationId: auth.orgId },\n })\n\n const updated = await em.findOne(UserTask, {\n id: params.id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n return NextResponse.json({\n ok: true,\n task: updated ? serializeUserTask(updated) : null,\n })\n } catch (error) {\n const code = (error as { code?: unknown }).code\n if (code === 'UNKNOWN_DECISION' || code === 'FORM_VALIDATION_FAILED') {\n return NextResponse.json(\n { ok: false, error: error instanceof Error ? error.message : 'Invalid submission' },\n { status: 400 },\n )\n }\n // Anything the handler refuses on ownership or status grounds collapses into\n // the same 404 the predicate would have produced, so the act path discloses\n // no more than the read path.\n if (\n code === 'TASK_NOT_FOUND' ||\n code === 'TASK_ASSIGNED_TO_ANOTHER_USER' ||\n code === 'WORKFLOW_NOT_ACTIVE'\n ) {\n return NextResponse.json(PORTAL_TASK_REFUSAL.body, { status: PORTAL_TASK_REFUSAL.status })\n }\n logger.error('Error completing portal task', { err: error })\n return NextResponse.json({ ok: false, error: 'Failed to complete task' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: workflowsTag,\n summary: 'Portal user task completion',\n methods: {\n POST: {\n summary: 'Complete a portal task',\n description:\n 'Completes a customer-assigned task and resumes the workflow. Only the assignee may complete; a portal admin who can read a company member\\'s task is refused.',\n requestBody: { schema: portalTaskCompleteRequestSchema },\n responses: [\n { status: 200, description: 'Task completed', schema: portalTaskCompleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request body or form data', schema: portalTaskErrorSchema },\n { status: 401, description: 'Authentication required', schema: portalTaskErrorSchema },\n { status: 403, description: 'Insufficient permissions or no company association', schema: portalTaskErrorSchema },\n { status: 404, description: 'Task not found or not actionable by this principal', schema: portalTaskErrorSchema },\n { status: 500, description: 'Internal server error', schema: portalTaskErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -105,6 +105,12 @@ async function POST(request, { params }) {
|
|
|
105
105
|
{ status: 409 }
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
|
+
if (error.code === "WORKFLOW_NOT_ACTIVE") {
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{ error: error.message, code: "WORKFLOW_NOT_ACTIVE" },
|
|
111
|
+
{ status: 409 }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
108
114
|
if (error.message.includes("not found")) {
|
|
109
115
|
return NextResponse.json(
|
|
110
116
|
{ error: error.message },
|
|
@@ -152,7 +158,7 @@ const openApi = {
|
|
|
152
158
|
{ status: 401, description: "Unauthorized", schema: workflowErrorSchema },
|
|
153
159
|
{ status: 403, description: "The task is visible to the caller but is not theirs to act on \u2014 it has no assignee and no role queue, so it must be reassigned first (\xA76.4: administration widens seeing, never acting).", schema: workflowErrorSchema },
|
|
154
160
|
{ status: 404, description: "Task not found, or not visible to the caller", schema: workflowErrorSchema },
|
|
155
|
-
{ status: 409, description: "Task already completed", schema: workflowErrorSchema },
|
|
161
|
+
{ status: 409, description: "Task already completed, assigned to another user, or its workflow run is no longer active (`code: WORKFLOW_NOT_ACTIVE` \u2014 the run is completed, failed, cancelled, compensating or compensated)", schema: workflowErrorSchema },
|
|
156
162
|
{ status: 500, description: "Internal server error", schema: workflowErrorSchema }
|
|
157
163
|
]
|
|
158
164
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/workflows/api/tasks/%5Bid%5D/complete/route.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * User Task Completion API\n *\n * Endpoints:\n * - POST /api/workflows/tasks/[id]/complete - Complete a user task with form data\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { TaskHandlerService } from '../../../../lib/task-handler'\nimport { gateTaskAction } from '../../../../lib/task-visibility-request'\nimport {\n workflowsTag,\n completeTaskRequestSchema as openApiCompleteTaskSchema,\n userTaskCompleteResponseSchema,\n workflowErrorSchema,\n} from '../../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\n\nconst logger = createLogger('workflows')\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.tasks.complete'],\n}\n\n// Request body schema\nconst completeTaskSchema = z.object({\n formData: z.record(z.string(), z.any()),\n comments: z.string().optional(),\n /**\n * The decision button the assignee pressed. Optional and additive: a task\n * without decisions completes through the plain form exactly as before.\n */\n decisionId: z.string().min(1).optional(),\n})\n\n/**\n * POST /api/workflows/tasks/[id]/complete\n *\n * Complete a user task with form data\n *\n * Request body:\n * {\n * formData: { [key: string]: any }, // Form field values\n * comments: string (optional) // Optional comments\n * }\n *\n * This endpoint:\n * 1. Validates the task can be completed\n * 2. Validates form data against the task's form schema\n * 3. Updates the task with completion data\n * 4. Merges form data into workflow context\n * 5. Resumes workflow execution\n */\nexport async function POST(\n request: NextRequest,\n { params }: { params: { id: string } }\n) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth)\n\n if (!tenantId || !organizationId) {\n return NextResponse.json(\n { error: 'Missing tenant or organization context' },\n { status: 400 }\n )\n }\n\n // Parse request body\n const body = await request.json()\n const parseResult = completeTaskSchema.safeParse(body)\n\n if (!parseResult.success) {\n return NextResponse.json(\n {\n error: 'Invalid request body',\n details: parseResult.error.format(),\n },\n { status: 400 }\n )\n }\n\n const { formData, comments, decisionId } = parseResult.data\n\n // \u00A76.4 replaces the bare tenant/organization check that used to stand here.\n // The narrowing D-1 keeps `workflows.tasks.complete` on the route and still\n // demands: holding it no longer completes anyone ELSE's task. Refusals are\n // indistinguishable from a nonexistent id unless the caller already sees the\n // row.\n const gate = await gateTaskAction({\n container,\n em,\n auth: { userId: auth.sub, tenantId, roleNames: auth.roles ?? [] },\n taskId: params.id,\n organizationId,\n // Complete is the one act where the gate, not the handler, decides\n // ownership: the handler leaves an OWNERLESS row open to anyone holding\n // the feature, which is exactly the pre-change semantics \u00A76.4 narrows.\n requireOwnership: true,\n })\n\n if (!gate.allowed) {\n return NextResponse.json(gate.refusal.body, { status: gate.refusal.status })\n }\n\n const { UserTask } = await import('../../../../data/entities')\n\n // Call task handler to complete task and resume workflow\n const taskHandler = container.resolve<TaskHandlerService>('taskHandler')\n await taskHandler.completeUserTask(em, container, {\n taskId: params.id,\n formData,\n userId: auth.sub,\n comments,\n decisionId,\n scope: { tenantId, organizationId },\n })\n\n // Fetch updated task\n const updatedTask = await em.findOne(UserTask, {\n id: params.id,\n tenantId,\n organizationId,\n })\n\n return NextResponse.json({\n data: updatedTask,\n message: 'Task completed successfully. Workflow resumed.',\n })\n } catch (error) {\n if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })\n logger.error('Error completing user task', { err: error })\n\n // Handle specific error codes from task-handler\n if (error instanceof Error) {\n if ((error as { code?: unknown }).code === 'UNKNOWN_DECISION') {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n // \"Task is assigned to another user\" matches none of the message arms\n // below, so this refusal used to answer 500. It is a conflict.\n if ((error as { code?: unknown }).code === 'TASK_ASSIGNED_TO_ANOTHER_USER') {\n return NextResponse.json(\n { error: error.message },\n { status: 409 }\n )\n }\n if (error.message.includes('not found')) {\n return NextResponse.json(\n { error: error.message },\n { status: 404 }\n )\n }\n if (error.message.includes('validation')) {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n if (error.message.includes('already completed')) {\n return NextResponse.json(\n { error: error.message },\n { status: 409 }\n )\n }\n }\n\n return NextResponse.json(\n {\n error: 'Failed to complete user task',\n details: error instanceof Error ? error.message : String(error),\n },\n { status: 500 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: workflowsTag,\n summary: 'Complete user task',\n methods: {\n POST: {\n summary: 'Complete a task with form data',\n description: 'Validates form data against task schema, updates task with completion data, merges form data into workflow context, and resumes workflow execution.',\n requestBody: {\n contentType: 'application/json',\n schema: openApiCompleteTaskSchema,\n },\n responses: [\n { status: 200, description: 'Task completed successfully', schema: userTaskCompleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request body, validation failed, or missing context', schema: workflowErrorSchema },\n { status: 401, description: 'Unauthorized', schema: workflowErrorSchema },\n { status: 403, description: 'The task is visible to the caller but is not theirs to act on \u2014 it has no assignee and no role queue, so it must be reassigned first (\u00A76.4: administration widens seeing, never acting).', schema: workflowErrorSchema },\n { status: 404, description: 'Task not found, or not visible to the caller', schema: workflowErrorSchema },\n { status: 409, description: 'Task already completed', schema: workflowErrorSchema },\n { status: 500, description: 'Internal server error', schema: workflowErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * User Task Completion API\n *\n * Endpoints:\n * - POST /api/workflows/tasks/[id]/complete - Complete a user task with form data\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { TaskHandlerService } from '../../../../lib/task-handler'\nimport { gateTaskAction } from '../../../../lib/task-visibility-request'\nimport {\n workflowsTag,\n completeTaskRequestSchema as openApiCompleteTaskSchema,\n userTaskCompleteResponseSchema,\n workflowErrorSchema,\n} from '../../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\n\nconst logger = createLogger('workflows')\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.tasks.complete'],\n}\n\n// Request body schema\nconst completeTaskSchema = z.object({\n formData: z.record(z.string(), z.any()),\n comments: z.string().optional(),\n /**\n * The decision button the assignee pressed. Optional and additive: a task\n * without decisions completes through the plain form exactly as before.\n */\n decisionId: z.string().min(1).optional(),\n})\n\n/**\n * POST /api/workflows/tasks/[id]/complete\n *\n * Complete a user task with form data\n *\n * Request body:\n * {\n * formData: { [key: string]: any }, // Form field values\n * comments: string (optional) // Optional comments\n * }\n *\n * This endpoint:\n * 1. Validates the task can be completed\n * 2. Validates form data against the task's form schema\n * 3. Updates the task with completion data\n * 4. Merges form data into workflow context\n * 5. Resumes workflow execution\n */\nexport async function POST(\n request: NextRequest,\n { params }: { params: { id: string } }\n) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth)\n\n if (!tenantId || !organizationId) {\n return NextResponse.json(\n { error: 'Missing tenant or organization context' },\n { status: 400 }\n )\n }\n\n // Parse request body\n const body = await request.json()\n const parseResult = completeTaskSchema.safeParse(body)\n\n if (!parseResult.success) {\n return NextResponse.json(\n {\n error: 'Invalid request body',\n details: parseResult.error.format(),\n },\n { status: 400 }\n )\n }\n\n const { formData, comments, decisionId } = parseResult.data\n\n // \u00A76.4 replaces the bare tenant/organization check that used to stand here.\n // The narrowing D-1 keeps `workflows.tasks.complete` on the route and still\n // demands: holding it no longer completes anyone ELSE's task. Refusals are\n // indistinguishable from a nonexistent id unless the caller already sees the\n // row.\n const gate = await gateTaskAction({\n container,\n em,\n auth: { userId: auth.sub, tenantId, roleNames: auth.roles ?? [] },\n taskId: params.id,\n organizationId,\n // Complete is the one act where the gate, not the handler, decides\n // ownership: the handler leaves an OWNERLESS row open to anyone holding\n // the feature, which is exactly the pre-change semantics \u00A76.4 narrows.\n requireOwnership: true,\n })\n\n if (!gate.allowed) {\n return NextResponse.json(gate.refusal.body, { status: gate.refusal.status })\n }\n\n const { UserTask } = await import('../../../../data/entities')\n\n // Call task handler to complete task and resume workflow\n const taskHandler = container.resolve<TaskHandlerService>('taskHandler')\n await taskHandler.completeUserTask(em, container, {\n taskId: params.id,\n formData,\n userId: auth.sub,\n comments,\n decisionId,\n scope: { tenantId, organizationId },\n })\n\n // Fetch updated task\n const updatedTask = await em.findOne(UserTask, {\n id: params.id,\n tenantId,\n organizationId,\n })\n\n return NextResponse.json({\n data: updatedTask,\n message: 'Task completed successfully. Workflow resumed.',\n })\n } catch (error) {\n if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })\n logger.error('Error completing user task', { err: error })\n\n // Handle specific error codes from task-handler\n if (error instanceof Error) {\n if ((error as { code?: unknown }).code === 'UNKNOWN_DECISION') {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n // \"Task is assigned to another user\" matches none of the message arms\n // below, so this refusal used to answer 500. It is a conflict.\n if ((error as { code?: unknown }).code === 'TASK_ASSIGNED_TO_ANOTHER_USER') {\n return NextResponse.json(\n { error: error.message },\n { status: 409 }\n )\n }\n if ((error as { code?: unknown }).code === 'WORKFLOW_NOT_ACTIVE') {\n return NextResponse.json(\n { error: error.message, code: 'WORKFLOW_NOT_ACTIVE' },\n { status: 409 }\n )\n }\n if (error.message.includes('not found')) {\n return NextResponse.json(\n { error: error.message },\n { status: 404 }\n )\n }\n if (error.message.includes('validation')) {\n return NextResponse.json(\n { error: error.message },\n { status: 400 }\n )\n }\n if (error.message.includes('already completed')) {\n return NextResponse.json(\n { error: error.message },\n { status: 409 }\n )\n }\n }\n\n return NextResponse.json(\n {\n error: 'Failed to complete user task',\n details: error instanceof Error ? error.message : String(error),\n },\n { status: 500 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: workflowsTag,\n summary: 'Complete user task',\n methods: {\n POST: {\n summary: 'Complete a task with form data',\n description: 'Validates form data against task schema, updates task with completion data, merges form data into workflow context, and resumes workflow execution.',\n requestBody: {\n contentType: 'application/json',\n schema: openApiCompleteTaskSchema,\n },\n responses: [\n { status: 200, description: 'Task completed successfully', schema: userTaskCompleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request body, validation failed, or missing context', schema: workflowErrorSchema },\n { status: 401, description: 'Unauthorized', schema: workflowErrorSchema },\n { status: 403, description: 'The task is visible to the caller but is not theirs to act on \u2014 it has no assignee and no role queue, so it must be reassigned first (\u00A76.4: administration widens seeing, never acting).', schema: workflowErrorSchema },\n { status: 404, description: 'Task not found, or not visible to the caller', schema: workflowErrorSchema },\n { status: 409, description: 'Task already completed, assigned to another user, or its workflow run is no longer active (`code: WORKFLOW_NOT_ACTIVE` \u2014 the run is completed, failed, cancelled, compensating or compensated)', schema: workflowErrorSchema },\n { status: 500, description: 'Internal server error', schema: workflowErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -156,7 +156,8 @@ function UserTaskDetailPage({ params }) {
|
|
|
156
156
|
refreshTask();
|
|
157
157
|
} else {
|
|
158
158
|
const failure = result.result;
|
|
159
|
-
|
|
159
|
+
const failureMessage = failure?.code === "WORKFLOW_NOT_ACTIVE" ? t("workflows.tasks.messages.workflowNotActive") : failure?.error || t("workflows.tasks.messages.completeFailed");
|
|
160
|
+
flash(failureMessage, "error");
|
|
160
161
|
}
|
|
161
162
|
} catch (err) {
|
|
162
163
|
logger.error("Error completing task", { err });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/workflows/backend/tasks/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Task detail \u2014 decision beside context (spec \u00A76.2).\n *\n * > \"Task detail = decision + context side-by-side: left \u2014 instructions, form,\n * > decision buttons; right \u2014 bound entity card(s) with deep links. [\u2026] After\n * > completion: 'next task' \u2014 a one-click claim-next affordance.\"\n *\n * Three things about this page are load-bearing:\n *\n * - **The decision buttons are DERIVED, never stored.** The API re-resolves them\n * per request from the instance's pinned definition\n * (`lib/task-decisions.ts`), so no column carries the button list and a\n * definition that gains a decision immediately offers it. Pressing one both\n * completes the task and selects the outgoing route by the decision's durable\n * `transitionId`; WHICH button was pressed is recorded server-side as part of\n * the completion payload.\n * - **Every write goes through `useGuardedMutation`.** This page cannot use\n * `CrudForm`, which is exactly the case the project rule names, and the\n * mutation context carries `retryLastMutation` so the shared conflict/retry\n * bar can re-run the last attempt.\n * - **Status is never colour-only** \u2014 every badge pairs its design-system token\n * with an icon and a translated label (`lib/work-inbox/presentation.ts`).\n * - **Every affordance is gated by what the SERVER said** (`canComplete` /\n * `canClaim` / `canRelease` / `canReassign`, decided by `lib/task-visibility.ts`\n * and sent on the detail response). The page never re-derives \u00A76.4: a second\n * copy of that rule is how a `workflows.tasks.view_all` administrator came to\n * be offered a Complete button that always answered 409. When an action is\n * unavailable the page states the server's reason and, where \u00A76.4 names one,\n * offers the remedy \u2014 reassignment.\n */\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 { Button } from '@open-mercato/ui/primitives/button'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Separator } from '@open-mercato/ui/primitives/separator'\nimport { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MobileTaskForm } from '../../../components/mobile/MobileTaskForm'\nimport { StatusChip } from '../../../components/work-inbox/StatusChip'\nimport { TaskActionNotice } from '../../../components/work-inbox/TaskActionNotice'\nimport {\n ReassignTaskDialog,\n type ReassignTaskTarget,\n} from '../../../components/work-inbox/ReassignTaskDialog'\nimport { TaskFormFields } from '../../../components/work-inbox/TaskFormFields'\nimport { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'\nimport type { UserTaskDecision, UserTaskResponse, UserTaskStatus } from '../../../data/types'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n TASK_DETAIL_INJECTION_SPOT_ID,\n WORK_INBOX_HREF,\n buildWorkInboxItemHref,\n} from '../../../lib/work-inbox/navigation'\nimport {\n EntityContextPanel,\n bindingsToEntityContextItems,\n} from '../../../components/work-inbox/EntityContextPanel'\nimport {\n WORK_INBOX_OVERDUE_TONE,\n describeTaskActionBlock,\n describeWorkInboxPriority,\n describeWorkInboxStatus,\n} from '../../../lib/work-inbox/presentation'\nimport { normalizeWorkInboxPriority } from '../../../lib/work-inbox/provider'\nimport { flattenTaskText } from '../../../lib/task-resolution'\nimport { readRecordedDecision } from '../../../lib/task-decisions'\nimport { normalizeTaskFormSchema } from '../../../lib/task-form-schema'\n\nconst logger = createLogger('workflows')\n\ntype TaskMutationContext = {\n formId: string\n taskId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction decisionButtonVariant(style: UserTaskDecision['style']): 'default' | 'outline' | 'destructive' {\n if (style === 'destructive') return 'destructive'\n if (style === 'secondary') return 'outline'\n return 'default'\n}\n\nexport default function UserTaskDetailPage({ params }: { params: { id: string } }) {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const isMobile = useIsMobile()\n const [formData, setFormData] = React.useState<Record<string, string | number | boolean>>({})\n const [comments, setComments] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n const [reassignOpen, setReassignOpen] = React.useState(false)\n // Tracks the first required field that failed validation so we can mark the\n // field with aria-invalid + a red ring (Radix Select can't carry HTML\n // `required`, so we enforce constraint validation in JS instead).\n const [invalidField, setInvalidField] = React.useState<string | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<TaskMutationContext>({\n contextId: `workflows-task:${params.id}`,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const { data: task, isLoading, error } = useQuery({\n queryKey: ['workflow-task', params.id],\n queryFn: async () => {\n const result = await apiCall<{ data: UserTaskResponse }>(\n `/api/workflows/tasks/${params.id}`\n )\n if (!result.ok) {\n const httpErr = new Error(\n result.status === 404\n ? t('workflows.tasks.detail.notFound', 'Task not found')\n : t('workflows.tasks.detail.loadFailed', 'Failed to load task')\n ) as Error & { status: number }\n httpErr.status = result.status\n throw httpErr\n }\n return result.result?.data ?? null\n },\n })\n\n const mutationContext = React.useMemo<TaskMutationContext>(\n () => ({\n formId: `workflows-task:${params.id}`,\n taskId: params.id,\n resourceKind: 'workflows.user_task',\n resourceId: params.id,\n retryLastMutation,\n }),\n [params.id, retryLastMutation],\n )\n\n // Injection spot for modules that OWN what a task is about (spec \u00A77.6). It\n // is a CONTRACT SURFACE (BACKWARD_COMPATIBILITY.md \u00A76): the id is frozen once\n // a third party mounts a widget on it.\n const taskInjectionContext = React.useMemo(\n () => ({\n taskId: params.id,\n workflowInstanceId: task?.workflowInstanceId ?? null,\n status: task?.status ?? null,\n formSchema: task?.formSchema ?? null,\n formData: task?.formData ?? null,\n retryLastMutation,\n }),\n [params.id, retryLastMutation, task?.formData, task?.formSchema, task?.status, task?.workflowInstanceId],\n )\n\n const refreshTask = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: ['workflow-task', params.id] })\n }, [params.id, queryClient])\n\n const handleFieldChange = (fieldName: string, value: string | number | boolean) => {\n setFormData(prev => ({\n ...prev,\n [fieldName]: value,\n }))\n // Clear invalid state once the user touches the offending field.\n if (invalidField === fieldName) setInvalidField(null)\n }\n\n const validateRequiredFields = React.useCallback((): boolean => {\n // Normalized, so a Studio-authored `{ fields: [...] }` schema is validated\n // by the same rule the server enforces (`lib/task-form-schema.ts`).\n const normalizedSchema = normalizeTaskFormSchema(task?.formSchema)\n if (!normalizedSchema?.required) return true\n for (const requiredField of normalizedSchema.required) {\n if (!formData[requiredField] || formData[requiredField] === '') {\n const fieldSchema = normalizedSchema.properties?.[requiredField]\n const fieldLabel = fieldSchema?.title ?? requiredField\n flash(t('workflows.tasks.detail.validation.requiredField', { field: fieldLabel }), 'error')\n setInvalidField(requiredField)\n // Scroll + focus the trigger so the user sees what's missing.\n if (typeof document !== 'undefined') {\n const trigger = document.getElementById(requiredField)\n trigger?.scrollIntoView({ behavior: 'smooth', block: 'center' })\n trigger?.focus()\n }\n return false\n }\n }\n setInvalidField(null)\n return true\n }, [formData, task, t])\n\n const completeTask = React.useCallback(\n async (decisionId?: string) => {\n if (!task) return\n if (!validateRequiredFields()) return\n\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall(`/api/workflows/tasks/${params.id}/complete`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n formData,\n comments: comments || undefined,\n ...(decisionId ? { decisionId } : {}),\n }),\n }),\n context: mutationContext,\n mutationPayload: { formData, comments, decisionId },\n })\n\n if (result.ok) {\n flash(t('workflows.tasks.messages.completed'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string } | null\n flash(failure?.error || t('workflows.tasks.messages.completeFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error completing task', { err })\n flash(t('workflows.tasks.messages.completeFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [comments, formData, mutationContext, params.id, refreshTask, runMutation, t, task, validateRequiredFields],\n )\n\n const handleSubmit = React.useCallback(\n (event: React.FormEvent) => {\n event.preventDefault()\n void completeTask()\n },\n [completeTask],\n )\n\n const handleClaim = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/claim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'claim' },\n })\n if (result.ok) {\n flash(t('workflows.tasks.messages.claimed'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error claiming task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n const handleRelease = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/unclaim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'unclaim' },\n })\n if (result.ok) {\n flash(t('workflows.workInbox.messages.released'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error releasing task', { err })\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n /**\n * Reassignment is the remedy \u00A76.4 names for every ownership refusal, and the\n * only way to rescue a task authored with no assignee, no queue and no claim.\n *\n * The endpoint honours the optimistic-lock header explicitly (`UserTask` is\n * outside the curated CRUD lock list), so the row's `updatedAt` rides along:\n * two administrators racing to take the same task get a 409 rather than a\n * silent last-write-wins.\n */\n const handleReassign = React.useCallback(\n async (target: ReassignTaskTarget) => {\n if (!task) return\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(task.updatedAt), () =>\n apiCall(`/api/workflows/tasks/${params.id}/reassign`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(target),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'reassign', ...target },\n })\n if (result.ok) {\n setReassignOpen(false)\n flash(t('workflows.tasks.messages.reassigned'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string } | null\n flash(failure?.error || t('workflows.tasks.messages.reassignFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error reassigning task', { err })\n flash(t('workflows.tasks.messages.reassignFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [mutationContext, params.id, refreshTask, runMutation, t, task],\n )\n\n const handleNextTask = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall<{ data: { id: string } | null }>('/api/workflows/work-inbox/next', {\n method: 'POST',\n }),\n context: mutationContext,\n mutationPayload: { action: 'claim-next' },\n })\n if (!result.ok) {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n return\n }\n const claimed = result.result?.data ?? null\n if (!claimed) {\n flash(t('workflows.workInbox.messages.nothingClaimable'), 'info')\n return\n }\n flash(t('workflows.tasks.messages.claimed'), 'success')\n router.push(buildWorkInboxItemHref(claimed.id))\n } catch (err) {\n logger.error('Error claiming the next task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, router, runMutation, t])\n\n const getStatusBadgeClass = React.useCallback(\n (status: UserTaskStatus) => describeWorkInboxStatus(status).badge,\n [],\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex items-center justify-center py-12\">\n <Spinner />\n <span className=\"ml-3 text-muted-foreground\">{t('workflows.tasks.detail.loading')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('workflows.tasks.detail.notFound', 'Task not found')}\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.detail.backToList', 'Back to Tasks')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !task) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={(error as Error | null)?.message ?? t('workflows.tasks.detail.loadFailed', 'Failed to load task')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList', 'Back to Tasks')}</Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const isCompletable = task.status === 'PENDING' || task.status === 'IN_PROGRESS'\n const isOverdue = Boolean(task.dueDate) && new Date(task.dueDate as string) < new Date() && isCompletable\n const statusDescriptor = describeWorkInboxStatus(task.status)\n const priority = normalizeWorkInboxPriority(task.priority)\n const priorityDescriptor = describeWorkInboxPriority(priority)\n const decisions = task.decisions ?? []\n const contextItems = bindingsToEntityContextItems(task.entityBindings, {\n noLink: t('workflows.tasks.detail.records.noLink'),\n })\n const recordedDecisionId = readRecordedDecision(task.formData)\n const recordedDecision = recordedDecisionId\n ? decisions.find((decision) => decision.id === recordedDecisionId) ?? null\n : null\n // A task nobody owns yet, offered to a role queue, is the one this operator\n // can take. An individually assigned task is already theirs to finish.\n //\n // Kept ONLY as the compatibility fallback below \u2014 it is the pre-change\n // behaviour, restored verbatim when the server sends no `canClaim`.\n const legacyClaimable =\n task.status === 'PENDING' &&\n !task.assignedTo &&\n !task.claimedBy &&\n (task.assignedToRoles?.length ?? 0) > 0\n\n /*\n * The server decides; the page renders. `??` is the additive-compatibility\n * fallback for a payload written before these fields existed\n * (BACKWARD_COMPATIBILITY.md \u00A77) \u2014 it restores exactly the pre-change\n * behaviour and is never a second copy of the \u00A76.4 rule. Release and reassign\n * are new affordances with no legacy behaviour to preserve, so an old payload\n * simply does not offer them.\n */\n const canComplete = task.canComplete ?? isCompletable\n const canClaim = task.canClaim ?? legacyClaimable\n const canRelease = task.canRelease ?? false\n const canReassign = task.canReassign ?? false\n const blockDescriptor = canComplete\n ? null\n : describeTaskActionBlock(task.actBlockedReason ?? 'not-workable')\n\n if (isMobile) {\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-4\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n />\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n <MobileTaskForm\n task={task}\n formData={formData}\n comments={comments}\n submitting={submitting}\n isCompletable={canComplete}\n isOverdue={isOverdue}\n onFieldChange={handleFieldChange}\n onCommentsChange={setComments}\n onSubmit={handleSubmit}\n onCancel={() => router.push(WORK_INBOX_HREF)}\n getStatusBadgeClass={getStatusBadgeClass}\n decisions={decisions}\n onDecision={(decisionId) => void completeTask(decisionId)}\n />\n </div>\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n entityTypeLabel={t('workflows.tasks.detail.type', 'User task')}\n title={task.taskName}\n subtitle={task.description || undefined}\n statusBadge={\n <span className=\"inline-flex items-center gap-2\">\n <StatusChip\n descriptor={statusDescriptor}\n label={t(`workflows.tasks.statuses.${task.status}`, task.status)}\n />\n {priorityDescriptor && priority ? (\n <StatusChip\n descriptor={priorityDescriptor}\n label={t(`workflows.workInbox.priorities.${priority}`, priority)}\n />\n ) : null}\n {isOverdue ? (\n <StatusChip\n descriptor={WORK_INBOX_OVERDUE_TONE}\n label={t('workflows.tasks.overdue')}\n />\n ) : null}\n </span>\n }\n />\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Left \u2014 instructions, form, decisions */}\n <div className=\"space-y-6 lg:col-span-2\">\n {isOverdue ? (\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-3\">\n <p className=\"text-sm text-status-error-text font-medium\">\n {t('workflows.tasks.detail.overdueWarning')}\n </p>\n </div>\n ) : null}\n\n {task.description ? (\n <div className=\"rounded-lg border border-border p-4\">\n <h2 className=\"text-sm font-semibold mb-2\">\n {t('workflows.tasks.detail.sections.instructions')}\n </h2>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.description}</p>\n </div>\n ) : null}\n\n {/*\n Where a module that OWNS the thing a task is about renders it in\n its own vocabulary. The agent orchestrator mounts the proposal\n draft card here (spec \u00A77.6): this page can only walk a task's\n `formSchema` generically, and a proposal rendered that way is a\n key/value dump of the mutation an operator is being asked to\n approve. Additive \u2014 no widget, nothing renders.\n */}\n <InjectionSpot spotId={TASK_DETAIL_INJECTION_SPOT_ID} context={taskInjectionContext} />\n\n {canClaim ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.claimPrompt')}\n </p>\n <Button\n type=\"button\"\n data-testid=\"task-claim\"\n disabled={submitting}\n onClick={() => void handleClaim()}\n >\n {t('workflows.tasks.actions.claim')}\n </Button>\n </div>\n ) : null}\n\n {canRelease ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.releasePrompt')}\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-release\"\n disabled={submitting}\n onClick={() => void handleRelease()}\n >\n {t('workflows.tasks.actions.unclaim')}\n </Button>\n </div>\n ) : null}\n\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n\n {/*\n Offered even when the task IS completable: an administrator may\n legitimately hand a row to somebody else, and the endpoint gates\n on visibility rather than ownership precisely so it can.\n */}\n {canComplete && canReassign ? (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-reassign\"\n disabled={submitting}\n onClick={() => setReassignOpen(true)}\n >\n {t('workflows.tasks.actions.reassign')}\n </Button>\n </div>\n ) : null}\n\n {canComplete ? (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <TaskFormFields\n formSchema={task.formSchema}\n values={formData}\n invalidField={invalidField}\n disabled={submitting}\n onFieldChange={handleFieldChange}\n formKey={task.formKey}\n taskId={task.id}\n taskName={task.taskName}\n entityBindings={task.entityBindings}\n decisions={decisions}\n onValuesChange={setFormData}\n />\n\n <Separator />\n\n <div className=\"space-y-2\">\n <label htmlFor=\"comments\" className=\"block text-sm font-medium text-foreground\">\n {t('workflows.tasks.detail.comments')} ({t('workflows.tasks.detail.optional')})\n </label>\n <textarea\n id=\"comments\"\n value={comments}\n onChange={(e) => setComments(e.target.value)}\n rows={3}\n className=\"w-full px-3 py-2 border border-border rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n placeholder={t('workflows.tasks.detail.commentsPlaceholder')}\n />\n </div>\n\n {decisions.length > 0 ? (\n <div className=\"space-y-3\" data-testid=\"task-decisions\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.decisions')}\n </h2>\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap\">\n {decisions.map((decision) => (\n <Button\n key={decision.id}\n type=\"button\"\n data-testid={`task-decision-${decision.id}`}\n variant={decisionButtonVariant(decision.style)}\n disabled={submitting}\n onClick={() => void completeTask(decision.id)}\n className=\"w-full sm:w-auto\"\n >\n {flattenTaskText(decision.label) ?? decision.id}\n </Button>\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-3 pt-4 sm:flex-row sm:items-center\">\n <Button type=\"submit\" data-testid=\"task-complete\" disabled={submitting} className=\"w-full sm:w-auto\">\n {submitting\n ? t('workflows.tasks.detail.submitting')\n : t('workflows.tasks.detail.completeTask')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => router.push(WORK_INBOX_HREF)}\n disabled={submitting}\n className=\"w-full sm:w-auto\"\n >\n {t('common.cancel')}\n </Button>\n </div>\n )}\n </form>\n ) : null}\n\n {task.status === 'COMPLETED' ? (\n <div className=\"space-y-4\">\n {recordedDecisionId ? (\n <div className=\"rounded-lg border border-border p-4\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.decisionTaken')}:{' '}\n <span className=\"text-foreground font-medium\">\n {recordedDecision\n ? flattenTaskText(recordedDecision.label) ?? recordedDecisionId\n : recordedDecisionId}\n </span>\n </p>\n </div>\n ) : null}\n\n {task.formData ? (\n <JsonDisplay\n data={task.formData}\n title={t('workflows.tasks.detail.sections.submittedData')}\n maxInitialDepth={2}\n />\n ) : null}\n\n {task.comments ? (\n <div className=\"bg-muted/50 rounded-lg p-4\">\n <p className=\"text-sm font-medium text-foreground mb-2\">\n {t('workflows.tasks.detail.comments')}:\n </p>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.comments}</p>\n </div>\n ) : null}\n\n {/* Spec \u00A76.2: keep frontline flow going with one click. */}\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.nextTaskPrompt')}\n </p>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n data-testid=\"task-next\"\n disabled={submitting}\n onClick={() => void handleNextTask()}\n >\n {t('workflows.workInbox.actions.claimNext')}\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList')}</Link>\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n </div>\n\n {/* Right \u2014 the records this task is about, plus how it got here */}\n <div className=\"space-y-4\">\n <EntityContextPanel\n testId=\"task-context-records\"\n title={t('workflows.tasks.detail.sections.records')}\n items={contextItems}\n emptyLabel={t('workflows.tasks.detail.records.empty')}\n linkLabel={t('workflows.tasks.detail.records.open')}\n />\n\n <div className=\"rounded-lg border border-border p-4 space-y-2\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.taskInfo')}\n </h2>\n <div className=\"space-y-1 text-sm\">\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.createdAt')}:</span>\n <span className=\"ml-2 text-foreground\">{new Date(task.createdAt).toLocaleString()}</span>\n </div>\n {task.dueDate ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.dueDate')}:</span>\n <span\n className={`ml-2 ${isOverdue ? `${WORK_INBOX_OVERDUE_TONE.text} font-medium` : 'text-foreground'}`}\n >\n {new Date(task.dueDate).toLocaleString()}\n </span>\n </div>\n ) : null}\n {task.assignedTo ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.detail.assignedTo')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedTo}</span>\n </div>\n ) : null}\n {task.assignedToRoles?.length ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.roles')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedToRoles.join(', ')}</span>\n </div>\n ) : null}\n {task.claimedBy ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.claimedBy')}:</span>\n <span className=\"ml-2 text-foreground\">{task.claimedBy}</span>\n </div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">\n {t('workflows.tasks.detail.workflowInstance')}:\n </span>\n <Link\n href={`/backend/instances/${task.workflowInstanceId}`}\n className=\"ml-2 text-primary hover:underline text-xs font-mono\"\n >\n {task.workflowInstanceId.slice(0, 8)}...\n </Link>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Task detail \u2014 decision beside context (spec \u00A76.2).\n *\n * > \"Task detail = decision + context side-by-side: left \u2014 instructions, form,\n * > decision buttons; right \u2014 bound entity card(s) with deep links. [\u2026] After\n * > completion: 'next task' \u2014 a one-click claim-next affordance.\"\n *\n * Three things about this page are load-bearing:\n *\n * - **The decision buttons are DERIVED, never stored.** The API re-resolves them\n * per request from the instance's pinned definition\n * (`lib/task-decisions.ts`), so no column carries the button list and a\n * definition that gains a decision immediately offers it. Pressing one both\n * completes the task and selects the outgoing route by the decision's durable\n * `transitionId`; WHICH button was pressed is recorded server-side as part of\n * the completion payload.\n * - **Every write goes through `useGuardedMutation`.** This page cannot use\n * `CrudForm`, which is exactly the case the project rule names, and the\n * mutation context carries `retryLastMutation` so the shared conflict/retry\n * bar can re-run the last attempt.\n * - **Status is never colour-only** \u2014 every badge pairs its design-system token\n * with an icon and a translated label (`lib/work-inbox/presentation.ts`).\n * - **Every affordance is gated by what the SERVER said** (`canComplete` /\n * `canClaim` / `canRelease` / `canReassign`, decided by `lib/task-visibility.ts`\n * and sent on the detail response). The page never re-derives \u00A76.4: a second\n * copy of that rule is how a `workflows.tasks.view_all` administrator came to\n * be offered a Complete button that always answered 409. When an action is\n * unavailable the page states the server's reason and, where \u00A76.4 names one,\n * offers the remedy \u2014 reassignment.\n */\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 { Button } from '@open-mercato/ui/primitives/button'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Separator } from '@open-mercato/ui/primitives/separator'\nimport { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MobileTaskForm } from '../../../components/mobile/MobileTaskForm'\nimport { StatusChip } from '../../../components/work-inbox/StatusChip'\nimport { TaskActionNotice } from '../../../components/work-inbox/TaskActionNotice'\nimport {\n ReassignTaskDialog,\n type ReassignTaskTarget,\n} from '../../../components/work-inbox/ReassignTaskDialog'\nimport { TaskFormFields } from '../../../components/work-inbox/TaskFormFields'\nimport { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'\nimport type { UserTaskDecision, UserTaskResponse, UserTaskStatus } from '../../../data/types'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n TASK_DETAIL_INJECTION_SPOT_ID,\n WORK_INBOX_HREF,\n buildWorkInboxItemHref,\n} from '../../../lib/work-inbox/navigation'\nimport {\n EntityContextPanel,\n bindingsToEntityContextItems,\n} from '../../../components/work-inbox/EntityContextPanel'\nimport {\n WORK_INBOX_OVERDUE_TONE,\n describeTaskActionBlock,\n describeWorkInboxPriority,\n describeWorkInboxStatus,\n} from '../../../lib/work-inbox/presentation'\nimport { normalizeWorkInboxPriority } from '../../../lib/work-inbox/provider'\nimport { flattenTaskText } from '../../../lib/task-resolution'\nimport { readRecordedDecision } from '../../../lib/task-decisions'\nimport { normalizeTaskFormSchema } from '../../../lib/task-form-schema'\n\nconst logger = createLogger('workflows')\n\ntype TaskMutationContext = {\n formId: string\n taskId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n}\n\nfunction decisionButtonVariant(style: UserTaskDecision['style']): 'default' | 'outline' | 'destructive' {\n if (style === 'destructive') return 'destructive'\n if (style === 'secondary') return 'outline'\n return 'default'\n}\n\nexport default function UserTaskDetailPage({ params }: { params: { id: string } }) {\n const router = useRouter()\n const t = useT()\n const queryClient = useQueryClient()\n const isMobile = useIsMobile()\n const [formData, setFormData] = React.useState<Record<string, string | number | boolean>>({})\n const [comments, setComments] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n const [reassignOpen, setReassignOpen] = React.useState(false)\n // Tracks the first required field that failed validation so we can mark the\n // field with aria-invalid + a red ring (Radix Select can't carry HTML\n // `required`, so we enforce constraint validation in JS instead).\n const [invalidField, setInvalidField] = React.useState<string | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<TaskMutationContext>({\n contextId: `workflows-task:${params.id}`,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const { data: task, isLoading, error } = useQuery({\n queryKey: ['workflow-task', params.id],\n queryFn: async () => {\n const result = await apiCall<{ data: UserTaskResponse }>(\n `/api/workflows/tasks/${params.id}`\n )\n if (!result.ok) {\n const httpErr = new Error(\n result.status === 404\n ? t('workflows.tasks.detail.notFound', 'Task not found')\n : t('workflows.tasks.detail.loadFailed', 'Failed to load task')\n ) as Error & { status: number }\n httpErr.status = result.status\n throw httpErr\n }\n return result.result?.data ?? null\n },\n })\n\n const mutationContext = React.useMemo<TaskMutationContext>(\n () => ({\n formId: `workflows-task:${params.id}`,\n taskId: params.id,\n resourceKind: 'workflows.user_task',\n resourceId: params.id,\n retryLastMutation,\n }),\n [params.id, retryLastMutation],\n )\n\n // Injection spot for modules that OWN what a task is about (spec \u00A77.6). It\n // is a CONTRACT SURFACE (BACKWARD_COMPATIBILITY.md \u00A76): the id is frozen once\n // a third party mounts a widget on it.\n const taskInjectionContext = React.useMemo(\n () => ({\n taskId: params.id,\n workflowInstanceId: task?.workflowInstanceId ?? null,\n status: task?.status ?? null,\n formSchema: task?.formSchema ?? null,\n formData: task?.formData ?? null,\n retryLastMutation,\n }),\n [params.id, retryLastMutation, task?.formData, task?.formSchema, task?.status, task?.workflowInstanceId],\n )\n\n const refreshTask = React.useCallback(() => {\n queryClient.invalidateQueries({ queryKey: ['workflow-task', params.id] })\n }, [params.id, queryClient])\n\n const handleFieldChange = (fieldName: string, value: string | number | boolean) => {\n setFormData(prev => ({\n ...prev,\n [fieldName]: value,\n }))\n // Clear invalid state once the user touches the offending field.\n if (invalidField === fieldName) setInvalidField(null)\n }\n\n const validateRequiredFields = React.useCallback((): boolean => {\n // Normalized, so a Studio-authored `{ fields: [...] }` schema is validated\n // by the same rule the server enforces (`lib/task-form-schema.ts`).\n const normalizedSchema = normalizeTaskFormSchema(task?.formSchema)\n if (!normalizedSchema?.required) return true\n for (const requiredField of normalizedSchema.required) {\n if (!formData[requiredField] || formData[requiredField] === '') {\n const fieldSchema = normalizedSchema.properties?.[requiredField]\n const fieldLabel = fieldSchema?.title ?? requiredField\n flash(t('workflows.tasks.detail.validation.requiredField', { field: fieldLabel }), 'error')\n setInvalidField(requiredField)\n // Scroll + focus the trigger so the user sees what's missing.\n if (typeof document !== 'undefined') {\n const trigger = document.getElementById(requiredField)\n trigger?.scrollIntoView({ behavior: 'smooth', block: 'center' })\n trigger?.focus()\n }\n return false\n }\n }\n setInvalidField(null)\n return true\n }, [formData, task, t])\n\n const completeTask = React.useCallback(\n async (decisionId?: string) => {\n if (!task) return\n if (!validateRequiredFields()) return\n\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall(`/api/workflows/tasks/${params.id}/complete`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n formData,\n comments: comments || undefined,\n ...(decisionId ? { decisionId } : {}),\n }),\n }),\n context: mutationContext,\n mutationPayload: { formData, comments, decisionId },\n })\n\n if (result.ok) {\n flash(t('workflows.tasks.messages.completed'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string; code?: string } | null\n const failureMessage =\n failure?.code === 'WORKFLOW_NOT_ACTIVE'\n ? t('workflows.tasks.messages.workflowNotActive')\n : failure?.error || t('workflows.tasks.messages.completeFailed')\n flash(failureMessage, 'error')\n }\n } catch (err) {\n logger.error('Error completing task', { err })\n flash(t('workflows.tasks.messages.completeFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [comments, formData, mutationContext, params.id, refreshTask, runMutation, t, task, validateRequiredFields],\n )\n\n const handleSubmit = React.useCallback(\n (event: React.FormEvent) => {\n event.preventDefault()\n void completeTask()\n },\n [completeTask],\n )\n\n const handleClaim = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/claim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'claim' },\n })\n if (result.ok) {\n flash(t('workflows.tasks.messages.claimed'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error claiming task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n const handleRelease = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () => apiCall(`/api/workflows/tasks/${params.id}/unclaim`, { method: 'POST' }),\n context: mutationContext,\n mutationPayload: { action: 'unclaim' },\n })\n if (result.ok) {\n flash(t('workflows.workInbox.messages.released'), 'success')\n refreshTask()\n } else {\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error releasing task', { err })\n flash(t('workflows.workInbox.messages.releaseFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, params.id, refreshTask, runMutation, t])\n\n /**\n * Reassignment is the remedy \u00A76.4 names for every ownership refusal, and the\n * only way to rescue a task authored with no assignee, no queue and no claim.\n *\n * The endpoint honours the optimistic-lock header explicitly (`UserTask` is\n * outside the curated CRUD lock list), so the row's `updatedAt` rides along:\n * two administrators racing to take the same task get a 409 rather than a\n * silent last-write-wins.\n */\n const handleReassign = React.useCallback(\n async (target: ReassignTaskTarget) => {\n if (!task) return\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(task.updatedAt), () =>\n apiCall(`/api/workflows/tasks/${params.id}/reassign`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(target),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'reassign', ...target },\n })\n if (result.ok) {\n setReassignOpen(false)\n flash(t('workflows.tasks.messages.reassigned'), 'success')\n refreshTask()\n } else {\n const failure = result.result as { error?: string } | null\n flash(failure?.error || t('workflows.tasks.messages.reassignFailed'), 'error')\n }\n } catch (err) {\n logger.error('Error reassigning task', { err })\n flash(t('workflows.tasks.messages.reassignFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n },\n [mutationContext, params.id, refreshTask, runMutation, t, task],\n )\n\n const handleNextTask = React.useCallback(async () => {\n setSubmitting(true)\n try {\n const result = await runMutation({\n operation: () =>\n apiCall<{ data: { id: string } | null }>('/api/workflows/work-inbox/next', {\n method: 'POST',\n }),\n context: mutationContext,\n mutationPayload: { action: 'claim-next' },\n })\n if (!result.ok) {\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n return\n }\n const claimed = result.result?.data ?? null\n if (!claimed) {\n flash(t('workflows.workInbox.messages.nothingClaimable'), 'info')\n return\n }\n flash(t('workflows.tasks.messages.claimed'), 'success')\n router.push(buildWorkInboxItemHref(claimed.id))\n } catch (err) {\n logger.error('Error claiming the next task', { err })\n flash(t('workflows.tasks.messages.claimFailed'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [mutationContext, router, runMutation, t])\n\n const getStatusBadgeClass = React.useCallback(\n (status: UserTaskStatus) => describeWorkInboxStatus(status).badge,\n [],\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex items-center justify-center py-12\">\n <Spinner />\n <span className=\"ml-3 text-muted-foreground\">{t('workflows.tasks.detail.loading')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('workflows.tasks.detail.notFound', 'Task not found')}\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.detail.backToList', 'Back to Tasks')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !task) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={(error as Error | null)?.message ?? t('workflows.tasks.detail.loadFailed', 'Failed to load task')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList', 'Back to Tasks')}</Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const isCompletable = task.status === 'PENDING' || task.status === 'IN_PROGRESS'\n const isOverdue = Boolean(task.dueDate) && new Date(task.dueDate as string) < new Date() && isCompletable\n const statusDescriptor = describeWorkInboxStatus(task.status)\n const priority = normalizeWorkInboxPriority(task.priority)\n const priorityDescriptor = describeWorkInboxPriority(priority)\n const decisions = task.decisions ?? []\n const contextItems = bindingsToEntityContextItems(task.entityBindings, {\n noLink: t('workflows.tasks.detail.records.noLink'),\n })\n const recordedDecisionId = readRecordedDecision(task.formData)\n const recordedDecision = recordedDecisionId\n ? decisions.find((decision) => decision.id === recordedDecisionId) ?? null\n : null\n // A task nobody owns yet, offered to a role queue, is the one this operator\n // can take. An individually assigned task is already theirs to finish.\n //\n // Kept ONLY as the compatibility fallback below \u2014 it is the pre-change\n // behaviour, restored verbatim when the server sends no `canClaim`.\n const legacyClaimable =\n task.status === 'PENDING' &&\n !task.assignedTo &&\n !task.claimedBy &&\n (task.assignedToRoles?.length ?? 0) > 0\n\n /*\n * The server decides; the page renders. `??` is the additive-compatibility\n * fallback for a payload written before these fields existed\n * (BACKWARD_COMPATIBILITY.md \u00A77) \u2014 it restores exactly the pre-change\n * behaviour and is never a second copy of the \u00A76.4 rule. Release and reassign\n * are new affordances with no legacy behaviour to preserve, so an old payload\n * simply does not offer them.\n */\n const canComplete = task.canComplete ?? isCompletable\n const canClaim = task.canClaim ?? legacyClaimable\n const canRelease = task.canRelease ?? false\n const canReassign = task.canReassign ?? false\n const blockDescriptor = canComplete\n ? null\n : describeTaskActionBlock(task.actBlockedReason ?? 'not-workable')\n\n if (isMobile) {\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-4\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n />\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n <MobileTaskForm\n task={task}\n formData={formData}\n comments={comments}\n submitting={submitting}\n isCompletable={canComplete}\n isOverdue={isOverdue}\n onFieldChange={handleFieldChange}\n onCommentsChange={setComments}\n onSubmit={handleSubmit}\n onCancel={() => router.push(WORK_INBOX_HREF)}\n getStatusBadgeClass={getStatusBadgeClass}\n decisions={decisions}\n onDecision={(decisionId) => void completeTask(decisionId)}\n />\n </div>\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref={WORK_INBOX_HREF}\n backLabel={t('workflows.tasks.backToList', 'Back to tasks')}\n entityTypeLabel={t('workflows.tasks.detail.type', 'User task')}\n title={task.taskName}\n subtitle={task.description || undefined}\n statusBadge={\n <span className=\"inline-flex items-center gap-2\">\n <StatusChip\n descriptor={statusDescriptor}\n label={t(`workflows.tasks.statuses.${task.status}`, task.status)}\n />\n {priorityDescriptor && priority ? (\n <StatusChip\n descriptor={priorityDescriptor}\n label={t(`workflows.workInbox.priorities.${priority}`, priority)}\n />\n ) : null}\n {isOverdue ? (\n <StatusChip\n descriptor={WORK_INBOX_OVERDUE_TONE}\n label={t('workflows.tasks.overdue')}\n />\n ) : null}\n </span>\n }\n />\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Left \u2014 instructions, form, decisions */}\n <div className=\"space-y-6 lg:col-span-2\">\n {isOverdue ? (\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-3\">\n <p className=\"text-sm text-status-error-text font-medium\">\n {t('workflows.tasks.detail.overdueWarning')}\n </p>\n </div>\n ) : null}\n\n {task.description ? (\n <div className=\"rounded-lg border border-border p-4\">\n <h2 className=\"text-sm font-semibold mb-2\">\n {t('workflows.tasks.detail.sections.instructions')}\n </h2>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.description}</p>\n </div>\n ) : null}\n\n {/*\n Where a module that OWNS the thing a task is about renders it in\n its own vocabulary. The agent orchestrator mounts the proposal\n draft card here (spec \u00A77.6): this page can only walk a task's\n `formSchema` generically, and a proposal rendered that way is a\n key/value dump of the mutation an operator is being asked to\n approve. Additive \u2014 no widget, nothing renders.\n */}\n <InjectionSpot spotId={TASK_DETAIL_INJECTION_SPOT_ID} context={taskInjectionContext} />\n\n {canClaim ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.claimPrompt')}\n </p>\n <Button\n type=\"button\"\n data-testid=\"task-claim\"\n disabled={submitting}\n onClick={() => void handleClaim()}\n >\n {t('workflows.tasks.actions.claim')}\n </Button>\n </div>\n ) : null}\n\n {canRelease ? (\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.releasePrompt')}\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-release\"\n disabled={submitting}\n onClick={() => void handleRelease()}\n >\n {t('workflows.tasks.actions.unclaim')}\n </Button>\n </div>\n ) : null}\n\n {blockDescriptor ? (\n <TaskActionNotice\n descriptor={blockDescriptor}\n canReassign={canReassign}\n disabled={submitting}\n onReassign={() => setReassignOpen(true)}\n />\n ) : null}\n\n {/*\n Offered even when the task IS completable: an administrator may\n legitimately hand a row to somebody else, and the endpoint gates\n on visibility rather than ownership precisely so it can.\n */}\n {canComplete && canReassign ? (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"outline\"\n data-testid=\"task-reassign\"\n disabled={submitting}\n onClick={() => setReassignOpen(true)}\n >\n {t('workflows.tasks.actions.reassign')}\n </Button>\n </div>\n ) : null}\n\n {canComplete ? (\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <TaskFormFields\n formSchema={task.formSchema}\n values={formData}\n invalidField={invalidField}\n disabled={submitting}\n onFieldChange={handleFieldChange}\n formKey={task.formKey}\n taskId={task.id}\n taskName={task.taskName}\n entityBindings={task.entityBindings}\n decisions={decisions}\n onValuesChange={setFormData}\n />\n\n <Separator />\n\n <div className=\"space-y-2\">\n <label htmlFor=\"comments\" className=\"block text-sm font-medium text-foreground\">\n {t('workflows.tasks.detail.comments')} ({t('workflows.tasks.detail.optional')})\n </label>\n <textarea\n id=\"comments\"\n value={comments}\n onChange={(e) => setComments(e.target.value)}\n rows={3}\n className=\"w-full px-3 py-2 border border-border rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n placeholder={t('workflows.tasks.detail.commentsPlaceholder')}\n />\n </div>\n\n {decisions.length > 0 ? (\n <div className=\"space-y-3\" data-testid=\"task-decisions\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.decisions')}\n </h2>\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap\">\n {decisions.map((decision) => (\n <Button\n key={decision.id}\n type=\"button\"\n data-testid={`task-decision-${decision.id}`}\n variant={decisionButtonVariant(decision.style)}\n disabled={submitting}\n onClick={() => void completeTask(decision.id)}\n className=\"w-full sm:w-auto\"\n >\n {flattenTaskText(decision.label) ?? decision.id}\n </Button>\n ))}\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-3 pt-4 sm:flex-row sm:items-center\">\n <Button type=\"submit\" data-testid=\"task-complete\" disabled={submitting} className=\"w-full sm:w-auto\">\n {submitting\n ? t('workflows.tasks.detail.submitting')\n : t('workflows.tasks.detail.completeTask')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => router.push(WORK_INBOX_HREF)}\n disabled={submitting}\n className=\"w-full sm:w-auto\"\n >\n {t('common.cancel')}\n </Button>\n </div>\n )}\n </form>\n ) : null}\n\n {task.status === 'COMPLETED' ? (\n <div className=\"space-y-4\">\n {recordedDecisionId ? (\n <div className=\"rounded-lg border border-border p-4\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.decisionTaken')}:{' '}\n <span className=\"text-foreground font-medium\">\n {recordedDecision\n ? flattenTaskText(recordedDecision.label) ?? recordedDecisionId\n : recordedDecisionId}\n </span>\n </p>\n </div>\n ) : null}\n\n {task.formData ? (\n <JsonDisplay\n data={task.formData}\n title={t('workflows.tasks.detail.sections.submittedData')}\n maxInitialDepth={2}\n />\n ) : null}\n\n {task.comments ? (\n <div className=\"bg-muted/50 rounded-lg p-4\">\n <p className=\"text-sm font-medium text-foreground mb-2\">\n {t('workflows.tasks.detail.comments')}:\n </p>\n <p className=\"text-sm text-muted-foreground whitespace-pre-wrap\">{task.comments}</p>\n </div>\n ) : null}\n\n {/* Spec \u00A76.2: keep frontline flow going with one click. */}\n <div className=\"rounded-lg border border-border bg-muted/50 p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {t('workflows.tasks.detail.nextTaskPrompt')}\n </p>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n data-testid=\"task-next\"\n disabled={submitting}\n onClick={() => void handleNextTask()}\n >\n {t('workflows.workInbox.actions.claimNext')}\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={WORK_INBOX_HREF}>{t('workflows.tasks.detail.backToList')}</Link>\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n </div>\n\n {/* Right \u2014 the records this task is about, plus how it got here */}\n <div className=\"space-y-4\">\n <EntityContextPanel\n testId=\"task-context-records\"\n title={t('workflows.tasks.detail.sections.records')}\n items={contextItems}\n emptyLabel={t('workflows.tasks.detail.records.empty')}\n linkLabel={t('workflows.tasks.detail.records.open')}\n />\n\n <div className=\"rounded-lg border border-border p-4 space-y-2\">\n <h2 className=\"text-sm font-semibold\">\n {t('workflows.tasks.detail.sections.taskInfo')}\n </h2>\n <div className=\"space-y-1 text-sm\">\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.createdAt')}:</span>\n <span className=\"ml-2 text-foreground\">{new Date(task.createdAt).toLocaleString()}</span>\n </div>\n {task.dueDate ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.fields.dueDate')}:</span>\n <span\n className={`ml-2 ${isOverdue ? `${WORK_INBOX_OVERDUE_TONE.text} font-medium` : 'text-foreground'}`}\n >\n {new Date(task.dueDate).toLocaleString()}\n </span>\n </div>\n ) : null}\n {task.assignedTo ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.detail.assignedTo')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedTo}</span>\n </div>\n ) : null}\n {task.assignedToRoles?.length ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.roles')}:</span>\n <span className=\"ml-2 text-foreground\">{task.assignedToRoles.join(', ')}</span>\n </div>\n ) : null}\n {task.claimedBy ? (\n <div>\n <span className=\"text-muted-foreground\">{t('workflows.tasks.claimedBy')}:</span>\n <span className=\"ml-2 text-foreground\">{task.claimedBy}</span>\n </div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">\n {t('workflows.tasks.detail.workflowInstance')}:\n </span>\n <Link\n href={`/backend/instances/${task.workflowInstanceId}`}\n className=\"ml-2 text-primary hover:underline text-xs font-mono\"\n >\n {task.workflowInstanceId.slice(0, 8)}...\n </Link>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <ReassignTaskDialog\n open={reassignOpen}\n submitting={submitting}\n onOpenChange={setReassignOpen}\n onSubmit={(target) => void handleReassign(target)}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const RUN_STATUSES_CLOSED_TO_USER_TASKS = [
|
|
2
|
+
"COMPLETED",
|
|
3
|
+
"FAILED",
|
|
4
|
+
"CANCELLED",
|
|
5
|
+
"COMPENSATING",
|
|
6
|
+
"COMPENSATED"
|
|
7
|
+
];
|
|
8
|
+
function isRunClosedToUserTasks(status) {
|
|
9
|
+
if (!status) return false;
|
|
10
|
+
return RUN_STATUSES_CLOSED_TO_USER_TASKS.includes(status);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
RUN_STATUSES_CLOSED_TO_USER_TASKS,
|
|
14
|
+
isRunClosedToUserTasks
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=instance-status.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/workflows/lib/instance-status.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Workflow instance statuses a user-task completion or a user-task SLA job must\n * not touch.\n *\n * PURE: a type-only import, so the engine does not depend on the metrics\n * module's `WORKFLOW_TERMINAL_STATUSES` for a runtime decision.\n *\n * The name means \"a completion must not touch this run\", NOT \"its tasks are\n * closed\": a FAILED run keeps its open task so a retry can pick it up.\n * `COMPENSATING` is included although it is not terminal \u2014 `compensateWorkflow`\n * holds it while rollback activities run and then overwrites it with\n * `COMPENSATED` or `FAILED`, and a completion in that window would advance a\n * run that is being rolled back.\n */\n\nimport type { WorkflowInstanceStatus } from '../data/entities'\n\nexport const RUN_STATUSES_CLOSED_TO_USER_TASKS: readonly WorkflowInstanceStatus[] = [\n 'COMPLETED',\n 'FAILED',\n 'CANCELLED',\n 'COMPENSATING',\n 'COMPENSATED',\n]\n\nexport function isRunClosedToUserTasks(status: string | null | undefined): boolean {\n if (!status) return false\n return (RUN_STATUSES_CLOSED_TO_USER_TASKS as readonly string[]).includes(status)\n}\n"],
|
|
5
|
+
"mappings": "AAiBO,MAAM,oCAAuE;AAAA,EAClF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,uBAAuB,QAA4C;AACjF,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAQ,kCAAwD,SAAS,MAAM;AACjF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./task-decisions.js";
|
|
14
14
|
import { loadTaskDecisionContext } from "./task-decision-context.js";
|
|
15
15
|
import { readRequiredFormFields } from "./task-form-schema.js";
|
|
16
|
+
import { isRunClosedToUserTasks } from "./instance-status.js";
|
|
16
17
|
import * as stepHandler from "./step-handler.js";
|
|
17
18
|
import * as transitionHandler from "./transition-handler.js";
|
|
18
19
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
@@ -67,6 +68,21 @@ async function completeUserTask(em, container, options) {
|
|
|
67
68
|
{ taskId, decisionId }
|
|
68
69
|
);
|
|
69
70
|
}
|
|
71
|
+
const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId);
|
|
72
|
+
if (!instance) {
|
|
73
|
+
throw new UserTaskError(
|
|
74
|
+
"Workflow instance not found",
|
|
75
|
+
"INSTANCE_NOT_FOUND",
|
|
76
|
+
{ workflowInstanceId: task.workflowInstanceId }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (isRunClosedToUserTasks(instance.status)) {
|
|
80
|
+
throw new UserTaskError(
|
|
81
|
+
"Workflow is no longer active",
|
|
82
|
+
"WORKFLOW_NOT_ACTIVE",
|
|
83
|
+
{ taskId, workflowInstanceId: instance.id, status: instance.status }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
70
86
|
const completionData = withRecordedDecision(formData, chosenDecision?.id);
|
|
71
87
|
const now = /* @__PURE__ */ new Date();
|
|
72
88
|
task.status = "COMPLETED";
|
|
@@ -76,14 +92,6 @@ async function completeUserTask(em, container, options) {
|
|
|
76
92
|
task.comments = comments || null;
|
|
77
93
|
task.updatedAt = now;
|
|
78
94
|
await em.flush();
|
|
79
|
-
const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId);
|
|
80
|
-
if (!instance) {
|
|
81
|
-
throw new UserTaskError(
|
|
82
|
-
"Workflow instance not found",
|
|
83
|
-
"INSTANCE_NOT_FOUND",
|
|
84
|
-
{ workflowInstanceId: task.workflowInstanceId }
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
95
|
if (task.branchInstanceId) {
|
|
88
96
|
await logWorkflowEvent(em, {
|
|
89
97
|
workflowInstanceId: instance.id,
|