@open-mercato/core 0.10.1-develop.7666.1.04d86c2dc5 → 0.10.1-develop.7669.1.120a2cf7f1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/api/todos/route.js +5 -3
- package/dist/modules/customers/api/todos/route.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js +3 -3
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
- package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js +12 -1
- package/dist/modules/staff/api/timesheets/reports/[id]/export/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-reports/reportExport.js +31 -21
- package/dist/modules/staff/lib/timesheets-reports/reportExport.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/api/todos/route.ts +16 -8
- package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +3 -3
- package/src/modules/staff/api/timesheets/reports/[id]/export/route.ts +14 -1
- package/src/modules/staff/lib/timesheets-reports/reportExport.ts +33 -20
package/.turbo/turbo-build.log
CHANGED
|
@@ -410,9 +410,11 @@ async function PUT(request) {
|
|
|
410
410
|
...nextDone !== void 0 ? { status: nextDone ? "done" : "planned" } : {},
|
|
411
411
|
...customValues ? {
|
|
412
412
|
customValues,
|
|
413
|
-
priority: typeof customValues.priority === "number" ? customValues.priority : null,
|
|
414
|
-
body: typeof customValues.description === "string" ? customValues.description : null,
|
|
415
|
-
|
|
413
|
+
...customValues.priority !== void 0 ? { priority: typeof customValues.priority === "number" ? customValues.priority : null } : {},
|
|
414
|
+
...customValues.description !== void 0 ? { body: typeof customValues.description === "string" ? customValues.description : null } : {},
|
|
415
|
+
...customValues.due_at !== void 0 || customValues.dueAt !== void 0 ? {
|
|
416
|
+
scheduledAt: typeof customValues.due_at === "string" ? customValues.due_at : typeof customValues.dueAt === "string" ? customValues.dueAt : null
|
|
417
|
+
} : {}
|
|
416
418
|
} : {}
|
|
417
419
|
},
|
|
418
420
|
ctx: commandContext
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/api/todos/route.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @deprecated Use /api/customers/interactions instead. This route is maintained\n * as a compatibility bridge per SPEC-046b. Will be removed in a future release.\n */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { isUnrestrictedOrganizationScope } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { createCustomersCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../openapi'\nimport { CustomerInteraction, CustomerTodoLink } from '../../data/entities'\nimport { todoLinkWithTodoCreateSchema } from '../../data/validators'\nimport { withOperationMetadata } from '../../lib/operationMetadata'\nimport { resolveCustomerInteractionFeatureFlags } from '../../lib/interactionFeatureFlags'\nimport { resolveCustomersRequestContext } from '../../lib/interactionRequestContext'\nimport {\n EXAMPLE_TODO_SOURCE,\n CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n} from '../../lib/interactionCompatibility'\nimport {\n filterTodoRows,\n listCanonicalTodoRows,\n listLegacyTodoRows,\n normalizeTodoSearch,\n paginateTodoRows,\n sortTodoRows,\n resolveLegacyTodoDetails,\n} from '../../lib/todoCompatibility'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('customers')\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n all: z.string().optional(),\n entityId: z.string().uuid().optional(),\n})\n\nconst todoCreateBodySchema = todoLinkWithTodoCreateSchema.omit({\n tenantId: true,\n organizationId: true,\n}).passthrough()\n\nconst todoUpdateBodySchema = z\n .object({\n id: z.string().uuid(),\n linkId: z.string().uuid().optional(),\n title: z.string().trim().min(1).max(200).optional(),\n isDone: z.boolean().optional(),\n is_done: z.boolean().optional(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n customValues: z.record(z.string(), z.unknown()).optional(),\n })\n .passthrough()\n\nconst todoDeleteBodySchema = z.object({\n id: z.string().uuid(),\n todoId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n}\n\nconst DEPRECATION_HEADERS = {\n Deprecation: 'true',\n Sunset: 'Tue, 30 Jun 2026 00:00:00 GMT',\n Link: '</api/customers/interactions>; rel=\"successor-version\"',\n}\n\n// Caps the per-source fetch window used by the deprecated merged (legacy +\n// canonical bridge) read path. Keeps memory bounded on tenants with large\n// todo history; deep-pagination beyond this window is not supported here \u2014\n// use /api/customers/interactions instead.\nconst MERGED_TODO_FETCH_CAP = 2000\n\nfunction resolveGuardUserId(auth: {\n sub?: string | null\n userId?: string | null\n keyId?: string | null\n}): string {\n if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId\n if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId\n return 'system'\n}\n\nfunction withAdapterHeaders(response: Response): Response {\n const headers = new Headers(response.headers)\n Object.entries(DEPRECATION_HEADERS).forEach(([key, value]) => headers.set(key, value))\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n })\n}\n\nasync function legacyAdaptersDisabledResponse(): Promise<Response> {\n const { translate } = await resolveTranslations()\n return withAdapterHeaders(NextResponse.json(\n {\n error: translate(\n 'customers.interactions.legacyAdapters.disabled',\n 'This legacy adapter has been disabled. Use /api/customers/interactions instead.',\n ),\n },\n { status: 410 },\n ))\n}\n\nfunction normalizeTodoStatusInput(body: z.infer<typeof todoUpdateBodySchema>): boolean | undefined {\n if (typeof body.isDone === 'boolean') return body.isDone\n if (typeof body.is_done === 'boolean') return body.is_done\n return undefined\n}\n\nfunction collectTodoCustomValues(\n body: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const direct =\n body.customValues && typeof body.customValues === 'object'\n ? { ...(body.customValues as Record<string, unknown>) }\n : {}\n if (body.todoCustom && typeof body.todoCustom === 'object') {\n Object.assign(direct, body.todoCustom as Record<string, unknown>)\n }\n if (body.customFields && typeof body.customFields === 'object') {\n Object.assign(direct, body.customFields as Record<string, unknown>)\n }\n return Object.keys(direct).length > 0 ? direct : undefined\n}\n\nasync function findLegacyTodoLink(\n em: EntityManager,\n target: { linkId?: string; todoId?: string },\n tenantId: string,\n): Promise<CustomerTodoLink | null> {\n if (target.linkId) {\n const byLinkId = await em.findOne(CustomerTodoLink, { id: target.linkId, tenantId }, { populate: ['entity'] })\n if (byLinkId) return byLinkId\n }\n if (target.todoId) {\n return await em.findOne(CustomerTodoLink, { todoId: target.todoId, tenantId }, { populate: ['entity'] })\n }\n return null\n}\n\nasync function ensureCanonicalTodoBridge(\n em: EntityManager,\n queryEngine: QueryEngine,\n commandBus: CommandBus,\n commandContext: Parameters<CommandBus['execute']>[1]['ctx'],\n link: CustomerTodoLink,\n): Promise<string> {\n const existing = await em.findOne(CustomerInteraction, { id: link.todoId, tenantId: link.tenantId })\n if (existing) return existing.id\n\n const detailMap = await resolveLegacyTodoDetails(\n queryEngine,\n [link],\n link.tenantId,\n [link.organizationId],\n )\n const source =\n typeof link.todoSource === 'string' && link.todoSource.trim().length > 0\n ? link.todoSource\n : EXAMPLE_TODO_SOURCE\n const detail = detailMap.get(`${source}:${link.todoId}`) ?? null\n const entityId = typeof link.entity === 'string' ? link.entity : link.entity.id\n\n await commandBus.execute('customers.interactions.create', {\n input: {\n id: link.todoId,\n tenantId: link.tenantId,\n organizationId: link.organizationId,\n entityId,\n interactionType: 'task',\n title: detail?.title ?? null,\n body: detail?.description ?? null,\n status: detail?.isDone ? 'done' : 'planned',\n scheduledAt: detail?.dueAt ?? null,\n priority: detail?.priority ?? null,\n source: CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n ...(detail?.customValues ? { customValues: detail.customValues } : {}),\n },\n ctx: commandContext,\n })\n\n return link.todoId\n}\n\nasync function resolveCanonicalTodoTargetId(\n em: EntityManager,\n queryEngine: QueryEngine,\n commandBus: CommandBus,\n commandContext: Parameters<CommandBus['execute']>[1]['ctx'],\n target: { todoId?: string; linkId?: string },\n tenantId: string,\n): Promise<string> {\n if (target.todoId) {\n const interaction = await em.findOne(CustomerInteraction, { id: target.todoId, tenantId })\n if (interaction) return interaction.id\n }\n\n const legacyLink = await findLegacyTodoLink(em, target, tenantId)\n if (!legacyLink) {\n if (!target.todoId) throw notFound('Todo not found')\n return target.todoId\n }\n\n return ensureCanonicalTodoBridge(\n em,\n queryEngine,\n commandBus,\n commandContext,\n legacyLink,\n )\n}\n\nexport async function GET(request: Request): Promise<Response> {\n try {\n const { auth, em, organizationIds, container, selectedOrganizationId, scope } =\n await resolveCustomersRequestContext(request)\n const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams))\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const exportAll = parseBooleanToken(query.all) === true\n const isUnrestricted = isUnrestrictedOrganizationScope({\n isSuperAdmin: auth.isSuperAdmin === true,\n allowedOrganizationIds: scope?.allowedIds,\n })\n if (!isUnrestricted && (!organizationIds || organizationIds.length === 0)) {\n logger.warn('customers.todos.list collapsed organization scope', {\n tenantId: auth.tenantId,\n organizationIds,\n selectedId: scope?.selectedId ?? null,\n allowedIdsCount: scope?.allowedIds?.length ?? null,\n })\n return withAdapterHeaders(\n NextResponse.json({\n items: [],\n total: 0,\n page: exportAll ? 1 : query.page,\n pageSize: exportAll ? 0 : query.pageSize,\n totalPages: 1,\n }),\n )\n }\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const search = normalizeTodoSearch(query.search)\n\n if (flags.unified) {\n const canonical = await listCanonicalTodoRows(\n em,\n container,\n auth,\n selectedOrganizationId,\n organizationIds,\n {\n entityId: query.entityId,\n pagination: exportAll ? null : { page: query.page, pageSize: query.pageSize },\n searchText: search,\n isUnrestricted,\n },\n )\n const total = canonical.total\n return withAdapterHeaders(\n NextResponse.json({\n items: canonical.items,\n total,\n page: exportAll ? 1 : query.page,\n pageSize: exportAll ? canonical.items.length : query.pageSize,\n totalPages: exportAll ? 1 : Math.max(1, Math.ceil(total / query.pageSize)),\n }),\n )\n }\n\n const legacyWindow = exportAll\n ? null\n : Math.min(MERGED_TODO_FETCH_CAP, Math.max(query.pageSize, query.page * query.pageSize + query.pageSize))\n const [legacyRows, canonicalRows] = await Promise.all([\n listLegacyTodoRows(em, queryEngine, auth.tenantId, organizationIds, query.entityId, {\n limit: legacyWindow,\n isUnrestricted,\n }),\n listCanonicalTodoRows(\n em,\n container,\n auth,\n selectedOrganizationId,\n organizationIds,\n {\n entityId: query.entityId,\n includeDeleted: true,\n source: CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n limit: legacyWindow,\n isUnrestricted,\n },\n ),\n ])\n const mergedRows = [\n ...legacyRows.filter((row) => !canonicalRows.bridgeIds.has(row.todoId)),\n ...canonicalRows.items,\n ]\n const filteredRows = filterTodoRows(sortTodoRows(mergedRows), search)\n const paged = paginateTodoRows(filteredRows, query.page, query.pageSize, exportAll)\n\n return withAdapterHeaders(\n NextResponse.json({\n items: paged.items,\n total: paged.total,\n page: paged.page,\n pageSize: paged.pageSize,\n totalPages: paged.totalPages,\n }),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.get failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nexport async function POST(request: Request): Promise<Response> {\n try {\n const { commandContext, container, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const commandBus = container.resolve('commandBus') as CommandBus\n const body = todoCreateBodySchema.parse(await readJsonSafe<Record<string, unknown>>(request, {}))\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.entityId,\n operation: 'create',\n requestMethod: request.method,\n requestHeaders: request.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return withAdapterHeaders(NextResponse.json(guardResult.body, { status: guardResult.status }))\n }\n const customValues = collectTodoCustomValues(body as Record<string, unknown>)\n\n const { result, logEntry } = await commandBus.execute('customers.interactions.create', {\n input: {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId ?? auth.orgId,\n entityId: body.entityId,\n interactionType: 'task',\n title: body.title,\n status: body.is_done === true || body.isDone === true ? 'done' : 'planned',\n source: CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n ...(customValues ? {\n customValues,\n priority: typeof customValues.priority === 'number' ? customValues.priority : null,\n body: typeof customValues.description === 'string' ? customValues.description : null,\n scheduledAt:\n typeof customValues.due_at === 'string'\n ? customValues.due_at\n : typeof customValues.dueAt === 'string'\n ? customValues.dueAt\n : null,\n } : {}),\n },\n ctx: commandContext,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.entityId,\n operation: 'create',\n requestMethod: request.method,\n requestHeaders: request.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const interactionId =\n result &&\n typeof result === 'object' &&\n 'interactionId' in result &&\n typeof result.interactionId === 'string'\n ? result.interactionId\n : result &&\n typeof result === 'object' &&\n 'id' in result &&\n typeof result.id === 'string'\n ? result.id\n : null\n return withAdapterHeaders(\n withOperationMetadata(\n NextResponse.json(\n {\n linkId: interactionId,\n todoId: interactionId,\n },\n { status: 201 },\n ),\n logEntry,\n { resourceKind: 'customers.todoLink', resourceId: interactionId },\n ),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return withAdapterHeaders(NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.post failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nexport async function PUT(request: Request): Promise<Response> {\n try {\n const { commandContext, container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const commandBus = container.resolve('commandBus') as CommandBus\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const body = todoUpdateBodySchema.parse(await readJsonSafe<Record<string, unknown>>(request, {}))\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.linkId ?? body.id,\n operation: 'update',\n requestMethod: request.method,\n requestHeaders: request.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return withAdapterHeaders(NextResponse.json(guardResult.body, { status: guardResult.status }))\n }\n const interactionId = flags.unified\n ? body.id\n : await resolveCanonicalTodoTargetId(\n em,\n queryEngine,\n commandBus,\n commandContext,\n {\n todoId: body.id,\n linkId: body.linkId,\n },\n auth.tenantId,\n )\n const customValues = collectTodoCustomValues(body as Record<string, unknown>)\n const nextDone = normalizeTodoStatusInput(body)\n\n const { logEntry } = await commandBus.execute('customers.interactions.update', {\n input: {\n id: interactionId,\n ...(body.title !== undefined ? { title: body.title } : {}),\n ...(nextDone !== undefined ? { status: nextDone ? 'done' : 'planned' } : {}),\n ...(customValues ? {\n customValues,\n priority: typeof customValues.priority === 'number' ? customValues.priority : null,\n body: typeof customValues.description === 'string' ? customValues.description : null,\n scheduledAt:\n typeof customValues.due_at === 'string'\n ? customValues.due_at\n : typeof customValues.dueAt === 'string'\n ? customValues.dueAt\n : null,\n } : {}),\n },\n ctx: commandContext,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.linkId ?? body.id,\n operation: 'update',\n requestMethod: request.method,\n requestHeaders: request.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return withAdapterHeaders(\n withOperationMetadata(\n NextResponse.json({ ok: true }),\n logEntry,\n { resourceKind: 'customers.todoLink', resourceId: body.linkId ?? body.id },\n ),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return withAdapterHeaders(NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.put failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nexport async function DELETE(request: Request): Promise<Response> {\n try {\n const { commandContext, container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const commandBus = container.resolve('commandBus') as CommandBus\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const body = todoDeleteBodySchema.parse(await readJsonSafe<Record<string, unknown>>(request, {}))\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.id,\n operation: 'delete',\n requestMethod: request.method,\n requestHeaders: request.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return withAdapterHeaders(NextResponse.json(guardResult.body, { status: guardResult.status }))\n }\n const interactionId = flags.unified\n ? body.todoId ?? body.id\n : await resolveCanonicalTodoTargetId(\n em,\n queryEngine,\n commandBus,\n commandContext,\n {\n linkId: body.id,\n todoId: body.todoId ?? body.id,\n },\n auth.tenantId,\n )\n\n const { logEntry } = await commandBus.execute('customers.interactions.delete', {\n input: { id: interactionId },\n ctx: commandContext,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.id,\n operation: 'delete',\n requestMethod: request.method,\n requestHeaders: request.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return withAdapterHeaders(\n withOperationMetadata(\n NextResponse.json({ ok: true }),\n logEntry,\n { resourceKind: 'customers.todoLink', resourceId: body.id },\n ),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return withAdapterHeaders(NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.delete failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nconst todoItemSchema = z.object({\n id: z.string(),\n todoId: z.string(),\n todoSource: z.string(),\n todoTitle: z.string().nullable(),\n todoIsDone: z.boolean().nullable(),\n todoPriority: z.number().nullable().optional(),\n todoSeverity: z.string().nullable().optional(),\n todoDescription: z.string().nullable().optional(),\n todoDueAt: z.string().nullable().optional(),\n todoCustomValues: z.record(z.string(), z.unknown()).nullable().optional(),\n todoOrganizationId: z.string().nullable(),\n todoUpdatedAt: z.string().nullable().optional(),\n organizationId: z.string(),\n tenantId: z.string(),\n createdAt: z.string(),\n externalHref: z.string().nullable().optional(),\n _integrations: z.record(z.string(), z.unknown()).optional(),\n customer: z.object({\n id: z.string().nullable(),\n displayName: z.string().nullable(),\n kind: z.string().nullable(),\n }),\n})\n\nconst todoCreateResponseSchema = z.object({\n linkId: z.string().uuid().nullable(),\n todoId: z.string().uuid().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = createCustomersCrudOpenApi({\n resourceName: 'CustomerTodo',\n querySchema,\n listResponseSchema: createPagedListResponseSchema(todoItemSchema),\n create: {\n schema: todoCreateBodySchema,\n responseSchema: todoCreateResponseSchema,\n description: 'DEPRECATED (sunset 2026-06-30): Creates a customer task. Use POST /api/customers/interactions instead.',\n },\n update: {\n schema: todoUpdateBodySchema,\n responseSchema: defaultOkResponseSchema,\n description: 'DEPRECATED (sunset 2026-06-30): Updates a customer task. Use PUT /api/customers/interactions instead.',\n },\n del: {\n schema: todoDeleteBodySchema,\n responseSchema: defaultOkResponseSchema,\n description: 'DEPRECATED (sunset 2026-06-30): Deletes a customer task. Use DELETE /api/customers/interactions instead.',\n },\n})\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * @deprecated Use /api/customers/interactions instead. This route is maintained\n * as a compatibility bridge per SPEC-046b. Will be removed in a future release.\n */\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { isUnrestrictedOrganizationScope } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { createCustomersCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../openapi'\nimport { CustomerInteraction, CustomerTodoLink } from '../../data/entities'\nimport { todoLinkWithTodoCreateSchema } from '../../data/validators'\nimport { withOperationMetadata } from '../../lib/operationMetadata'\nimport { resolveCustomerInteractionFeatureFlags } from '../../lib/interactionFeatureFlags'\nimport { resolveCustomersRequestContext } from '../../lib/interactionRequestContext'\nimport {\n EXAMPLE_TODO_SOURCE,\n CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n} from '../../lib/interactionCompatibility'\nimport {\n filterTodoRows,\n listCanonicalTodoRows,\n listLegacyTodoRows,\n normalizeTodoSearch,\n paginateTodoRows,\n sortTodoRows,\n resolveLegacyTodoDetails,\n} from '../../lib/todoCompatibility'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('customers')\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n all: z.string().optional(),\n entityId: z.string().uuid().optional(),\n})\n\nconst todoCreateBodySchema = todoLinkWithTodoCreateSchema.omit({\n tenantId: true,\n organizationId: true,\n}).passthrough()\n\nconst todoUpdateBodySchema = z\n .object({\n id: z.string().uuid(),\n linkId: z.string().uuid().optional(),\n title: z.string().trim().min(1).max(200).optional(),\n isDone: z.boolean().optional(),\n is_done: z.boolean().optional(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n customValues: z.record(z.string(), z.unknown()).optional(),\n })\n .passthrough()\n\nconst todoDeleteBodySchema = z.object({\n id: z.string().uuid(),\n todoId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n}\n\nconst DEPRECATION_HEADERS = {\n Deprecation: 'true',\n Sunset: 'Tue, 30 Jun 2026 00:00:00 GMT',\n Link: '</api/customers/interactions>; rel=\"successor-version\"',\n}\n\n// Caps the per-source fetch window used by the deprecated merged (legacy +\n// canonical bridge) read path. Keeps memory bounded on tenants with large\n// todo history; deep-pagination beyond this window is not supported here \u2014\n// use /api/customers/interactions instead.\nconst MERGED_TODO_FETCH_CAP = 2000\n\nfunction resolveGuardUserId(auth: {\n sub?: string | null\n userId?: string | null\n keyId?: string | null\n}): string {\n if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId\n if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId\n return 'system'\n}\n\nfunction withAdapterHeaders(response: Response): Response {\n const headers = new Headers(response.headers)\n Object.entries(DEPRECATION_HEADERS).forEach(([key, value]) => headers.set(key, value))\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n })\n}\n\nasync function legacyAdaptersDisabledResponse(): Promise<Response> {\n const { translate } = await resolveTranslations()\n return withAdapterHeaders(NextResponse.json(\n {\n error: translate(\n 'customers.interactions.legacyAdapters.disabled',\n 'This legacy adapter has been disabled. Use /api/customers/interactions instead.',\n ),\n },\n { status: 410 },\n ))\n}\n\nfunction normalizeTodoStatusInput(body: z.infer<typeof todoUpdateBodySchema>): boolean | undefined {\n if (typeof body.isDone === 'boolean') return body.isDone\n if (typeof body.is_done === 'boolean') return body.is_done\n return undefined\n}\n\nfunction collectTodoCustomValues(\n body: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const direct =\n body.customValues && typeof body.customValues === 'object'\n ? { ...(body.customValues as Record<string, unknown>) }\n : {}\n if (body.todoCustom && typeof body.todoCustom === 'object') {\n Object.assign(direct, body.todoCustom as Record<string, unknown>)\n }\n if (body.customFields && typeof body.customFields === 'object') {\n Object.assign(direct, body.customFields as Record<string, unknown>)\n }\n return Object.keys(direct).length > 0 ? direct : undefined\n}\n\nasync function findLegacyTodoLink(\n em: EntityManager,\n target: { linkId?: string; todoId?: string },\n tenantId: string,\n): Promise<CustomerTodoLink | null> {\n if (target.linkId) {\n const byLinkId = await em.findOne(CustomerTodoLink, { id: target.linkId, tenantId }, { populate: ['entity'] })\n if (byLinkId) return byLinkId\n }\n if (target.todoId) {\n return await em.findOne(CustomerTodoLink, { todoId: target.todoId, tenantId }, { populate: ['entity'] })\n }\n return null\n}\n\nasync function ensureCanonicalTodoBridge(\n em: EntityManager,\n queryEngine: QueryEngine,\n commandBus: CommandBus,\n commandContext: Parameters<CommandBus['execute']>[1]['ctx'],\n link: CustomerTodoLink,\n): Promise<string> {\n const existing = await em.findOne(CustomerInteraction, { id: link.todoId, tenantId: link.tenantId })\n if (existing) return existing.id\n\n const detailMap = await resolveLegacyTodoDetails(\n queryEngine,\n [link],\n link.tenantId,\n [link.organizationId],\n )\n const source =\n typeof link.todoSource === 'string' && link.todoSource.trim().length > 0\n ? link.todoSource\n : EXAMPLE_TODO_SOURCE\n const detail = detailMap.get(`${source}:${link.todoId}`) ?? null\n const entityId = typeof link.entity === 'string' ? link.entity : link.entity.id\n\n await commandBus.execute('customers.interactions.create', {\n input: {\n id: link.todoId,\n tenantId: link.tenantId,\n organizationId: link.organizationId,\n entityId,\n interactionType: 'task',\n title: detail?.title ?? null,\n body: detail?.description ?? null,\n status: detail?.isDone ? 'done' : 'planned',\n scheduledAt: detail?.dueAt ?? null,\n priority: detail?.priority ?? null,\n source: CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n ...(detail?.customValues ? { customValues: detail.customValues } : {}),\n },\n ctx: commandContext,\n })\n\n return link.todoId\n}\n\nasync function resolveCanonicalTodoTargetId(\n em: EntityManager,\n queryEngine: QueryEngine,\n commandBus: CommandBus,\n commandContext: Parameters<CommandBus['execute']>[1]['ctx'],\n target: { todoId?: string; linkId?: string },\n tenantId: string,\n): Promise<string> {\n if (target.todoId) {\n const interaction = await em.findOne(CustomerInteraction, { id: target.todoId, tenantId })\n if (interaction) return interaction.id\n }\n\n const legacyLink = await findLegacyTodoLink(em, target, tenantId)\n if (!legacyLink) {\n if (!target.todoId) throw notFound('Todo not found')\n return target.todoId\n }\n\n return ensureCanonicalTodoBridge(\n em,\n queryEngine,\n commandBus,\n commandContext,\n legacyLink,\n )\n}\n\nexport async function GET(request: Request): Promise<Response> {\n try {\n const { auth, em, organizationIds, container, selectedOrganizationId, scope } =\n await resolveCustomersRequestContext(request)\n const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams))\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const exportAll = parseBooleanToken(query.all) === true\n const isUnrestricted = isUnrestrictedOrganizationScope({\n isSuperAdmin: auth.isSuperAdmin === true,\n allowedOrganizationIds: scope?.allowedIds,\n })\n if (!isUnrestricted && (!organizationIds || organizationIds.length === 0)) {\n logger.warn('customers.todos.list collapsed organization scope', {\n tenantId: auth.tenantId,\n organizationIds,\n selectedId: scope?.selectedId ?? null,\n allowedIdsCount: scope?.allowedIds?.length ?? null,\n })\n return withAdapterHeaders(\n NextResponse.json({\n items: [],\n total: 0,\n page: exportAll ? 1 : query.page,\n pageSize: exportAll ? 0 : query.pageSize,\n totalPages: 1,\n }),\n )\n }\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const search = normalizeTodoSearch(query.search)\n\n if (flags.unified) {\n const canonical = await listCanonicalTodoRows(\n em,\n container,\n auth,\n selectedOrganizationId,\n organizationIds,\n {\n entityId: query.entityId,\n pagination: exportAll ? null : { page: query.page, pageSize: query.pageSize },\n searchText: search,\n isUnrestricted,\n },\n )\n const total = canonical.total\n return withAdapterHeaders(\n NextResponse.json({\n items: canonical.items,\n total,\n page: exportAll ? 1 : query.page,\n pageSize: exportAll ? canonical.items.length : query.pageSize,\n totalPages: exportAll ? 1 : Math.max(1, Math.ceil(total / query.pageSize)),\n }),\n )\n }\n\n const legacyWindow = exportAll\n ? null\n : Math.min(MERGED_TODO_FETCH_CAP, Math.max(query.pageSize, query.page * query.pageSize + query.pageSize))\n const [legacyRows, canonicalRows] = await Promise.all([\n listLegacyTodoRows(em, queryEngine, auth.tenantId, organizationIds, query.entityId, {\n limit: legacyWindow,\n isUnrestricted,\n }),\n listCanonicalTodoRows(\n em,\n container,\n auth,\n selectedOrganizationId,\n organizationIds,\n {\n entityId: query.entityId,\n includeDeleted: true,\n source: CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n limit: legacyWindow,\n isUnrestricted,\n },\n ),\n ])\n const mergedRows = [\n ...legacyRows.filter((row) => !canonicalRows.bridgeIds.has(row.todoId)),\n ...canonicalRows.items,\n ]\n const filteredRows = filterTodoRows(sortTodoRows(mergedRows), search)\n const paged = paginateTodoRows(filteredRows, query.page, query.pageSize, exportAll)\n\n return withAdapterHeaders(\n NextResponse.json({\n items: paged.items,\n total: paged.total,\n page: paged.page,\n pageSize: paged.pageSize,\n totalPages: paged.totalPages,\n }),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.get failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nexport async function POST(request: Request): Promise<Response> {\n try {\n const { commandContext, container, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const commandBus = container.resolve('commandBus') as CommandBus\n const body = todoCreateBodySchema.parse(await readJsonSafe<Record<string, unknown>>(request, {}))\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.entityId,\n operation: 'create',\n requestMethod: request.method,\n requestHeaders: request.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return withAdapterHeaders(NextResponse.json(guardResult.body, { status: guardResult.status }))\n }\n const customValues = collectTodoCustomValues(body as Record<string, unknown>)\n\n const { result, logEntry } = await commandBus.execute('customers.interactions.create', {\n input: {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId ?? auth.orgId,\n entityId: body.entityId,\n interactionType: 'task',\n title: body.title,\n status: body.is_done === true || body.isDone === true ? 'done' : 'planned',\n source: CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,\n ...(customValues ? {\n customValues,\n priority: typeof customValues.priority === 'number' ? customValues.priority : null,\n body: typeof customValues.description === 'string' ? customValues.description : null,\n scheduledAt:\n typeof customValues.due_at === 'string'\n ? customValues.due_at\n : typeof customValues.dueAt === 'string'\n ? customValues.dueAt\n : null,\n } : {}),\n },\n ctx: commandContext,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.entityId,\n operation: 'create',\n requestMethod: request.method,\n requestHeaders: request.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const interactionId =\n result &&\n typeof result === 'object' &&\n 'interactionId' in result &&\n typeof result.interactionId === 'string'\n ? result.interactionId\n : result &&\n typeof result === 'object' &&\n 'id' in result &&\n typeof result.id === 'string'\n ? result.id\n : null\n return withAdapterHeaders(\n withOperationMetadata(\n NextResponse.json(\n {\n linkId: interactionId,\n todoId: interactionId,\n },\n { status: 201 },\n ),\n logEntry,\n { resourceKind: 'customers.todoLink', resourceId: interactionId },\n ),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return withAdapterHeaders(NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.post failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nexport async function PUT(request: Request): Promise<Response> {\n try {\n const { commandContext, container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const commandBus = container.resolve('commandBus') as CommandBus\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const body = todoUpdateBodySchema.parse(await readJsonSafe<Record<string, unknown>>(request, {}))\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.linkId ?? body.id,\n operation: 'update',\n requestMethod: request.method,\n requestHeaders: request.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return withAdapterHeaders(NextResponse.json(guardResult.body, { status: guardResult.status }))\n }\n const interactionId = flags.unified\n ? body.id\n : await resolveCanonicalTodoTargetId(\n em,\n queryEngine,\n commandBus,\n commandContext,\n {\n todoId: body.id,\n linkId: body.linkId,\n },\n auth.tenantId,\n )\n const customValues = collectTodoCustomValues(body as Record<string, unknown>)\n const nextDone = normalizeTodoStatusInput(body)\n\n const { logEntry } = await commandBus.execute('customers.interactions.update', {\n input: {\n id: interactionId,\n ...(body.title !== undefined ? { title: body.title } : {}),\n ...(nextDone !== undefined ? { status: nextDone ? 'done' : 'planned' } : {}),\n ...(customValues ? {\n customValues,\n ...(customValues.priority !== undefined\n ? { priority: typeof customValues.priority === 'number' ? customValues.priority : null }\n : {}),\n ...(customValues.description !== undefined\n ? { body: typeof customValues.description === 'string' ? customValues.description : null }\n : {}),\n ...(customValues.due_at !== undefined || customValues.dueAt !== undefined\n ? {\n scheduledAt:\n typeof customValues.due_at === 'string'\n ? customValues.due_at\n : typeof customValues.dueAt === 'string'\n ? customValues.dueAt\n : null,\n }\n : {}),\n } : {}),\n },\n ctx: commandContext,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.linkId ?? body.id,\n operation: 'update',\n requestMethod: request.method,\n requestHeaders: request.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return withAdapterHeaders(\n withOperationMetadata(\n NextResponse.json({ ok: true }),\n logEntry,\n { resourceKind: 'customers.todoLink', resourceId: body.linkId ?? body.id },\n ),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return withAdapterHeaders(NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.put failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nexport async function DELETE(request: Request): Promise<Response> {\n try {\n const { commandContext, container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)\n if (!flags.legacyAdapters) {\n return await legacyAdaptersDisabledResponse()\n }\n const commandBus = container.resolve('commandBus') as CommandBus\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const body = todoDeleteBodySchema.parse(await readJsonSafe<Record<string, unknown>>(request, {}))\n const guardUserId = resolveGuardUserId(auth)\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.id,\n operation: 'delete',\n requestMethod: request.method,\n requestHeaders: request.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return withAdapterHeaders(NextResponse.json(guardResult.body, { status: guardResult.status }))\n }\n const interactionId = flags.unified\n ? body.todoId ?? body.id\n : await resolveCanonicalTodoTargetId(\n em,\n queryEngine,\n commandBus,\n commandContext,\n {\n linkId: body.id,\n todoId: body.todoId ?? body.id,\n },\n auth.tenantId,\n )\n\n const { logEntry } = await commandBus.execute('customers.interactions.delete', {\n input: { id: interactionId },\n ctx: commandContext,\n })\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n userId: guardUserId,\n resourceKind: 'customers.todoLink',\n resourceId: body.id,\n operation: 'delete',\n requestMethod: request.method,\n requestHeaders: request.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return withAdapterHeaders(\n withOperationMetadata(\n NextResponse.json({ ok: true }),\n logEntry,\n { resourceKind: 'customers.todoLink', resourceId: body.id },\n ),\n )\n } catch (err) {\n if (isCrudHttpError(err)) {\n return withAdapterHeaders(NextResponse.json(err.body, { status: err.status }))\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return withAdapterHeaders(NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status }))\n }\n if (err instanceof z.ZodError) {\n return withAdapterHeaders(\n NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 }),\n )\n }\n logger.error('customers.todos.delete failed', { err })\n return withAdapterHeaders(\n NextResponse.json({ error: 'Internal server error' }, { status: 500 }),\n )\n }\n}\n\nconst todoItemSchema = z.object({\n id: z.string(),\n todoId: z.string(),\n todoSource: z.string(),\n todoTitle: z.string().nullable(),\n todoIsDone: z.boolean().nullable(),\n todoPriority: z.number().nullable().optional(),\n todoSeverity: z.string().nullable().optional(),\n todoDescription: z.string().nullable().optional(),\n todoDueAt: z.string().nullable().optional(),\n todoCustomValues: z.record(z.string(), z.unknown()).nullable().optional(),\n todoOrganizationId: z.string().nullable(),\n todoUpdatedAt: z.string().nullable().optional(),\n organizationId: z.string(),\n tenantId: z.string(),\n createdAt: z.string(),\n externalHref: z.string().nullable().optional(),\n _integrations: z.record(z.string(), z.unknown()).optional(),\n customer: z.object({\n id: z.string().nullable(),\n displayName: z.string().nullable(),\n kind: z.string().nullable(),\n }),\n})\n\nconst todoCreateResponseSchema = z.object({\n linkId: z.string().uuid().nullable(),\n todoId: z.string().uuid().nullable(),\n})\n\nexport const openApi: OpenApiRouteDoc = createCustomersCrudOpenApi({\n resourceName: 'CustomerTodo',\n querySchema,\n listResponseSchema: createPagedListResponseSchema(todoItemSchema),\n create: {\n schema: todoCreateBodySchema,\n responseSchema: todoCreateResponseSchema,\n description: 'DEPRECATED (sunset 2026-06-30): Creates a customer task. Use POST /api/customers/interactions instead.',\n },\n update: {\n schema: todoUpdateBodySchema,\n responseSchema: defaultOkResponseSchema,\n description: 'DEPRECATED (sunset 2026-06-30): Updates a customer task. Use PUT /api/customers/interactions instead.',\n },\n del: {\n schema: todoDeleteBodySchema,\n responseSchema: defaultOkResponseSchema,\n description: 'DEPRECATED (sunset 2026-06-30): Deletes a customer task. Use DELETE /api/customers/interactions instead.',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -85,9 +85,9 @@ function buildLegacyTaskCustomValues(payload, options) {
|
|
|
85
85
|
}
|
|
86
86
|
custom[key] = value;
|
|
87
87
|
};
|
|
88
|
-
assignValue("priority", payload.base.priority
|
|
89
|
-
assignValue("description", payload.base.description
|
|
90
|
-
assignValue("due_at", payload.base.scheduledAt
|
|
88
|
+
assignValue("priority", payload.base.priority);
|
|
89
|
+
assignValue("description", payload.base.description);
|
|
90
|
+
assignValue("due_at", payload.base.scheduledAt);
|
|
91
91
|
return custom;
|
|
92
92
|
}
|
|
93
93
|
function usePersonTasks({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/components/detail/hooks/usePersonTasks.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { resolveTodoApiPath } from '../utils'\nimport type { TodoLinkSummary } from '../types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE } from '../../../lib/interactionCompatibility'\n\nconst DEFAULT_TODO_SOURCE = CUSTOMER_INTERACTION_TASK_SOURCE\n\ntype CustomerTodoRow = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n todoIsDone: boolean | null\n todoPriority: number | null\n todoSeverity: string | null\n todoDescription: string | null\n todoDueAt: string | null\n todoCustomValues: Record<string, unknown> | null\n todoOrganizationId: string | null\n todoUpdatedAt?: string | null\n organizationId: string\n tenantId: string\n createdAt: string\n}\n\n// `/api/customers/todos` also reports `totalPages`, deliberately left out of\n// this type: the load-more affordance terminates on a short page instead \u2014 see\n// `hasMoreFromPage`. `total` stays because it feeds the section's count badge,\n// where an under-report is cosmetic rather than a way to strand rows.\ntype CustomerTodosResponse = {\n items: CustomerTodoRow[]\n total: number\n page: number\n pageSize: number\n}\n\nexport type TaskFormPayload = {\n base: {\n title: string\n is_done?: boolean\n status?: string\n description?: string | null\n priority?: number | null\n scheduledAt?: string | null\n }\n custom: Record<string, unknown>\n}\n\nexport type UsePersonTasksOptions = {\n entityId: string | null\n initialTasks?: TodoLinkSummary[]\n pageSize?: number\n}\n\nexport type UsePersonTasksResult = {\n tasks: TodoLinkSummary[]\n isInitialLoading: boolean\n isLoadingMore: boolean\n isMutating: boolean\n hasMore: boolean\n loadMore: () => Promise<void>\n refresh: () => Promise<void>\n createTask: (payload: TaskFormPayload) => Promise<void>\n updateTask: (task: TodoLinkSummary, payload: TaskFormPayload) => Promise<void>\n toggleTask: (task: TodoLinkSummary, nextIsDone: boolean) => Promise<void>\n unlinkTask: (task: TodoLinkSummary) => Promise<void>\n pendingTaskId: string | null\n totalCount: number\n error: string | null\n}\n\nfunction mapRowToSummary(row: CustomerTodoRow): TodoLinkSummary {\n return {\n id: row.id,\n todoId: row.todoId,\n todoSource: row.todoSource || DEFAULT_TODO_SOURCE,\n createdAt: row.createdAt,\n title: row.todoTitle ?? null,\n isDone: row.todoIsDone ?? null,\n status: row.todoIsDone ? 'done' : 'planned',\n priority: row.todoPriority ?? null,\n severity: row.todoSeverity ?? null,\n description: row.todoDescription ?? null,\n dueAt: row.todoDueAt ?? null,\n todoOrganizationId: row.todoOrganizationId ?? null,\n updatedAt: row.todoUpdatedAt ?? null,\n customValues: row.todoCustomValues ?? null,\n }\n}\n\nfunction mergeUnique(existing: TodoLinkSummary[], incoming: TodoLinkSummary[]): TodoLinkSummary[] {\n if (!existing.length) return incoming\n if (!incoming.length) return existing\n const byId = new Map<string, TodoLinkSummary>()\n const result: TodoLinkSummary[] = []\n for (const item of existing) {\n byId.set(item.id, item)\n result.push(item)\n }\n for (const item of incoming) {\n if (byId.has(item.id)) {\n const index = result.findIndex((entry) => entry.id === item.id)\n if (index !== -1) result[index] = item\n } else {\n byId.set(item.id, item)\n result.push(item)\n }\n }\n return result\n}\n\nfunction normalizeBoolean(value: unknown): boolean | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'boolean') return value\n if (typeof value === 'string') {\n const parsed = parseBooleanToken(value)\n return parsed === null ? undefined : parsed\n }\n return undefined\n}\n\nfunction normalizeNumber(value: unknown): number | null | undefined {\n if (value === null || value === undefined || value === '') return undefined\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n const parsed = Number(trimmed)\n if (!Number.isNaN(parsed)) return parsed\n }\n return null\n}\n\nfunction normalizeString(value: unknown): string | null | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n }\n return String(value)\n}\n\nfunction buildLegacyTaskCustomValues(\n payload: TaskFormPayload,\n options?: { includeEmptyFields?: boolean },\n): Record<string, unknown> {\n const includeEmptyFields = options?.includeEmptyFields === true\n const custom = { ...payload.custom }\n\n const assignValue = (key: string, value: unknown) => {\n if (value === undefined) return\n if (value === null) {\n if (includeEmptyFields) custom[key] = null\n return\n }\n custom[key] = value\n }\n\n assignValue('priority', payload.base.priority ?? null)\n assignValue('description', payload.base.description ?? null)\n assignValue('due_at', payload.base.scheduledAt ?? null)\n\n return custom\n}\n\nexport function usePersonTasks({\n entityId,\n initialTasks = [],\n pageSize = 20,\n}: UsePersonTasksOptions): UsePersonTasksResult {\n const [tasks, setTasks] = React.useState<TodoLinkSummary[]>(initialTasks)\n const [pageInfo, setPageInfo] = React.useState<{ page: number; hasMore: boolean; total: number }>({\n page: 1,\n hasMore: false,\n total: initialTasks.length,\n })\n const [isInitialLoading, setIsInitialLoading] = React.useState<boolean>(() => Boolean(entityId))\n const [isLoadingMore, setIsLoadingMore] = React.useState(false)\n const [isMutating, setIsMutating] = React.useState(false)\n const [pendingTaskId, setPendingTaskId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const mapResponse = React.useCallback((payload: CustomerTodosResponse) => {\n const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : []\n setPageInfo({\n page: payload.page ?? 1,\n // Short-page termination instead of `page < totalPages` \u2014 see\n // `hasMoreFromPage`. `mapRowToSummary` is a 1:1 map and the `mergeUnique`\n // dedupe happens after this, so `mapped.length` is what the server served.\n // Measured against the page size the server echoed rather than the one\n // requested, so a page size the endpoint narrows server-side cannot make a\n // full page read as short and silently end the sequence.\n hasMore: hasMoreFromPage(mapped.length, payload.pageSize ?? pageSize),\n total: payload.total ?? mapped.length,\n })\n setError(null)\n return mapped\n }, [pageSize])\n\n const fetchPage = React.useCallback(\n async (page: number): Promise<CustomerTodosResponse> => {\n if (!entityId) {\n return {\n items: [],\n total: 0,\n page: 1,\n pageSize,\n }\n }\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n entityId,\n })\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to load tasks.' },\n )\n },\n [entityId, pageSize],\n )\n\n const refresh = React.useCallback(async () => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, hasMore: false, total: 0 })\n return\n }\n setIsInitialLoading(true)\n try {\n const payload = await fetchPage(1)\n const mapped = mapResponse(payload)\n setTasks(mapped)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsInitialLoading(false)\n }\n }, [entityId, fetchPage, mapResponse])\n\n const loadMore = React.useCallback(async () => {\n if (!entityId) return\n if (isLoadingMore) return\n if (!pageInfo.hasMore) return\n setIsLoadingMore(true)\n try {\n const payload = await fetchPage(pageInfo.page + 1)\n const mapped = mapResponse(payload)\n setTasks((prev) => mergeUnique(prev, mapped))\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsLoadingMore(false)\n }\n }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.hasMore, pageInfo.page])\n\n React.useEffect(() => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, hasMore: false, total: 0 })\n setError(null)\n setIsInitialLoading(false)\n return\n }\n setTasks(initialTasks)\n setPageInfo({\n page: 1,\n hasMore: false,\n total: initialTasks.length,\n })\n setError(null)\n let cancelled = false\n setIsInitialLoading(true)\n fetchPage(1)\n .then((payload) => {\n if (cancelled) return\n const mapped = mapResponse(payload)\n setTasks(mapped)\n })\n .catch((err) => {\n if (cancelled) return\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n })\n .finally(() => {\n if (!cancelled) setIsInitialLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [entityId, initialTasks, fetchPage, mapResponse])\n\n const createTask = React.useCallback(\n async ({ base, custom }: TaskFormPayload) => {\n if (!entityId) throw new Error('Task creation requires an entity id')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom })\n const payload: Record<string, unknown> = {\n entityId,\n title: base.title,\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) payload.isDone = normalizedDone\n if (Object.keys(customValues).length) payload.todoCustom = customValues\n\n const response = await apiCallOrThrow<{ linkId?: string; todoId?: string }>(\n '/api/customers/todos',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: 'Failed to create task.' },\n )\n const body = response.result ?? {}\n const linkId = typeof body.linkId === 'string' && body.linkId.length ? body.linkId : generateTempId()\n const todoId = typeof body.todoId === 'string' && body.todoId.length ? body.todoId : generateTempId()\n const createdAt = new Date().toISOString()\n const persistedCustomValues = Object.keys(customValues).length ? { ...customValues } : null\n const priority = normalizeNumber(base.priority ?? customValues.priority)\n const severity = normalizeString(customValues.severity) ?? null\n const description = normalizeString(base.description ?? customValues.description) ?? null\n const newTask: TodoLinkSummary = {\n id: linkId,\n todoId,\n todoSource: DEFAULT_TODO_SOURCE,\n createdAt,\n title: base.title,\n isDone: normalizedDone ?? false,\n status: normalizedDone ? 'done' : 'planned',\n priority: priority === undefined ? null : priority,\n severity,\n description,\n dueAt: normalizeString(base.scheduledAt ?? customValues.due_at) ?? normalizeString(customValues.dueAt) ?? null,\n todoOrganizationId: null,\n customValues: persistedCustomValues,\n }\n setTasks((prev) => [newTask, ...prev])\n setPageInfo((prev) => ({\n page: 1,\n hasMore: prev.hasMore,\n total: prev.total + 1,\n }))\n await refresh()\n } finally {\n setIsMutating(false)\n }\n },\n [entityId, refresh],\n )\n\n const updateTask = React.useCallback(\n async (task: TodoLinkSummary, { base, custom }: TaskFormPayload) => {\n if (!task.todoId) throw new Error('Task is missing todo id')\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) throw new Error('Unsupported task source')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom }, { includeEmptyFields: true })\n const body: Record<string, unknown> = {\n id: task.todoId,\n linkId: task.id,\n }\n if (typeof base.title === 'string' && base.title.trim().length) {\n body.title = base.title.trim()\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) body.is_done = normalizedDone\n if (Object.keys(customValues).length) {\n body.customFields = customValues\n }\n // Send the optimistic-lock header (task's loaded updatedAt) so a stale\n // edit \u2014 or an edit after the task was deleted in another tab \u2014 surfaces\n // the unified conflict bar (409) instead of silently overwriting or\n // returning a bare \"Interaction not found\" 404 (#2055).\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n apiPath,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: 'Failed to update task.' },\n ),\n )\n setTasks((prev) =>\n prev.map((item) => {\n if (item.id !== task.id) return item\n const nextCustomValues = { ...(item.customValues ?? {}) }\n for (const [key, value] of Object.entries(customValues)) {\n nextCustomValues[key] = value === undefined ? null : value\n }\n return {\n ...item,\n title: typeof base.title === 'string' && base.title.trim().length ? base.title.trim() : item.title,\n isDone: normalizedDone !== undefined ? normalizedDone : item.isDone,\n status: normalizedDone !== undefined ? (normalizedDone ? 'done' : 'planned') : item.status ?? null,\n priority:\n normalizeNumber(base.priority ?? customValues.priority) ??\n (base.priority === undefined && customValues.priority === undefined ? item.priority ?? null : null),\n severity:\n normalizeString(customValues.severity) ??\n (customValues.severity === undefined ? item.severity ?? null : null),\n description:\n normalizeString(base.description ?? customValues.description) ??\n (base.description === undefined && customValues.description === undefined ? item.description ?? null : null),\n dueAt:\n normalizeString(base.scheduledAt ?? customValues.due_at) ??\n normalizeString(customValues.dueAt) ??\n (base.scheduledAt === undefined && customValues.due_at === undefined && customValues.dueAt === undefined\n ? item.dueAt ?? null\n : null),\n customValues: Object.keys(nextCustomValues).length ? nextCustomValues : null,\n }\n }),\n )\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const toggleTask = React.useCallback(\n async (task: TodoLinkSummary, nextIsDone: boolean) => {\n if (!task.todoId) {\n throw new Error('Task is missing todo id')\n }\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) {\n throw new Error('Unsupported task source')\n }\n setPendingTaskId(task.todoId)\n try {\n await updateTask(task, { base: { title: task.title ?? '', is_done: nextIsDone }, custom: {} })\n } finally {\n setPendingTaskId(null)\n }\n },\n [updateTask],\n )\n\n const unlinkTask = React.useCallback(\n async (task: TodoLinkSummary) => {\n if (!task.id) throw new Error('Task link id missing')\n setIsMutating(true)\n try {\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n '/api/customers/todos',\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: task.id }),\n },\n { errorMessage: 'Failed to remove task.' },\n ),\n )\n setTasks((prev) => prev.filter((item) => item.id !== task.id))\n setPageInfo((prev) => ({\n page: prev.page,\n hasMore: prev.hasMore,\n total: Math.max(0, prev.total - 1),\n }))\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const hasMore = entityId != null && pageInfo.hasMore\n\n return {\n tasks,\n isInitialLoading,\n isLoadingMore,\n isMutating,\n hasMore,\n loadMore,\n refresh,\n createTask,\n updateTask,\n toggleTask,\n unlinkTask,\n pendingTaskId,\n totalCount: pageInfo.total,\n error,\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { resolveTodoApiPath } from '../utils'\nimport type { TodoLinkSummary } from '../types'\nimport { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'\nimport { CUSTOMER_INTERACTION_TASK_SOURCE } from '../../../lib/interactionCompatibility'\n\nconst DEFAULT_TODO_SOURCE = CUSTOMER_INTERACTION_TASK_SOURCE\n\ntype CustomerTodoRow = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n todoIsDone: boolean | null\n todoPriority: number | null\n todoSeverity: string | null\n todoDescription: string | null\n todoDueAt: string | null\n todoCustomValues: Record<string, unknown> | null\n todoOrganizationId: string | null\n todoUpdatedAt?: string | null\n organizationId: string\n tenantId: string\n createdAt: string\n}\n\n// `/api/customers/todos` also reports `totalPages`, deliberately left out of\n// this type: the load-more affordance terminates on a short page instead \u2014 see\n// `hasMoreFromPage`. `total` stays because it feeds the section's count badge,\n// where an under-report is cosmetic rather than a way to strand rows.\ntype CustomerTodosResponse = {\n items: CustomerTodoRow[]\n total: number\n page: number\n pageSize: number\n}\n\nexport type TaskFormPayload = {\n base: {\n title: string\n is_done?: boolean\n status?: string\n description?: string | null\n priority?: number | null\n scheduledAt?: string | null\n }\n custom: Record<string, unknown>\n}\n\nexport type UsePersonTasksOptions = {\n entityId: string | null\n initialTasks?: TodoLinkSummary[]\n pageSize?: number\n}\n\nexport type UsePersonTasksResult = {\n tasks: TodoLinkSummary[]\n isInitialLoading: boolean\n isLoadingMore: boolean\n isMutating: boolean\n hasMore: boolean\n loadMore: () => Promise<void>\n refresh: () => Promise<void>\n createTask: (payload: TaskFormPayload) => Promise<void>\n updateTask: (task: TodoLinkSummary, payload: TaskFormPayload) => Promise<void>\n toggleTask: (task: TodoLinkSummary, nextIsDone: boolean) => Promise<void>\n unlinkTask: (task: TodoLinkSummary) => Promise<void>\n pendingTaskId: string | null\n totalCount: number\n error: string | null\n}\n\nfunction mapRowToSummary(row: CustomerTodoRow): TodoLinkSummary {\n return {\n id: row.id,\n todoId: row.todoId,\n todoSource: row.todoSource || DEFAULT_TODO_SOURCE,\n createdAt: row.createdAt,\n title: row.todoTitle ?? null,\n isDone: row.todoIsDone ?? null,\n status: row.todoIsDone ? 'done' : 'planned',\n priority: row.todoPriority ?? null,\n severity: row.todoSeverity ?? null,\n description: row.todoDescription ?? null,\n dueAt: row.todoDueAt ?? null,\n todoOrganizationId: row.todoOrganizationId ?? null,\n updatedAt: row.todoUpdatedAt ?? null,\n customValues: row.todoCustomValues ?? null,\n }\n}\n\nfunction mergeUnique(existing: TodoLinkSummary[], incoming: TodoLinkSummary[]): TodoLinkSummary[] {\n if (!existing.length) return incoming\n if (!incoming.length) return existing\n const byId = new Map<string, TodoLinkSummary>()\n const result: TodoLinkSummary[] = []\n for (const item of existing) {\n byId.set(item.id, item)\n result.push(item)\n }\n for (const item of incoming) {\n if (byId.has(item.id)) {\n const index = result.findIndex((entry) => entry.id === item.id)\n if (index !== -1) result[index] = item\n } else {\n byId.set(item.id, item)\n result.push(item)\n }\n }\n return result\n}\n\nfunction normalizeBoolean(value: unknown): boolean | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'boolean') return value\n if (typeof value === 'string') {\n const parsed = parseBooleanToken(value)\n return parsed === null ? undefined : parsed\n }\n return undefined\n}\n\nfunction normalizeNumber(value: unknown): number | null | undefined {\n if (value === null || value === undefined || value === '') return undefined\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return undefined\n const parsed = Number(trimmed)\n if (!Number.isNaN(parsed)) return parsed\n }\n return null\n}\n\nfunction normalizeString(value: unknown): string | null | undefined {\n if (value === null || value === undefined) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n }\n return String(value)\n}\n\nfunction buildLegacyTaskCustomValues(\n payload: TaskFormPayload,\n options?: { includeEmptyFields?: boolean },\n): Record<string, unknown> {\n const includeEmptyFields = options?.includeEmptyFields === true\n const custom = { ...payload.custom }\n\n const assignValue = (key: string, value: unknown) => {\n if (value === undefined) return\n if (value === null) {\n if (includeEmptyFields) custom[key] = null\n return\n }\n custom[key] = value\n }\n\n assignValue('priority', payload.base.priority)\n assignValue('description', payload.base.description)\n assignValue('due_at', payload.base.scheduledAt)\n\n return custom\n}\n\nexport function usePersonTasks({\n entityId,\n initialTasks = [],\n pageSize = 20,\n}: UsePersonTasksOptions): UsePersonTasksResult {\n const [tasks, setTasks] = React.useState<TodoLinkSummary[]>(initialTasks)\n const [pageInfo, setPageInfo] = React.useState<{ page: number; hasMore: boolean; total: number }>({\n page: 1,\n hasMore: false,\n total: initialTasks.length,\n })\n const [isInitialLoading, setIsInitialLoading] = React.useState<boolean>(() => Boolean(entityId))\n const [isLoadingMore, setIsLoadingMore] = React.useState(false)\n const [isMutating, setIsMutating] = React.useState(false)\n const [pendingTaskId, setPendingTaskId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const mapResponse = React.useCallback((payload: CustomerTodosResponse) => {\n const mapped = Array.isArray(payload.items) ? payload.items.map(mapRowToSummary) : []\n setPageInfo({\n page: payload.page ?? 1,\n // Short-page termination instead of `page < totalPages` \u2014 see\n // `hasMoreFromPage`. `mapRowToSummary` is a 1:1 map and the `mergeUnique`\n // dedupe happens after this, so `mapped.length` is what the server served.\n // Measured against the page size the server echoed rather than the one\n // requested, so a page size the endpoint narrows server-side cannot make a\n // full page read as short and silently end the sequence.\n hasMore: hasMoreFromPage(mapped.length, payload.pageSize ?? pageSize),\n total: payload.total ?? mapped.length,\n })\n setError(null)\n return mapped\n }, [pageSize])\n\n const fetchPage = React.useCallback(\n async (page: number): Promise<CustomerTodosResponse> => {\n if (!entityId) {\n return {\n items: [],\n total: 0,\n page: 1,\n pageSize,\n }\n }\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n entityId,\n })\n return readApiResultOrThrow<CustomerTodosResponse>(\n `/api/customers/todos?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to load tasks.' },\n )\n },\n [entityId, pageSize],\n )\n\n const refresh = React.useCallback(async () => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, hasMore: false, total: 0 })\n return\n }\n setIsInitialLoading(true)\n try {\n const payload = await fetchPage(1)\n const mapped = mapResponse(payload)\n setTasks(mapped)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsInitialLoading(false)\n }\n }, [entityId, fetchPage, mapResponse])\n\n const loadMore = React.useCallback(async () => {\n if (!entityId) return\n if (isLoadingMore) return\n if (!pageInfo.hasMore) return\n setIsLoadingMore(true)\n try {\n const payload = await fetchPage(pageInfo.page + 1)\n const mapped = mapResponse(payload)\n setTasks((prev) => mergeUnique(prev, mapped))\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n throw err\n } finally {\n setIsLoadingMore(false)\n }\n }, [entityId, fetchPage, isLoadingMore, mapResponse, pageInfo.hasMore, pageInfo.page])\n\n React.useEffect(() => {\n if (!entityId) {\n setTasks([])\n setPageInfo({ page: 1, hasMore: false, total: 0 })\n setError(null)\n setIsInitialLoading(false)\n return\n }\n setTasks(initialTasks)\n setPageInfo({\n page: 1,\n hasMore: false,\n total: initialTasks.length,\n })\n setError(null)\n let cancelled = false\n setIsInitialLoading(true)\n fetchPage(1)\n .then((payload) => {\n if (cancelled) return\n const mapped = mapResponse(payload)\n setTasks(mapped)\n })\n .catch((err) => {\n if (cancelled) return\n const message = err instanceof Error ? err.message : 'Failed to load tasks.'\n setError(message)\n })\n .finally(() => {\n if (!cancelled) setIsInitialLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [entityId, initialTasks, fetchPage, mapResponse])\n\n const createTask = React.useCallback(\n async ({ base, custom }: TaskFormPayload) => {\n if (!entityId) throw new Error('Task creation requires an entity id')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom })\n const payload: Record<string, unknown> = {\n entityId,\n title: base.title,\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) payload.isDone = normalizedDone\n if (Object.keys(customValues).length) payload.todoCustom = customValues\n\n const response = await apiCallOrThrow<{ linkId?: string; todoId?: string }>(\n '/api/customers/todos',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: 'Failed to create task.' },\n )\n const body = response.result ?? {}\n const linkId = typeof body.linkId === 'string' && body.linkId.length ? body.linkId : generateTempId()\n const todoId = typeof body.todoId === 'string' && body.todoId.length ? body.todoId : generateTempId()\n const createdAt = new Date().toISOString()\n const persistedCustomValues = Object.keys(customValues).length ? { ...customValues } : null\n const priority = normalizeNumber(base.priority ?? customValues.priority)\n const severity = normalizeString(customValues.severity) ?? null\n const description = normalizeString(base.description ?? customValues.description) ?? null\n const newTask: TodoLinkSummary = {\n id: linkId,\n todoId,\n todoSource: DEFAULT_TODO_SOURCE,\n createdAt,\n title: base.title,\n isDone: normalizedDone ?? false,\n status: normalizedDone ? 'done' : 'planned',\n priority: priority === undefined ? null : priority,\n severity,\n description,\n dueAt: normalizeString(base.scheduledAt ?? customValues.due_at) ?? normalizeString(customValues.dueAt) ?? null,\n todoOrganizationId: null,\n customValues: persistedCustomValues,\n }\n setTasks((prev) => [newTask, ...prev])\n setPageInfo((prev) => ({\n page: 1,\n hasMore: prev.hasMore,\n total: prev.total + 1,\n }))\n await refresh()\n } finally {\n setIsMutating(false)\n }\n },\n [entityId, refresh],\n )\n\n const updateTask = React.useCallback(\n async (task: TodoLinkSummary, { base, custom }: TaskFormPayload) => {\n if (!task.todoId) throw new Error('Task is missing todo id')\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) throw new Error('Unsupported task source')\n setIsMutating(true)\n try {\n const customValues = buildLegacyTaskCustomValues({ base, custom }, { includeEmptyFields: true })\n const body: Record<string, unknown> = {\n id: task.todoId,\n linkId: task.id,\n }\n if (typeof base.title === 'string' && base.title.trim().length) {\n body.title = base.title.trim()\n }\n const normalizedDone = normalizeBoolean(base.is_done)\n if (normalizedDone !== undefined) body.is_done = normalizedDone\n if (Object.keys(customValues).length) {\n body.customFields = customValues\n }\n // Send the optimistic-lock header (task's loaded updatedAt) so a stale\n // edit \u2014 or an edit after the task was deleted in another tab \u2014 surfaces\n // the unified conflict bar (409) instead of silently overwriting or\n // returning a bare \"Interaction not found\" 404 (#2055).\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n apiPath,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n { errorMessage: 'Failed to update task.' },\n ),\n )\n setTasks((prev) =>\n prev.map((item) => {\n if (item.id !== task.id) return item\n const nextCustomValues = { ...(item.customValues ?? {}) }\n for (const [key, value] of Object.entries(customValues)) {\n nextCustomValues[key] = value === undefined ? null : value\n }\n return {\n ...item,\n title: typeof base.title === 'string' && base.title.trim().length ? base.title.trim() : item.title,\n isDone: normalizedDone !== undefined ? normalizedDone : item.isDone,\n status: normalizedDone !== undefined ? (normalizedDone ? 'done' : 'planned') : item.status ?? null,\n priority:\n normalizeNumber(base.priority ?? customValues.priority) ??\n (base.priority === undefined && customValues.priority === undefined ? item.priority ?? null : null),\n severity:\n normalizeString(customValues.severity) ??\n (customValues.severity === undefined ? item.severity ?? null : null),\n description:\n normalizeString(base.description ?? customValues.description) ??\n (base.description === undefined && customValues.description === undefined ? item.description ?? null : null),\n dueAt:\n normalizeString(base.scheduledAt ?? customValues.due_at) ??\n normalizeString(customValues.dueAt) ??\n (base.scheduledAt === undefined && customValues.due_at === undefined && customValues.dueAt === undefined\n ? item.dueAt ?? null\n : null),\n customValues: Object.keys(nextCustomValues).length ? nextCustomValues : null,\n }\n }),\n )\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const toggleTask = React.useCallback(\n async (task: TodoLinkSummary, nextIsDone: boolean) => {\n if (!task.todoId) {\n throw new Error('Task is missing todo id')\n }\n const apiPath = resolveTodoApiPath(task.todoSource || DEFAULT_TODO_SOURCE)\n if (!apiPath) {\n throw new Error('Unsupported task source')\n }\n setPendingTaskId(task.todoId)\n try {\n await updateTask(task, { base: { title: task.title ?? '', is_done: nextIsDone }, custom: {} })\n } finally {\n setPendingTaskId(null)\n }\n },\n [updateTask],\n )\n\n const unlinkTask = React.useCallback(\n async (task: TodoLinkSummary) => {\n if (!task.id) throw new Error('Task link id missing')\n setIsMutating(true)\n try {\n await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(task.updatedAt ?? null),\n () => apiCallOrThrow(\n '/api/customers/todos',\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: task.id }),\n },\n { errorMessage: 'Failed to remove task.' },\n ),\n )\n setTasks((prev) => prev.filter((item) => item.id !== task.id))\n setPageInfo((prev) => ({\n page: prev.page,\n hasMore: prev.hasMore,\n total: Math.max(0, prev.total - 1),\n }))\n } finally {\n setIsMutating(false)\n }\n },\n [],\n )\n\n const hasMore = entityId != null && pageInfo.hasMore\n\n return {\n tasks,\n isInitialLoading,\n isLoadingMore,\n isMutating,\n hasMore,\n loadMore,\n refresh,\n createTask,\n updateTask,\n toggleTask,\n unlinkTask,\n pendingTaskId,\n totalCount: pageInfo.total,\n error,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -82,6 +82,16 @@ function readCustomerName(snapshot, fallback) {
|
|
|
82
82
|
}
|
|
83
83
|
return fallback;
|
|
84
84
|
}
|
|
85
|
+
function stripAmounts(groups) {
|
|
86
|
+
return groups.map((group) => ({
|
|
87
|
+
...group,
|
|
88
|
+
rate: null,
|
|
89
|
+
amount: 0,
|
|
90
|
+
lines: group.lines.map(function strip(line) {
|
|
91
|
+
return { ...line, rate: null, amount: 0, children: line.children.map(strip) };
|
|
92
|
+
})
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
85
95
|
async function GET(req) {
|
|
86
96
|
try {
|
|
87
97
|
const context = await resolveReportRequestContext(req, { segment: "export" });
|
|
@@ -143,8 +153,9 @@ async function GET(req) {
|
|
|
143
153
|
issuedByLabel: null,
|
|
144
154
|
issuedAtLabel: report.closedAt ? report.closedAt.toISOString().slice(0, 10) : null,
|
|
145
155
|
currencyCode: sheet.currencyCode,
|
|
156
|
+
canSeeMoney,
|
|
146
157
|
showRates: (report.showRates ?? true) && canSeeMoney,
|
|
147
|
-
groups: sheet.totals.groups,
|
|
158
|
+
groups: canSeeMoney ? sheet.totals.groups : stripAmounts(sheet.totals.groups),
|
|
148
159
|
rows: canSeeMoney ? rows : rows.map((row) => ({ ...row, rate: null, amount: null })),
|
|
149
160
|
totals: {
|
|
150
161
|
billableMinutes: sheet.totals.billableMinutes,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/staff/api/timesheets/reports/%5Bid%5D/export/route.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `GET /api/staff/timesheets/reports/[id]/export?format=pdf|csv|xlsx` \u2014 US-G4.\n *\n * Two rules from screen 14:\n *\n * - **Note 3.** The export inherits the current grouping, filters, rounding and\n * currency, because it is produced from the same `buildReportSheet` the page\n * rendered. PDF mirrors that sheet; CSV and XLSX add the raw accounting\n * columns (date, person, description, raw vs rounded minutes).\n * - **Note 5.** Exporting NEVER locks. The only write here is an append-only\n * `exported` report event, which is what lets the unlock dialog say \"the PDF\n * was downloaded on the 20th\" before someone restates a billed hour.\n *\n * The event is appended after the file is built, so a failed render leaves no\n * trace of an export that never happened; a failed event write is logged and\n * does not deny the caller their file.\n */\n\nimport { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitStaffEvent } from '../../../../../events'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { StaffTimeReport, StaffTimeReportEvent } from '../../../../../data/entities'\nimport { loadReportProjectIds } from '../../../../../commands/timesheets-reports'\nimport { buildReportSheet } from '../../../../../lib/timesheets-reports/buildReportSheet'\nimport { buildReportRows } from '../../../../../lib/timesheets-reports/reportRows'\nimport { formatReportPeriodDate } from '../../../../../lib/timesheets-reports/reportPeriod'\nimport {\n normalizeReportExportFormat,\n serializeReportExport,\n supportedReportExportFormats,\n type ReportExportLabels,\n} from '../../../../../lib/timesheets-reports/reportExport'\nimport {\n hasReportGrouping,\n reportGroupingIds,\n type ReportGrouping,\n} from '../../../../../lib/timesheets-reports/reportGroupings'\nimport { z } from 'zod'\nimport { resolveReportRequestContext, reportSheetLabels, type Translate } from '../../shared'\nimport {\n readSearchParamsRecord,\n runTimesheetInterceptors,\n} from '../../../_shared/withTimesheetInterceptors'\n\nconst logger = createLogger('staff').child({ component: 'api/timesheets/reports/export' })\n\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['staff.timesheets.reports.view'] },\n}\n\n/**\n * EP-35 / EP-36: both accepted sets come from their registries, so a contributed\n * format or grouping is accepted here without this file being edited.\n */\nfunction readQuery(params: URLSearchParams): { format: string | null; grouping: ReportGrouping | undefined } {\n const grouping = params.get('grouping')\n return {\n format: params.get('format'),\n grouping: hasReportGrouping(grouping) ? (grouping as ReportGrouping) : undefined,\n }\n}\n\n/**\n * Read lazily rather than frozen at module load: a module registering a format\n * after this route file is first imported must still appear in the published\n * schema.\n */\nfunction exportQuerySchema(): z.ZodTypeAny {\n return z.object({\n format: z.enum(supportedReportExportFormats() as [string, ...string[]]),\n grouping: z.enum(reportGroupingIds() as [string, ...string[]]).optional(),\n })\n}\n\nfunction exportLabels(translate: Translate): ReportExportLabels {\n return {\n documentTitle: translate('staff.time_tracking.reports.sheet.overline', 'Time and cost statement'),\n issuedBy: translate('staff.time_tracking.reports.sheet.issuedBy', 'Issued by'),\n reference: translate('staff.time_tracking.reports.sheet.reference', 'Reference'),\n period: translate('staff.time_tracking.reports.sheet.period', 'Period'),\n line: translate('staff.time_tracking.reports.sheet.lineLabel', 'Line'),\n time: translate('staff.time_tracking.reports.sheet.time', 'Time'),\n rate: translate('staff.time_tracking.reports.sheet.rate', 'Rate'),\n amount: translate('staff.time_tracking.reports.sheet.amount', 'Amount'),\n total: translate('staff.time_tracking.reports.sheet.total', 'Total to invoice'),\n totalHint: translate(\n 'staff.time_tracking.reports.sheet.totalHint',\n '{billable} billable \u00B7 {nonbillable} non-billable \u00B7 {rounding}',\n ),\n nonbillable: translate('staff.time_tracking.reports.sheet.nonbillable', 'Non-billable time'),\n overrideBadge: translate('staff.time_tracking.reports.sheet.overrideBadge', 'agreed rate'),\n date: translate('staff.time_tracking.reports.export.date', 'Date'),\n project: translate('staff.time_tracking.reports.export.project', 'Project'),\n task: translate('staff.time_tracking.reports.export.task', 'Task'),\n person: translate('staff.time_tracking.reports.export.person', 'Person'),\n description: translate('staff.time_tracking.reports.export.description', 'Description'),\n billable: translate('staff.time_tracking.reports.export.billable', 'Billable'),\n yes: translate('staff.time_tracking.reports.export.yes', 'Yes'),\n no: translate('staff.time_tracking.reports.export.no', 'No'),\n rawMinutes: translate('staff.time_tracking.reports.export.rawMinutes', 'Raw minutes'),\n roundedMinutes: translate('staff.time_tracking.reports.export.roundedMinutes', 'Rounded minutes'),\n }\n}\n\nfunction roundingLabel(translate: Translate, unitMinutes: number, direction: string): string {\n if (!unitMinutes) return translate('staff.time_tracking.reports.rounding.off', 'no rounding')\n const directionLabel =\n direction === 'nearest'\n ? translate('staff.time_tracking.reports.rounding.nearest', 'to the nearest')\n : translate('staff.time_tracking.reports.rounding.up', 'always up')\n return translate('staff.time_tracking.reports.rounding.rule', 'rounding {minutes} min, {direction}')\n .replace('{minutes}', String(unitMinutes))\n .replace('{direction}', directionLabel)\n}\n\nfunction readCustomerName(snapshot: Record<string, unknown> | null | undefined, fallback: string): string {\n if (!snapshot) return fallback\n for (const key of ['name', 'displayName', 'display_name', 'legalName', 'legal_name']) {\n const value = snapshot[key]\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n }\n return fallback\n}\n\nexport async function GET(req: Request) {\n try {\n const context = await resolveReportRequestContext(req, { segment: 'export' })\n // `canSeeMoney` comes from the shared context and is never re-derived here:\n // the previous `grantedFeatures === null || authorize(...)` opened the money\n // fields whenever the grant read failed, and this route requires only\n // `reports.view`, so nothing else stood in the way.\n const { container, auth, tenantId, organizationId, reportId, translate, canSeeMoney, grantedFeatures } =\n context\n\n const interceptors = await runTimesheetInterceptors({\n request: req,\n method: 'GET',\n scope: { container, userId: auth.sub, tenantId, organizationId, userFeatures: grantedFeatures },\n query: readSearchParamsRecord(req.url),\n })\n if (!interceptors.ok) return interceptors.response\n const { session } = interceptors\n\n const { format: rawFormat, grouping } = readQuery(session.searchParams)\n const format = normalizeReportExportFormat(rawFormat)\n if (!format) {\n throw new CrudHttpError(400, {\n error: translate(\n 'staff.time_tracking.reports.errors.unsupportedFormat',\n 'Supported export formats are pdf, csv and xlsx.',\n ),\n supportedFormats: supportedReportExportFormats(),\n })\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n const report = await em.findOne(StaffTimeReport, {\n id: reportId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!report) {\n throw new CrudHttpError(404, {\n error: translate('staff.time_tracking.reports.errors.notFound', 'Report not found or not accessible.'),\n })\n }\n\n const timeProjectIds = await loadReportProjectIds(em, report.id, { tenantId, organizationId })\n const labels = reportSheetLabels(translate)\n const sheet = await buildReportSheet({\n em,\n scope: { tenantId, organizationId },\n report,\n timeProjectIds,\n labels,\n grouping,\n })\n const rows = buildReportRows({\n entries: sheet.entries,\n projects: sheet.projects,\n directory: sheet.directory,\n labels,\n })\n\n const periodFrom = formatReportPeriodDate(report.periodFrom) ?? ''\n const periodTo = formatReportPeriodDate(report.periodTo) ?? ''\n\n const file = serializeReportExport(format, {\n reference: report.reference,\n title: report.title,\n customerName: readCustomerName(report.customerSnapshot, report.title),\n periodLabel: `${periodFrom} \u2013 ${periodTo}`,\n issuedByLabel: null,\n issuedAtLabel: report.closedAt ? report.closedAt.toISOString().slice(0, 10) : null,\n currencyCode: sheet.currencyCode,\n showRates: (report.showRates ?? true) && canSeeMoney,\n groups: sheet.totals.groups,\n rows: canSeeMoney ? rows : rows.map((row) => ({ ...row, rate: null, amount: null })),\n totals: {\n billableMinutes: sheet.totals.billableMinutes,\n nonbillableMinutes: sheet.totals.nonbillableMinutes,\n totalAmount: canSeeMoney ? sheet.totals.totalAmount : 0,\n },\n roundingLabel: roundingLabel(\n translate,\n report.roundingUnitMinutes ?? 0,\n report.roundingDirection ?? 'up',\n ),\n labels: exportLabels(translate),\n })\n\n // Append-only audit. Never a lock \u2014 screen 14 note 5.\n try {\n const eventEm = (container.resolve('em') as EntityManager).fork()\n eventEm.persist(\n eventEm.create(StaffTimeReportEvent, {\n tenantId,\n organizationId,\n reportId: report.id,\n eventType: 'exported',\n reason: null,\n actorUserId: typeof auth.sub === 'string' ? auth.sub : null,\n metadata: { format, grouping: sheet.grouping, rowCount: rows.length },\n createdAt: new Date(),\n }),\n )\n await eventEm.flush()\n } catch (err) {\n // The caller already has a correct file; refusing it because the audit row\n // failed would be the wrong trade.\n logger.error('staff.timesheets.reports.export audit event failed', { err })\n }\n\n void emitStaffEvent('staff.timesheets.time_report.exported', {\n id: report.id,\n tenantId,\n organizationId,\n reference: report.reference,\n format,\n grouping: sheet.grouping,\n rowCount: rows.length,\n }, { persistent: true }).catch((err) => {\n logger.error('staff.timesheets emit time_report.exported failed', { err })\n })\n\n // The after-pass cannot rewrite an export's bytes, so what it shapes is the\n // file's descriptor: an interceptor may rename the download or restate its\n // media type, and a failing one replaces the file with its own error.\n const intercepted = await session.respondWithDescriptor({\n reportId: report.id,\n reference: report.reference,\n format,\n grouping: sheet.grouping,\n rowCount: rows.length,\n filename: file.filename,\n contentType: file.contentType,\n })\n if (!intercepted.ok) return intercepted.response\n\n const filename =\n typeof intercepted.descriptor.filename === 'string' && intercepted.descriptor.filename.trim().length > 0\n ? intercepted.descriptor.filename.trim()\n : file.filename\n const contentType =\n typeof intercepted.descriptor.contentType === 'string' && intercepted.descriptor.contentType.trim().length > 0\n ? intercepted.descriptor.contentType.trim()\n : file.contentType\n\n return new NextResponse(new Uint8Array(file.body), {\n status: 200,\n headers: {\n 'content-type': contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n 'cache-control': 'no-store',\n },\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('staff.timesheets.reports.export failed', { err })\n return NextResponse.json(\n { error: translate('staff.errors.internal', 'Internal server error') },\n { status: 500 },\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Export a customer report',\n methods: {\n GET: {\n summary: 'Export a customer report',\n get query() {\n return exportQuerySchema()\n },\n description:\n 'Renders the report as `pdf` (mirrors the on-screen sheet, client-facing), `csv` or `xlsx` (raw accounting columns: date, project, task, person, description, raw and rounded minutes, billable, rate, amount). Honours the report grouping \u2014 overridable per request with `?grouping=` \u2014 plus its filters, rounding and currency. Appends an `exported` report event; exporting never locks entries. Rates and amounts are omitted for a caller without staff.timesheets.rates.view.',\n responses: [{ status: 200, description: 'The rendered report file' }],\n errors: [\n { status: 400, description: 'Unsupported format' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.reports.view' },\n { status: 404, description: 'Report not found or not accessible' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAkBA,SAAS,oBAAoB;AAE7B,SAAS,eAAe,uBAAuB;AAC/C,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAE/B,SAAS,iBAAiB,4BAA4B;AACtD,SAAS,4BAA4B;AACrC,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;
|
|
4
|
+
"sourcesContent": ["/**\n * `GET /api/staff/timesheets/reports/[id]/export?format=pdf|csv|xlsx` \u2014 US-G4.\n *\n * Two rules from screen 14:\n *\n * - **Note 3.** The export inherits the current grouping, filters, rounding and\n * currency, because it is produced from the same `buildReportSheet` the page\n * rendered. PDF mirrors that sheet; CSV and XLSX add the raw accounting\n * columns (date, person, description, raw vs rounded minutes).\n * - **Note 5.** Exporting NEVER locks. The only write here is an append-only\n * `exported` report event, which is what lets the unlock dialog say \"the PDF\n * was downloaded on the 20th\" before someone restates a billed hour.\n *\n * The event is appended after the file is built, so a failed render leaves no\n * trace of an export that never happened; a failed event write is logged and\n * does not deny the caller their file.\n */\n\nimport { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitStaffEvent } from '../../../../../events'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { StaffTimeReport, StaffTimeReportEvent } from '../../../../../data/entities'\nimport { loadReportProjectIds } from '../../../../../commands/timesheets-reports'\nimport { buildReportSheet } from '../../../../../lib/timesheets-reports/buildReportSheet'\nimport { buildReportRows } from '../../../../../lib/timesheets-reports/reportRows'\nimport type { ReportGroup } from '../../../../../lib/timesheets-reports/reportTotals'\nimport { formatReportPeriodDate } from '../../../../../lib/timesheets-reports/reportPeriod'\nimport {\n normalizeReportExportFormat,\n serializeReportExport,\n supportedReportExportFormats,\n type ReportExportLabels,\n} from '../../../../../lib/timesheets-reports/reportExport'\nimport {\n hasReportGrouping,\n reportGroupingIds,\n type ReportGrouping,\n} from '../../../../../lib/timesheets-reports/reportGroupings'\nimport { z } from 'zod'\nimport { resolveReportRequestContext, reportSheetLabels, type Translate } from '../../shared'\nimport {\n readSearchParamsRecord,\n runTimesheetInterceptors,\n} from '../../../_shared/withTimesheetInterceptors'\n\nconst logger = createLogger('staff').child({ component: 'api/timesheets/reports/export' })\n\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['staff.timesheets.reports.view'] },\n}\n\n/**\n * EP-35 / EP-36: both accepted sets come from their registries, so a contributed\n * format or grouping is accepted here without this file being edited.\n */\nfunction readQuery(params: URLSearchParams): { format: string | null; grouping: ReportGrouping | undefined } {\n const grouping = params.get('grouping')\n return {\n format: params.get('format'),\n grouping: hasReportGrouping(grouping) ? (grouping as ReportGrouping) : undefined,\n }\n}\n\n/**\n * Read lazily rather than frozen at module load: a module registering a format\n * after this route file is first imported must still appear in the published\n * schema.\n */\nfunction exportQuerySchema(): z.ZodTypeAny {\n return z.object({\n format: z.enum(supportedReportExportFormats() as [string, ...string[]]),\n grouping: z.enum(reportGroupingIds() as [string, ...string[]]).optional(),\n })\n}\n\nfunction exportLabels(translate: Translate): ReportExportLabels {\n return {\n documentTitle: translate('staff.time_tracking.reports.sheet.overline', 'Time and cost statement'),\n issuedBy: translate('staff.time_tracking.reports.sheet.issuedBy', 'Issued by'),\n reference: translate('staff.time_tracking.reports.sheet.reference', 'Reference'),\n period: translate('staff.time_tracking.reports.sheet.period', 'Period'),\n line: translate('staff.time_tracking.reports.sheet.lineLabel', 'Line'),\n time: translate('staff.time_tracking.reports.sheet.time', 'Time'),\n rate: translate('staff.time_tracking.reports.sheet.rate', 'Rate'),\n amount: translate('staff.time_tracking.reports.sheet.amount', 'Amount'),\n total: translate('staff.time_tracking.reports.sheet.total', 'Total to invoice'),\n totalHint: translate(\n 'staff.time_tracking.reports.sheet.totalHint',\n '{billable} billable \u00B7 {nonbillable} non-billable \u00B7 {rounding}',\n ),\n nonbillable: translate('staff.time_tracking.reports.sheet.nonbillable', 'Non-billable time'),\n overrideBadge: translate('staff.time_tracking.reports.sheet.overrideBadge', 'agreed rate'),\n date: translate('staff.time_tracking.reports.export.date', 'Date'),\n project: translate('staff.time_tracking.reports.export.project', 'Project'),\n task: translate('staff.time_tracking.reports.export.task', 'Task'),\n person: translate('staff.time_tracking.reports.export.person', 'Person'),\n description: translate('staff.time_tracking.reports.export.description', 'Description'),\n billable: translate('staff.time_tracking.reports.export.billable', 'Billable'),\n yes: translate('staff.time_tracking.reports.export.yes', 'Yes'),\n no: translate('staff.time_tracking.reports.export.no', 'No'),\n rawMinutes: translate('staff.time_tracking.reports.export.rawMinutes', 'Raw minutes'),\n roundedMinutes: translate('staff.time_tracking.reports.export.roundedMinutes', 'Rounded minutes'),\n }\n}\n\nfunction roundingLabel(translate: Translate, unitMinutes: number, direction: string): string {\n if (!unitMinutes) return translate('staff.time_tracking.reports.rounding.off', 'no rounding')\n const directionLabel =\n direction === 'nearest'\n ? translate('staff.time_tracking.reports.rounding.nearest', 'to the nearest')\n : translate('staff.time_tracking.reports.rounding.up', 'always up')\n return translate('staff.time_tracking.reports.rounding.rule', 'rounding {minutes} min, {direction}')\n .replace('{minutes}', String(unitMinutes))\n .replace('{direction}', directionLabel)\n}\n\nfunction readCustomerName(snapshot: Record<string, unknown> | null | undefined, fallback: string): string {\n if (!snapshot) return fallback\n for (const key of ['name', 'displayName', 'display_name', 'legalName', 'legal_name']) {\n const value = snapshot[key]\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n }\n return fallback\n}\n\nfunction stripAmounts(groups: readonly ReportGroup[]): ReportGroup[] {\n return groups.map((group) => ({\n ...group,\n rate: null,\n amount: 0,\n lines: group.lines.map(function strip(line): ReportGroup['lines'][number] {\n return { ...line, rate: null, amount: 0, children: line.children.map(strip) }\n }),\n }))\n}\n\nexport async function GET(req: Request) {\n try {\n const context = await resolveReportRequestContext(req, { segment: 'export' })\n // `canSeeMoney` comes from the shared context and is never re-derived here:\n // the previous `grantedFeatures === null || authorize(...)` opened the money\n // fields whenever the grant read failed, and this route requires only\n // `reports.view`, so nothing else stood in the way.\n const { container, auth, tenantId, organizationId, reportId, translate, canSeeMoney, grantedFeatures } =\n context\n\n const interceptors = await runTimesheetInterceptors({\n request: req,\n method: 'GET',\n scope: { container, userId: auth.sub, tenantId, organizationId, userFeatures: grantedFeatures },\n query: readSearchParamsRecord(req.url),\n })\n if (!interceptors.ok) return interceptors.response\n const { session } = interceptors\n\n const { format: rawFormat, grouping } = readQuery(session.searchParams)\n const format = normalizeReportExportFormat(rawFormat)\n if (!format) {\n throw new CrudHttpError(400, {\n error: translate(\n 'staff.time_tracking.reports.errors.unsupportedFormat',\n 'Supported export formats are pdf, csv and xlsx.',\n ),\n supportedFormats: supportedReportExportFormats(),\n })\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n const report = await em.findOne(StaffTimeReport, {\n id: reportId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!report) {\n throw new CrudHttpError(404, {\n error: translate('staff.time_tracking.reports.errors.notFound', 'Report not found or not accessible.'),\n })\n }\n\n const timeProjectIds = await loadReportProjectIds(em, report.id, { tenantId, organizationId })\n const labels = reportSheetLabels(translate)\n const sheet = await buildReportSheet({\n em,\n scope: { tenantId, organizationId },\n report,\n timeProjectIds,\n labels,\n grouping,\n })\n const rows = buildReportRows({\n entries: sheet.entries,\n projects: sheet.projects,\n directory: sheet.directory,\n labels,\n })\n\n const periodFrom = formatReportPeriodDate(report.periodFrom) ?? ''\n const periodTo = formatReportPeriodDate(report.periodTo) ?? ''\n\n const file = serializeReportExport(format, {\n reference: report.reference,\n title: report.title,\n customerName: readCustomerName(report.customerSnapshot, report.title),\n periodLabel: `${periodFrom} \u2013 ${periodTo}`,\n issuedByLabel: null,\n issuedAtLabel: report.closedAt ? report.closedAt.toISOString().slice(0, 10) : null,\n currencyCode: sheet.currencyCode,\n canSeeMoney,\n showRates: (report.showRates ?? true) && canSeeMoney,\n groups: canSeeMoney ? sheet.totals.groups : stripAmounts(sheet.totals.groups),\n rows: canSeeMoney ? rows : rows.map((row) => ({ ...row, rate: null, amount: null })),\n totals: {\n billableMinutes: sheet.totals.billableMinutes,\n nonbillableMinutes: sheet.totals.nonbillableMinutes,\n totalAmount: canSeeMoney ? sheet.totals.totalAmount : 0,\n },\n roundingLabel: roundingLabel(\n translate,\n report.roundingUnitMinutes ?? 0,\n report.roundingDirection ?? 'up',\n ),\n labels: exportLabels(translate),\n })\n\n // Append-only audit. Never a lock \u2014 screen 14 note 5.\n try {\n const eventEm = (container.resolve('em') as EntityManager).fork()\n eventEm.persist(\n eventEm.create(StaffTimeReportEvent, {\n tenantId,\n organizationId,\n reportId: report.id,\n eventType: 'exported',\n reason: null,\n actorUserId: typeof auth.sub === 'string' ? auth.sub : null,\n metadata: { format, grouping: sheet.grouping, rowCount: rows.length },\n createdAt: new Date(),\n }),\n )\n await eventEm.flush()\n } catch (err) {\n // The caller already has a correct file; refusing it because the audit row\n // failed would be the wrong trade.\n logger.error('staff.timesheets.reports.export audit event failed', { err })\n }\n\n void emitStaffEvent('staff.timesheets.time_report.exported', {\n id: report.id,\n tenantId,\n organizationId,\n reference: report.reference,\n format,\n grouping: sheet.grouping,\n rowCount: rows.length,\n }, { persistent: true }).catch((err) => {\n logger.error('staff.timesheets emit time_report.exported failed', { err })\n })\n\n // The after-pass cannot rewrite an export's bytes, so what it shapes is the\n // file's descriptor: an interceptor may rename the download or restate its\n // media type, and a failing one replaces the file with its own error.\n const intercepted = await session.respondWithDescriptor({\n reportId: report.id,\n reference: report.reference,\n format,\n grouping: sheet.grouping,\n rowCount: rows.length,\n filename: file.filename,\n contentType: file.contentType,\n })\n if (!intercepted.ok) return intercepted.response\n\n const filename =\n typeof intercepted.descriptor.filename === 'string' && intercepted.descriptor.filename.trim().length > 0\n ? intercepted.descriptor.filename.trim()\n : file.filename\n const contentType =\n typeof intercepted.descriptor.contentType === 'string' && intercepted.descriptor.contentType.trim().length > 0\n ? intercepted.descriptor.contentType.trim()\n : file.contentType\n\n return new NextResponse(new Uint8Array(file.body), {\n status: 200,\n headers: {\n 'content-type': contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n 'cache-control': 'no-store',\n },\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('staff.timesheets.reports.export failed', { err })\n return NextResponse.json(\n { error: translate('staff.errors.internal', 'Internal server error') },\n { status: 500 },\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Export a customer report',\n methods: {\n GET: {\n summary: 'Export a customer report',\n get query() {\n return exportQuerySchema()\n },\n description:\n 'Renders the report as `pdf` (mirrors the on-screen sheet, client-facing), `csv` or `xlsx` (raw accounting columns: date, project, task, person, description, raw and rounded minutes, billable, rate, amount). Honours the report grouping \u2014 overridable per request with `?grouping=` \u2014 plus its filters, rounding and currency. Appends an `exported` report event; exporting never locks entries. Rates and amounts are omitted for a caller without staff.timesheets.rates.view.',\n responses: [{ status: 200, description: 'The rendered report file' }],\n errors: [\n { status: 400, description: 'Unsupported format' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing staff.timesheets.reports.view' },\n { status: 404, description: 'Report not found or not accessible' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -25,7 +25,7 @@ const COLUMN_LABEL_X = 42;
|
|
|
25
25
|
const COLUMN_TIME_X = 400;
|
|
26
26
|
const COLUMN_RATE_X = 470;
|
|
27
27
|
const COLUMN_AMOUNT_X = 553;
|
|
28
|
-
function pdfLinesForLine(line, depth, isNonBillable, showRates, currencyCode, labels) {
|
|
28
|
+
function pdfLinesForLine(line, depth, isNonBillable, showRates, canSeeMoney, currencyCode, labels) {
|
|
29
29
|
const label = line.hasOverride ? `${line.label} (${labels.overrideBadge})` : line.label;
|
|
30
30
|
const cells = [
|
|
31
31
|
{ text: `${" ".repeat(depth)}${label}`, x: COLUMN_LABEL_X, muted: depth > 0 },
|
|
@@ -38,20 +38,24 @@ function pdfLinesForLine(line, depth, isNonBillable, showRates, currencyCode, la
|
|
|
38
38
|
align: "right"
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
if (canSeeMoney) {
|
|
42
|
+
cells.push({
|
|
43
|
+
text: isNonBillable ? "\u2014" : formatAmount(line.amount, null),
|
|
44
|
+
x: COLUMN_AMOUNT_X,
|
|
45
|
+
align: "right"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
46
48
|
return [
|
|
47
49
|
{ kind: "cells", cells },
|
|
48
50
|
...line.children.flatMap(
|
|
49
|
-
(child) => pdfLinesForLine(child, depth + 1, isNonBillable, showRates, currencyCode, labels)
|
|
51
|
+
(child) => pdfLinesForLine(child, depth + 1, isNonBillable, showRates, canSeeMoney, currencyCode, labels)
|
|
50
52
|
)
|
|
51
53
|
];
|
|
52
54
|
}
|
|
53
55
|
function buildReportPdfLines(input) {
|
|
54
56
|
const { labels } = input;
|
|
57
|
+
const canSeeMoney = input.canSeeMoney !== false;
|
|
58
|
+
const showRates = input.showRates && canSeeMoney;
|
|
55
59
|
const lines = [
|
|
56
60
|
{
|
|
57
61
|
kind: "cells",
|
|
@@ -92,15 +96,15 @@ function buildReportPdfLines(input) {
|
|
|
92
96
|
cells: [
|
|
93
97
|
{ text: group.label, x: COLUMN_LABEL_X, bold: true },
|
|
94
98
|
{ text: formatReportMinutes(group.minutes), x: COLUMN_TIME_X, align: "right", bold: true },
|
|
95
|
-
{
|
|
99
|
+
...canSeeMoney ? [{
|
|
96
100
|
text: formatAmount(isNonBillable ? 0 : group.amount, input.currencyCode),
|
|
97
101
|
x: COLUMN_AMOUNT_X,
|
|
98
102
|
align: "right",
|
|
99
103
|
bold: true
|
|
100
|
-
}
|
|
104
|
+
}] : []
|
|
101
105
|
]
|
|
102
106
|
});
|
|
103
|
-
if (
|
|
107
|
+
if (showRates && !isNonBillable && group.rate !== null) {
|
|
104
108
|
lines.push({
|
|
105
109
|
kind: "cells",
|
|
106
110
|
cells: [{ text: `${formatAmount(group.rate, input.currencyCode)}/h`, x: COLUMN_LABEL_X, muted: true, size: 8 }]
|
|
@@ -110,20 +114,22 @@ function buildReportPdfLines(input) {
|
|
|
110
114
|
{ text: labels.line, x: COLUMN_LABEL_X, size: 8, muted: true },
|
|
111
115
|
{ text: labels.time, x: COLUMN_TIME_X, align: "right", size: 8, muted: true }
|
|
112
116
|
];
|
|
113
|
-
if (
|
|
117
|
+
if (showRates) {
|
|
114
118
|
header.push({ text: labels.rate, x: COLUMN_RATE_X, align: "right", size: 8, muted: true });
|
|
115
119
|
}
|
|
116
|
-
|
|
120
|
+
if (canSeeMoney) {
|
|
121
|
+
header.push({ text: labels.amount, x: COLUMN_AMOUNT_X, align: "right", size: 8, muted: true });
|
|
122
|
+
}
|
|
117
123
|
lines.push({ kind: "cells", cells: header });
|
|
118
124
|
for (const line of group.lines) {
|
|
119
125
|
lines.push(
|
|
120
|
-
...pdfLinesForLine(line, 0, isNonBillable,
|
|
126
|
+
...pdfLinesForLine(line, 0, isNonBillable, showRates, canSeeMoney, input.currencyCode, labels)
|
|
121
127
|
);
|
|
122
128
|
}
|
|
123
129
|
lines.push({ kind: "space", height: 8 }, { kind: "rule" });
|
|
124
130
|
}
|
|
125
|
-
|
|
126
|
-
{
|
|
131
|
+
if (canSeeMoney) {
|
|
132
|
+
lines.push({
|
|
127
133
|
kind: "cells",
|
|
128
134
|
cells: [
|
|
129
135
|
{ text: labels.total, x: COLUMN_LABEL_X, bold: true, size: 11 },
|
|
@@ -135,7 +141,9 @@ function buildReportPdfLines(input) {
|
|
|
135
141
|
size: 11
|
|
136
142
|
}
|
|
137
143
|
]
|
|
138
|
-
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
lines.push(
|
|
139
147
|
{
|
|
140
148
|
kind: "cells",
|
|
141
149
|
cells: [
|
|
@@ -152,6 +160,8 @@ function buildReportPdfLines(input) {
|
|
|
152
160
|
}
|
|
153
161
|
function buildReportTable(input) {
|
|
154
162
|
const { labels } = input;
|
|
163
|
+
const canSeeMoney = input.canSeeMoney !== false;
|
|
164
|
+
const showRates = input.showRates && canSeeMoney;
|
|
155
165
|
const columns = [
|
|
156
166
|
{ field: "date", header: labels.date },
|
|
157
167
|
{ field: "project", header: labels.project },
|
|
@@ -162,8 +172,8 @@ function buildReportTable(input) {
|
|
|
162
172
|
{ field: "roundedMinutes", header: labels.roundedMinutes },
|
|
163
173
|
{ field: "hours", header: labels.time },
|
|
164
174
|
{ field: "billable", header: labels.billable },
|
|
165
|
-
...
|
|
166
|
-
{ field: "amount", header: labels.amount }
|
|
175
|
+
...showRates ? [{ field: "rate", header: labels.rate }] : [],
|
|
176
|
+
...canSeeMoney ? [{ field: "amount", header: labels.amount }] : []
|
|
167
177
|
];
|
|
168
178
|
const rows = input.rows.map((row) => {
|
|
169
179
|
const record = {
|
|
@@ -175,10 +185,10 @@ function buildReportTable(input) {
|
|
|
175
185
|
rawMinutes: row.rawMinutes,
|
|
176
186
|
roundedMinutes: row.minutes,
|
|
177
187
|
hours: row.hours,
|
|
178
|
-
billable: row.isBillable ? labels.yes : labels.no
|
|
179
|
-
amount: row.amount === null ? "" : row.amount.toFixed(2)
|
|
188
|
+
billable: row.isBillable ? labels.yes : labels.no
|
|
180
189
|
};
|
|
181
|
-
if (
|
|
190
|
+
if (canSeeMoney) record.amount = row.amount === null ? "" : row.amount.toFixed(2);
|
|
191
|
+
if (showRates) record.rate = row.rate === null ? "" : row.rate.toFixed(2);
|
|
182
192
|
return record;
|
|
183
193
|
});
|
|
184
194
|
return { columns, rows };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/staff/lib/timesheets-reports/reportExport.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Turning a computed report into the three deliverables of US-G4.\n *\n * Screen 14 note 3 sets the split: **PDF mirrors the sheet** the client is\n * shown \u2014 the same groups, the same rolled-up lines, the same grand total \u2014 and\n * **CSV/XLSX add the raw columns** (date, person, description) an accounts\n * department reconciles against. All three honour the current grouping, the\n * filters, the rounding and the currency, because all three are built from the\n * same `ReportSheet` the screen rendered.\n *\n * No format locks anything (screen 14 note 5). Exporting appends an `exported`\n * event and nothing else.\n */\n\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { serializeExport, type PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport { buildPdf, PDF_CONTENT_TYPE, type PdfLine } from './pdf'\nimport { buildXlsx, XLSX_CONTENT_TYPE } from './xlsx'\nimport { formatReportMinutes, type ReportGroup, type ReportLine } from './reportTotals'\nimport type { ReportRow } from './reportRows'\nimport {\n getReportExportFormat,\n registerBuiltInReportExportFormat,\n reportExportFormatIds,\n type ReportExportFormat,\n} from './reportExportFormats'\n\nconst logger = createLogger('staff').child({ component: 'timesheets-reports/reportExport' })\n\nexport type { ReportExportFormat }\n\n/**\n * EP-35: the accepted set is the registry, not a literal union, so a contributed\n * format is accepted here and documented by the route's OpenAPI enum without\n * either being edited.\n */\nexport function normalizeReportExportFormat(value: unknown): ReportExportFormat | null {\n if (typeof value !== 'string') return null\n return getReportExportFormat(value) ? value : null\n}\n\nexport function supportedReportExportFormats(): string[] {\n return reportExportFormatIds()\n}\n\nexport type ReportExportLabels = {\n documentTitle: string\n issuedBy: string\n reference: string\n period: string\n line: string\n time: string\n rate: string\n amount: string\n total: string\n totalHint: string\n nonbillable: string\n overrideBadge: string\n date: string\n project: string\n task: string\n person: string\n description: string\n billable: string\n yes: string\n no: string\n rawMinutes: string\n roundedMinutes: string\n}\n\nexport type ReportExportInput = {\n reference: string\n title: string\n customerName: string\n periodLabel: string\n issuedByLabel: string | null\n issuedAtLabel: string | null\n currencyCode: string | null\n showRates: boolean\n groups: readonly ReportGroup[]\n rows: readonly ReportRow[]\n totals: { billableMinutes: number; nonbillableMinutes: number; totalAmount: number }\n roundingLabel: string\n labels: ReportExportLabels\n}\n\nexport type SerializedReportExport = {\n body: Buffer\n contentType: string\n filename: string\n}\n\nfunction formatAmount(value: number | null | undefined, currencyCode: string | null): string {\n if (value === null || value === undefined) return '\u2014'\n const fixed = value.toFixed(2)\n return currencyCode ? `${fixed} ${currencyCode}` : fixed\n}\n\nconst COLUMN_LABEL_X = 42\nconst COLUMN_TIME_X = 400\nconst COLUMN_RATE_X = 470\nconst COLUMN_AMOUNT_X = 553\n\n/** Recursion is one level deep by construction (D-2), but written generally. */\nfunction pdfLinesForLine(\n line: ReportLine,\n depth: number,\n isNonBillable: boolean,\n showRates: boolean,\n currencyCode: string | null,\n labels: ReportExportLabels,\n): PdfLine[] {\n const label = line.hasOverride ? `${line.label} (${labels.overrideBadge})` : line.label\n const cells = [\n { text: `${' '.repeat(depth)}${label}`, x: COLUMN_LABEL_X, muted: depth > 0 },\n { text: formatReportMinutes(line.minutes), x: COLUMN_TIME_X, align: 'right' as const },\n ]\n if (showRates) {\n cells.push({\n text: isNonBillable ? '\u2014' : formatAmount(line.rate, null),\n x: COLUMN_RATE_X,\n align: 'right' as const,\n })\n }\n cells.push({\n text: isNonBillable ? '\u2014' : formatAmount(line.amount, null),\n x: COLUMN_AMOUNT_X,\n align: 'right' as const,\n })\n\n return [\n { kind: 'cells', cells },\n ...line.children.flatMap((child) =>\n pdfLinesForLine(child, depth + 1, isNonBillable, showRates, currencyCode, labels),\n ),\n ]\n}\n\n/** The PDF mirrors the on-screen sheet, group for group and line for line. */\nexport function buildReportPdfLines(input: ReportExportInput): PdfLine[] {\n const { labels } = input\n const lines: PdfLine[] = [\n {\n kind: 'cells',\n cells: [{ text: labels.documentTitle, x: COLUMN_LABEL_X, size: 8, muted: true }],\n },\n { kind: 'cells', cells: [{ text: input.customerName, x: COLUMN_LABEL_X, size: 15, bold: true }] },\n { kind: 'cells', cells: [{ text: input.periodLabel, x: COLUMN_LABEL_X, muted: true }] },\n {\n kind: 'cells',\n cells: [\n {\n text: `${labels.reference}: ${input.reference}`,\n x: COLUMN_AMOUNT_X,\n align: 'right',\n muted: true,\n },\n ],\n },\n ]\n if (input.issuedByLabel) {\n lines.push({\n kind: 'cells',\n cells: [\n {\n text: `${labels.issuedBy}: ${input.issuedByLabel}${input.issuedAtLabel ? ` \u00B7 ${input.issuedAtLabel}` : ''}`,\n x: COLUMN_AMOUNT_X,\n align: 'right',\n muted: true,\n },\n ],\n })\n }\n lines.push({ kind: 'space', height: 10 }, { kind: 'rule' })\n\n for (const group of input.groups) {\n const isNonBillable = group.kind === 'nonbillable'\n lines.push({\n kind: 'cells',\n cells: [\n { text: group.label, x: COLUMN_LABEL_X, bold: true },\n { text: formatReportMinutes(group.minutes), x: COLUMN_TIME_X, align: 'right', bold: true },\n {\n text: formatAmount(isNonBillable ? 0 : group.amount, input.currencyCode),\n x: COLUMN_AMOUNT_X,\n align: 'right',\n bold: true,\n },\n ],\n })\n if (input.showRates && !isNonBillable && group.rate !== null) {\n lines.push({\n kind: 'cells',\n cells: [{ text: `${formatAmount(group.rate, input.currencyCode)}/h`, x: COLUMN_LABEL_X, muted: true, size: 8 }],\n })\n }\n\n const header = [\n { text: labels.line, x: COLUMN_LABEL_X, size: 8, muted: true },\n { text: labels.time, x: COLUMN_TIME_X, align: 'right' as const, size: 8, muted: true },\n ]\n if (input.showRates) {\n header.push({ text: labels.rate, x: COLUMN_RATE_X, align: 'right' as const, size: 8, muted: true })\n }\n header.push({ text: labels.amount, x: COLUMN_AMOUNT_X, align: 'right' as const, size: 8, muted: true })\n lines.push({ kind: 'cells', cells: header })\n\n for (const line of group.lines) {\n lines.push(\n ...pdfLinesForLine(line, 0, isNonBillable, input.showRates, input.currencyCode, labels),\n )\n }\n lines.push({ kind: 'space', height: 8 }, { kind: 'rule' })\n }\n\n lines.push(\n {\n kind: 'cells',\n cells: [\n { text: labels.total, x: COLUMN_LABEL_X, bold: true, size: 11 },\n {\n text: formatAmount(input.totals.totalAmount, input.currencyCode),\n x: COLUMN_AMOUNT_X,\n align: 'right',\n bold: true,\n size: 11,\n },\n ],\n },\n {\n kind: 'cells',\n cells: [\n {\n text: labels.totalHint\n .replace('{billable}', formatReportMinutes(input.totals.billableMinutes))\n .replace('{nonbillable}', formatReportMinutes(input.totals.nonbillableMinutes))\n .replace('{rounding}', input.roundingLabel),\n x: COLUMN_LABEL_X,\n size: 8,\n muted: true,\n },\n ],\n },\n )\n\n return lines\n}\n\n/**\n * The raw table. `rawMinutes` sits beside `roundedMinutes` on purpose: the\n * difference between them IS the rounding, and an accountant asking why an\n * amount is what it is should be able to see it rather than derive it.\n */\nexport function buildReportTable(input: ReportExportInput): PreparedExport {\n const { labels } = input\n const columns = [\n { field: 'date', header: labels.date },\n { field: 'project', header: labels.project },\n { field: 'task', header: labels.task },\n { field: 'person', header: labels.person },\n { field: 'description', header: labels.description },\n { field: 'rawMinutes', header: labels.rawMinutes },\n { field: 'roundedMinutes', header: labels.roundedMinutes },\n { field: 'hours', header: labels.time },\n { field: 'billable', header: labels.billable },\n ...(input.showRates ? [{ field: 'rate', header: labels.rate }] : []),\n { field: 'amount', header: labels.amount },\n ]\n\n const rows = input.rows.map((row) => {\n const record: Record<string, unknown> = {\n date: row.date,\n project: row.projectName,\n task: row.taskLabel,\n person: row.personLabel,\n description: row.description,\n rawMinutes: row.rawMinutes,\n roundedMinutes: row.minutes,\n hours: row.hours,\n billable: row.isBillable ? labels.yes : labels.no,\n amount: row.amount === null ? '' : row.amount.toFixed(2),\n }\n if (input.showRates) record.rate = row.rate === null ? '' : row.rate.toFixed(2)\n return record\n })\n\n return { columns, rows }\n}\n\nfunction exportBaseName(input: ReportExportInput): string {\n return input.reference || 'report'\n}\n\nregisterBuiltInReportExportFormat({\n id: 'pdf',\n labelKey: 'staff.time_tracking.reports.export.format.pdf',\n mimeType: PDF_CONTENT_TYPE,\n extension: 'pdf',\n serialize: (input) => ({\n body: buildPdf({ title: input.title, lines: buildReportPdfLines(input) }),\n contentType: PDF_CONTENT_TYPE,\n filename: `${exportBaseName(input)}.pdf`,\n }),\n})\n\nregisterBuiltInReportExportFormat({\n id: 'csv',\n labelKey: 'staff.time_tracking.reports.export.format.csv',\n mimeType: 'text/csv',\n extension: 'csv',\n serialize: (input) => {\n const serialized = serializeExport(buildReportTable(input), 'csv')\n return {\n // The BOM is what makes Excel open a UTF-8 CSV with Polish names intact\n // instead of mojibake; every other consumer ignores it.\n body: Buffer.concat([Buffer.from('\uFEFF', 'utf8'), Buffer.from(serialized.body, 'utf8')]),\n contentType: serialized.contentType,\n filename: `${exportBaseName(input)}.csv`,\n }\n },\n})\n\nregisterBuiltInReportExportFormat({\n id: 'xlsx',\n labelKey: 'staff.time_tracking.reports.export.format.xlsx',\n mimeType: XLSX_CONTENT_TYPE,\n extension: 'xlsx',\n serialize: (input) => {\n const table = buildReportTable(input)\n const rows = [\n table.columns.map((column) => column.header),\n ...table.rows.map((row) =>\n table.columns.map((column) => {\n const value = row[column.field]\n if (column.field === 'rate' || column.field === 'amount') {\n const parsed = typeof value === 'string' && value.length > 0 ? Number.parseFloat(value) : null\n return parsed !== null && Number.isFinite(parsed) ? parsed : ''\n }\n return typeof value === 'number' ? value : value === null || value === undefined ? '' : String(value)\n }),\n ),\n ]\n return {\n body: buildXlsx({ name: exportBaseName(input).slice(0, 31), rows }),\n contentType: XLSX_CONTENT_TYPE,\n filename: `${exportBaseName(input)}.xlsx`,\n }\n },\n})\n\n/**\n * A contributed `serialize` is the one strategy whose failure cannot be absorbed:\n * the caller asked for a named format and every fallback would answer with bytes of\n * a different type under the requested filename and MIME. So the throw propagates \u2014\n * but it is caught and re-raised as an internal error naming the format, so the one\n * request fails with something diagnosable instead of a stack trace from a third\n * party's serializer.\n */\nexport function serializeReportExport(\n format: ReportExportFormat,\n input: ReportExportInput,\n): SerializedReportExport {\n const definition = getReportExportFormat(format)\n if (!definition) {\n throw new Error(`[internal] unknown report export format: ${String(format)}`)\n }\n try {\n return definition.serialize(input)\n } catch (err) {\n logger.error('a report export format failed to serialize', { format: definition.id, err })\n throw new Error(`[internal] report export format ${definition.id} failed to serialize`)\n }\n}\n"],
|
|
5
|
-
"mappings": "AAcA,SAAS,oBAAoB;AAC7B,SAAS,uBAA4C;AACrD,SAAS,UAAU,wBAAsC;AACzD,SAAS,WAAW,yBAAyB;AAC7C,SAAS,2BAA8D;AAEvE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,MAAM,SAAS,aAAa,OAAO,EAAE,MAAM,EAAE,WAAW,kCAAkC,CAAC;AASpF,SAAS,4BAA4B,OAA2C;AACrF,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO,sBAAsB,KAAK,IAAI,QAAQ;AAChD;AAEO,SAAS,+BAAyC;AACvD,SAAO,sBAAsB;AAC/B;
|
|
4
|
+
"sourcesContent": ["/**\n * Turning a computed report into the three deliverables of US-G4.\n *\n * Screen 14 note 3 sets the split: **PDF mirrors the sheet** the client is\n * shown \u2014 the same groups, the same rolled-up lines, the same grand total \u2014 and\n * **CSV/XLSX add the raw columns** (date, person, description) an accounts\n * department reconciles against. All three honour the current grouping, the\n * filters, the rounding and the currency, because all three are built from the\n * same `ReportSheet` the screen rendered.\n *\n * No format locks anything (screen 14 note 5). Exporting appends an `exported`\n * event and nothing else.\n */\n\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { serializeExport, type PreparedExport } from '@open-mercato/shared/lib/crud/exporters'\nimport { buildPdf, PDF_CONTENT_TYPE, type PdfLine } from './pdf'\nimport { buildXlsx, XLSX_CONTENT_TYPE } from './xlsx'\nimport { formatReportMinutes, type ReportGroup, type ReportLine } from './reportTotals'\nimport type { ReportRow } from './reportRows'\nimport {\n getReportExportFormat,\n registerBuiltInReportExportFormat,\n reportExportFormatIds,\n type ReportExportFormat,\n} from './reportExportFormats'\n\nconst logger = createLogger('staff').child({ component: 'timesheets-reports/reportExport' })\n\nexport type { ReportExportFormat }\n\n/**\n * EP-35: the accepted set is the registry, not a literal union, so a contributed\n * format is accepted here and documented by the route's OpenAPI enum without\n * either being edited.\n */\nexport function normalizeReportExportFormat(value: unknown): ReportExportFormat | null {\n if (typeof value !== 'string') return null\n return getReportExportFormat(value) ? value : null\n}\n\nexport function supportedReportExportFormats(): string[] {\n return reportExportFormatIds()\n}\n\nexport type ReportExportLabels = {\n documentTitle: string\n issuedBy: string\n reference: string\n period: string\n line: string\n time: string\n rate: string\n amount: string\n total: string\n totalHint: string\n nonbillable: string\n overrideBadge: string\n date: string\n project: string\n task: string\n person: string\n description: string\n billable: string\n yes: string\n no: string\n rawMinutes: string\n roundedMinutes: string\n}\n\nexport type ReportExportInput = {\n reference: string\n title: string\n customerName: string\n periodLabel: string\n issuedByLabel: string | null\n issuedAtLabel: string | null\n currencyCode: string | null\n /** Omission preserves legacy visibility; the export route always supplies its permission decision. */\n canSeeMoney?: boolean\n showRates: boolean\n groups: readonly ReportGroup[]\n rows: readonly ReportRow[]\n totals: { billableMinutes: number; nonbillableMinutes: number; totalAmount: number }\n roundingLabel: string\n labels: ReportExportLabels\n}\n\nexport type SerializedReportExport = {\n body: Buffer\n contentType: string\n filename: string\n}\n\nfunction formatAmount(value: number | null | undefined, currencyCode: string | null): string {\n if (value === null || value === undefined) return '\u2014'\n const fixed = value.toFixed(2)\n return currencyCode ? `${fixed} ${currencyCode}` : fixed\n}\n\nconst COLUMN_LABEL_X = 42\nconst COLUMN_TIME_X = 400\nconst COLUMN_RATE_X = 470\nconst COLUMN_AMOUNT_X = 553\n\n/** Recursion is one level deep by construction (D-2), but written generally. */\nfunction pdfLinesForLine(\n line: ReportLine,\n depth: number,\n isNonBillable: boolean,\n showRates: boolean,\n canSeeMoney: boolean,\n currencyCode: string | null,\n labels: ReportExportLabels,\n): PdfLine[] {\n const label = line.hasOverride ? `${line.label} (${labels.overrideBadge})` : line.label\n const cells = [\n { text: `${' '.repeat(depth)}${label}`, x: COLUMN_LABEL_X, muted: depth > 0 },\n { text: formatReportMinutes(line.minutes), x: COLUMN_TIME_X, align: 'right' as const },\n ]\n if (showRates) {\n cells.push({\n text: isNonBillable ? '\u2014' : formatAmount(line.rate, null),\n x: COLUMN_RATE_X,\n align: 'right' as const,\n })\n }\n if (canSeeMoney) {\n cells.push({\n text: isNonBillable ? '\u2014' : formatAmount(line.amount, null),\n x: COLUMN_AMOUNT_X,\n align: 'right' as const,\n })\n }\n\n return [\n { kind: 'cells', cells },\n ...line.children.flatMap((child) =>\n pdfLinesForLine(child, depth + 1, isNonBillable, showRates, canSeeMoney, currencyCode, labels),\n ),\n ]\n}\n\n/** The PDF mirrors the on-screen sheet, group for group and line for line. */\nexport function buildReportPdfLines(input: ReportExportInput): PdfLine[] {\n const { labels } = input\n const canSeeMoney = input.canSeeMoney !== false\n const showRates = input.showRates && canSeeMoney\n const lines: PdfLine[] = [\n {\n kind: 'cells',\n cells: [{ text: labels.documentTitle, x: COLUMN_LABEL_X, size: 8, muted: true }],\n },\n { kind: 'cells', cells: [{ text: input.customerName, x: COLUMN_LABEL_X, size: 15, bold: true }] },\n { kind: 'cells', cells: [{ text: input.periodLabel, x: COLUMN_LABEL_X, muted: true }] },\n {\n kind: 'cells',\n cells: [\n {\n text: `${labels.reference}: ${input.reference}`,\n x: COLUMN_AMOUNT_X,\n align: 'right',\n muted: true,\n },\n ],\n },\n ]\n if (input.issuedByLabel) {\n lines.push({\n kind: 'cells',\n cells: [\n {\n text: `${labels.issuedBy}: ${input.issuedByLabel}${input.issuedAtLabel ? ` \u00B7 ${input.issuedAtLabel}` : ''}`,\n x: COLUMN_AMOUNT_X,\n align: 'right',\n muted: true,\n },\n ],\n })\n }\n lines.push({ kind: 'space', height: 10 }, { kind: 'rule' })\n\n for (const group of input.groups) {\n const isNonBillable = group.kind === 'nonbillable'\n lines.push({\n kind: 'cells',\n cells: [\n { text: group.label, x: COLUMN_LABEL_X, bold: true },\n { text: formatReportMinutes(group.minutes), x: COLUMN_TIME_X, align: 'right', bold: true },\n ...(canSeeMoney ? [{\n text: formatAmount(isNonBillable ? 0 : group.amount, input.currencyCode),\n x: COLUMN_AMOUNT_X,\n align: 'right' as const,\n bold: true,\n }] : []),\n ],\n })\n if (showRates && !isNonBillable && group.rate !== null) {\n lines.push({\n kind: 'cells',\n cells: [{ text: `${formatAmount(group.rate, input.currencyCode)}/h`, x: COLUMN_LABEL_X, muted: true, size: 8 }],\n })\n }\n\n const header = [\n { text: labels.line, x: COLUMN_LABEL_X, size: 8, muted: true },\n { text: labels.time, x: COLUMN_TIME_X, align: 'right' as const, size: 8, muted: true },\n ]\n if (showRates) {\n header.push({ text: labels.rate, x: COLUMN_RATE_X, align: 'right' as const, size: 8, muted: true })\n }\n if (canSeeMoney) {\n header.push({ text: labels.amount, x: COLUMN_AMOUNT_X, align: 'right' as const, size: 8, muted: true })\n }\n lines.push({ kind: 'cells', cells: header })\n\n for (const line of group.lines) {\n lines.push(\n ...pdfLinesForLine(line, 0, isNonBillable, showRates, canSeeMoney, input.currencyCode, labels),\n )\n }\n lines.push({ kind: 'space', height: 8 }, { kind: 'rule' })\n }\n\n if (canSeeMoney) {\n lines.push({\n kind: 'cells',\n cells: [\n { text: labels.total, x: COLUMN_LABEL_X, bold: true, size: 11 },\n {\n text: formatAmount(input.totals.totalAmount, input.currencyCode),\n x: COLUMN_AMOUNT_X,\n align: 'right',\n bold: true,\n size: 11,\n },\n ],\n })\n }\n lines.push(\n {\n kind: 'cells',\n cells: [\n {\n text: labels.totalHint\n .replace('{billable}', formatReportMinutes(input.totals.billableMinutes))\n .replace('{nonbillable}', formatReportMinutes(input.totals.nonbillableMinutes))\n .replace('{rounding}', input.roundingLabel),\n x: COLUMN_LABEL_X,\n size: 8,\n muted: true,\n },\n ],\n },\n )\n\n return lines\n}\n\n/**\n * The raw table. `rawMinutes` sits beside `roundedMinutes` on purpose: the\n * difference between them IS the rounding, and an accountant asking why an\n * amount is what it is should be able to see it rather than derive it.\n */\nexport function buildReportTable(input: ReportExportInput): PreparedExport {\n const { labels } = input\n const canSeeMoney = input.canSeeMoney !== false\n const showRates = input.showRates && canSeeMoney\n const columns = [\n { field: 'date', header: labels.date },\n { field: 'project', header: labels.project },\n { field: 'task', header: labels.task },\n { field: 'person', header: labels.person },\n { field: 'description', header: labels.description },\n { field: 'rawMinutes', header: labels.rawMinutes },\n { field: 'roundedMinutes', header: labels.roundedMinutes },\n { field: 'hours', header: labels.time },\n { field: 'billable', header: labels.billable },\n ...(showRates ? [{ field: 'rate', header: labels.rate }] : []),\n ...(canSeeMoney ? [{ field: 'amount', header: labels.amount }] : []),\n ]\n\n const rows = input.rows.map((row) => {\n const record: Record<string, unknown> = {\n date: row.date,\n project: row.projectName,\n task: row.taskLabel,\n person: row.personLabel,\n description: row.description,\n rawMinutes: row.rawMinutes,\n roundedMinutes: row.minutes,\n hours: row.hours,\n billable: row.isBillable ? labels.yes : labels.no,\n }\n if (canSeeMoney) record.amount = row.amount === null ? '' : row.amount.toFixed(2)\n if (showRates) record.rate = row.rate === null ? '' : row.rate.toFixed(2)\n return record\n })\n\n return { columns, rows }\n}\n\nfunction exportBaseName(input: ReportExportInput): string {\n return input.reference || 'report'\n}\n\nregisterBuiltInReportExportFormat({\n id: 'pdf',\n labelKey: 'staff.time_tracking.reports.export.format.pdf',\n mimeType: PDF_CONTENT_TYPE,\n extension: 'pdf',\n serialize: (input) => ({\n body: buildPdf({ title: input.title, lines: buildReportPdfLines(input) }),\n contentType: PDF_CONTENT_TYPE,\n filename: `${exportBaseName(input)}.pdf`,\n }),\n})\n\nregisterBuiltInReportExportFormat({\n id: 'csv',\n labelKey: 'staff.time_tracking.reports.export.format.csv',\n mimeType: 'text/csv',\n extension: 'csv',\n serialize: (input) => {\n const serialized = serializeExport(buildReportTable(input), 'csv')\n return {\n // The BOM is what makes Excel open a UTF-8 CSV with Polish names intact\n // instead of mojibake; every other consumer ignores it.\n body: Buffer.concat([Buffer.from('\uFEFF', 'utf8'), Buffer.from(serialized.body, 'utf8')]),\n contentType: serialized.contentType,\n filename: `${exportBaseName(input)}.csv`,\n }\n },\n})\n\nregisterBuiltInReportExportFormat({\n id: 'xlsx',\n labelKey: 'staff.time_tracking.reports.export.format.xlsx',\n mimeType: XLSX_CONTENT_TYPE,\n extension: 'xlsx',\n serialize: (input) => {\n const table = buildReportTable(input)\n const rows = [\n table.columns.map((column) => column.header),\n ...table.rows.map((row) =>\n table.columns.map((column) => {\n const value = row[column.field]\n if (column.field === 'rate' || column.field === 'amount') {\n const parsed = typeof value === 'string' && value.length > 0 ? Number.parseFloat(value) : null\n return parsed !== null && Number.isFinite(parsed) ? parsed : ''\n }\n return typeof value === 'number' ? value : value === null || value === undefined ? '' : String(value)\n }),\n ),\n ]\n return {\n body: buildXlsx({ name: exportBaseName(input).slice(0, 31), rows }),\n contentType: XLSX_CONTENT_TYPE,\n filename: `${exportBaseName(input)}.xlsx`,\n }\n },\n})\n\n/**\n * A contributed `serialize` is the one strategy whose failure cannot be absorbed:\n * the caller asked for a named format and every fallback would answer with bytes of\n * a different type under the requested filename and MIME. So the throw propagates \u2014\n * but it is caught and re-raised as an internal error naming the format, so the one\n * request fails with something diagnosable instead of a stack trace from a third\n * party's serializer.\n */\nexport function serializeReportExport(\n format: ReportExportFormat,\n input: ReportExportInput,\n): SerializedReportExport {\n const definition = getReportExportFormat(format)\n if (!definition) {\n throw new Error(`[internal] unknown report export format: ${String(format)}`)\n }\n try {\n return definition.serialize(input)\n } catch (err) {\n logger.error('a report export format failed to serialize', { format: definition.id, err })\n throw new Error(`[internal] report export format ${definition.id} failed to serialize`)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7669.1.120a2cf7f1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -265,16 +265,16 @@
|
|
|
265
265
|
"zod": "^4.4.3"
|
|
266
266
|
},
|
|
267
267
|
"peerDependencies": {
|
|
268
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
269
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
268
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7669.1.120a2cf7f1",
|
|
269
|
+
"@open-mercato/shared": "0.10.1-develop.7669.1.120a2cf7f1",
|
|
270
|
+
"@open-mercato/ui": "0.10.1-develop.7669.1.120a2cf7f1",
|
|
271
271
|
"react": "^19.0.0",
|
|
272
272
|
"react-dom": "^19.0.0"
|
|
273
273
|
},
|
|
274
274
|
"devDependencies": {
|
|
275
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
276
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
275
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7669.1.120a2cf7f1",
|
|
276
|
+
"@open-mercato/shared": "0.10.1-develop.7669.1.120a2cf7f1",
|
|
277
|
+
"@open-mercato/ui": "0.10.1-develop.7669.1.120a2cf7f1",
|
|
278
278
|
"@testing-library/dom": "^10.4.1",
|
|
279
279
|
"@testing-library/jest-dom": "^7.0.1",
|
|
280
280
|
"@testing-library/react": "^16.3.3",
|
|
@@ -504,14 +504,22 @@ export async function PUT(request: Request): Promise<Response> {
|
|
|
504
504
|
...(nextDone !== undefined ? { status: nextDone ? 'done' : 'planned' } : {}),
|
|
505
505
|
...(customValues ? {
|
|
506
506
|
customValues,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
507
|
+
...(customValues.priority !== undefined
|
|
508
|
+
? { priority: typeof customValues.priority === 'number' ? customValues.priority : null }
|
|
509
|
+
: {}),
|
|
510
|
+
...(customValues.description !== undefined
|
|
511
|
+
? { body: typeof customValues.description === 'string' ? customValues.description : null }
|
|
512
|
+
: {}),
|
|
513
|
+
...(customValues.due_at !== undefined || customValues.dueAt !== undefined
|
|
514
|
+
? {
|
|
515
|
+
scheduledAt:
|
|
516
|
+
typeof customValues.due_at === 'string'
|
|
517
|
+
? customValues.due_at
|
|
518
|
+
: typeof customValues.dueAt === 'string'
|
|
519
|
+
? customValues.dueAt
|
|
520
|
+
: null,
|
|
521
|
+
}
|
|
522
|
+
: {}),
|
|
515
523
|
} : {}),
|
|
516
524
|
},
|
|
517
525
|
ctx: commandContext,
|
|
@@ -163,9 +163,9 @@ function buildLegacyTaskCustomValues(
|
|
|
163
163
|
custom[key] = value
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
assignValue('priority', payload.base.priority
|
|
167
|
-
assignValue('description', payload.base.description
|
|
168
|
-
assignValue('due_at', payload.base.scheduledAt
|
|
166
|
+
assignValue('priority', payload.base.priority)
|
|
167
|
+
assignValue('description', payload.base.description)
|
|
168
|
+
assignValue('due_at', payload.base.scheduledAt)
|
|
169
169
|
|
|
170
170
|
return custom
|
|
171
171
|
}
|
|
@@ -27,6 +27,7 @@ import { StaffTimeReport, StaffTimeReportEvent } from '../../../../../data/entit
|
|
|
27
27
|
import { loadReportProjectIds } from '../../../../../commands/timesheets-reports'
|
|
28
28
|
import { buildReportSheet } from '../../../../../lib/timesheets-reports/buildReportSheet'
|
|
29
29
|
import { buildReportRows } from '../../../../../lib/timesheets-reports/reportRows'
|
|
30
|
+
import type { ReportGroup } from '../../../../../lib/timesheets-reports/reportTotals'
|
|
30
31
|
import { formatReportPeriodDate } from '../../../../../lib/timesheets-reports/reportPeriod'
|
|
31
32
|
import {
|
|
32
33
|
normalizeReportExportFormat,
|
|
@@ -127,6 +128,17 @@ function readCustomerName(snapshot: Record<string, unknown> | null | undefined,
|
|
|
127
128
|
return fallback
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
function stripAmounts(groups: readonly ReportGroup[]): ReportGroup[] {
|
|
132
|
+
return groups.map((group) => ({
|
|
133
|
+
...group,
|
|
134
|
+
rate: null,
|
|
135
|
+
amount: 0,
|
|
136
|
+
lines: group.lines.map(function strip(line): ReportGroup['lines'][number] {
|
|
137
|
+
return { ...line, rate: null, amount: 0, children: line.children.map(strip) }
|
|
138
|
+
}),
|
|
139
|
+
}))
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
export async function GET(req: Request) {
|
|
131
143
|
try {
|
|
132
144
|
const context = await resolveReportRequestContext(req, { segment: 'export' })
|
|
@@ -199,8 +211,9 @@ export async function GET(req: Request) {
|
|
|
199
211
|
issuedByLabel: null,
|
|
200
212
|
issuedAtLabel: report.closedAt ? report.closedAt.toISOString().slice(0, 10) : null,
|
|
201
213
|
currencyCode: sheet.currencyCode,
|
|
214
|
+
canSeeMoney,
|
|
202
215
|
showRates: (report.showRates ?? true) && canSeeMoney,
|
|
203
|
-
groups: sheet.totals.groups,
|
|
216
|
+
groups: canSeeMoney ? sheet.totals.groups : stripAmounts(sheet.totals.groups),
|
|
204
217
|
rows: canSeeMoney ? rows : rows.map((row) => ({ ...row, rate: null, amount: null })),
|
|
205
218
|
totals: {
|
|
206
219
|
billableMinutes: sheet.totals.billableMinutes,
|
|
@@ -76,6 +76,8 @@ export type ReportExportInput = {
|
|
|
76
76
|
issuedByLabel: string | null
|
|
77
77
|
issuedAtLabel: string | null
|
|
78
78
|
currencyCode: string | null
|
|
79
|
+
/** Omission preserves legacy visibility; the export route always supplies its permission decision. */
|
|
80
|
+
canSeeMoney?: boolean
|
|
79
81
|
showRates: boolean
|
|
80
82
|
groups: readonly ReportGroup[]
|
|
81
83
|
rows: readonly ReportRow[]
|
|
@@ -107,6 +109,7 @@ function pdfLinesForLine(
|
|
|
107
109
|
depth: number,
|
|
108
110
|
isNonBillable: boolean,
|
|
109
111
|
showRates: boolean,
|
|
112
|
+
canSeeMoney: boolean,
|
|
110
113
|
currencyCode: string | null,
|
|
111
114
|
labels: ReportExportLabels,
|
|
112
115
|
): PdfLine[] {
|
|
@@ -122,16 +125,18 @@ function pdfLinesForLine(
|
|
|
122
125
|
align: 'right' as const,
|
|
123
126
|
})
|
|
124
127
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
if (canSeeMoney) {
|
|
129
|
+
cells.push({
|
|
130
|
+
text: isNonBillable ? '—' : formatAmount(line.amount, null),
|
|
131
|
+
x: COLUMN_AMOUNT_X,
|
|
132
|
+
align: 'right' as const,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
130
135
|
|
|
131
136
|
return [
|
|
132
137
|
{ kind: 'cells', cells },
|
|
133
138
|
...line.children.flatMap((child) =>
|
|
134
|
-
pdfLinesForLine(child, depth + 1, isNonBillable, showRates, currencyCode, labels),
|
|
139
|
+
pdfLinesForLine(child, depth + 1, isNonBillable, showRates, canSeeMoney, currencyCode, labels),
|
|
135
140
|
),
|
|
136
141
|
]
|
|
137
142
|
}
|
|
@@ -139,6 +144,8 @@ function pdfLinesForLine(
|
|
|
139
144
|
/** The PDF mirrors the on-screen sheet, group for group and line for line. */
|
|
140
145
|
export function buildReportPdfLines(input: ReportExportInput): PdfLine[] {
|
|
141
146
|
const { labels } = input
|
|
147
|
+
const canSeeMoney = input.canSeeMoney !== false
|
|
148
|
+
const showRates = input.showRates && canSeeMoney
|
|
142
149
|
const lines: PdfLine[] = [
|
|
143
150
|
{
|
|
144
151
|
kind: 'cells',
|
|
@@ -180,15 +187,15 @@ export function buildReportPdfLines(input: ReportExportInput): PdfLine[] {
|
|
|
180
187
|
cells: [
|
|
181
188
|
{ text: group.label, x: COLUMN_LABEL_X, bold: true },
|
|
182
189
|
{ text: formatReportMinutes(group.minutes), x: COLUMN_TIME_X, align: 'right', bold: true },
|
|
183
|
-
{
|
|
190
|
+
...(canSeeMoney ? [{
|
|
184
191
|
text: formatAmount(isNonBillable ? 0 : group.amount, input.currencyCode),
|
|
185
192
|
x: COLUMN_AMOUNT_X,
|
|
186
|
-
align: 'right',
|
|
193
|
+
align: 'right' as const,
|
|
187
194
|
bold: true,
|
|
188
|
-
},
|
|
195
|
+
}] : []),
|
|
189
196
|
],
|
|
190
197
|
})
|
|
191
|
-
if (
|
|
198
|
+
if (showRates && !isNonBillable && group.rate !== null) {
|
|
192
199
|
lines.push({
|
|
193
200
|
kind: 'cells',
|
|
194
201
|
cells: [{ text: `${formatAmount(group.rate, input.currencyCode)}/h`, x: COLUMN_LABEL_X, muted: true, size: 8 }],
|
|
@@ -199,22 +206,24 @@ export function buildReportPdfLines(input: ReportExportInput): PdfLine[] {
|
|
|
199
206
|
{ text: labels.line, x: COLUMN_LABEL_X, size: 8, muted: true },
|
|
200
207
|
{ text: labels.time, x: COLUMN_TIME_X, align: 'right' as const, size: 8, muted: true },
|
|
201
208
|
]
|
|
202
|
-
if (
|
|
209
|
+
if (showRates) {
|
|
203
210
|
header.push({ text: labels.rate, x: COLUMN_RATE_X, align: 'right' as const, size: 8, muted: true })
|
|
204
211
|
}
|
|
205
|
-
|
|
212
|
+
if (canSeeMoney) {
|
|
213
|
+
header.push({ text: labels.amount, x: COLUMN_AMOUNT_X, align: 'right' as const, size: 8, muted: true })
|
|
214
|
+
}
|
|
206
215
|
lines.push({ kind: 'cells', cells: header })
|
|
207
216
|
|
|
208
217
|
for (const line of group.lines) {
|
|
209
218
|
lines.push(
|
|
210
|
-
...pdfLinesForLine(line, 0, isNonBillable,
|
|
219
|
+
...pdfLinesForLine(line, 0, isNonBillable, showRates, canSeeMoney, input.currencyCode, labels),
|
|
211
220
|
)
|
|
212
221
|
}
|
|
213
222
|
lines.push({ kind: 'space', height: 8 }, { kind: 'rule' })
|
|
214
223
|
}
|
|
215
224
|
|
|
216
|
-
|
|
217
|
-
{
|
|
225
|
+
if (canSeeMoney) {
|
|
226
|
+
lines.push({
|
|
218
227
|
kind: 'cells',
|
|
219
228
|
cells: [
|
|
220
229
|
{ text: labels.total, x: COLUMN_LABEL_X, bold: true, size: 11 },
|
|
@@ -226,7 +235,9 @@ export function buildReportPdfLines(input: ReportExportInput): PdfLine[] {
|
|
|
226
235
|
size: 11,
|
|
227
236
|
},
|
|
228
237
|
],
|
|
229
|
-
}
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
lines.push(
|
|
230
241
|
{
|
|
231
242
|
kind: 'cells',
|
|
232
243
|
cells: [
|
|
@@ -253,6 +264,8 @@ export function buildReportPdfLines(input: ReportExportInput): PdfLine[] {
|
|
|
253
264
|
*/
|
|
254
265
|
export function buildReportTable(input: ReportExportInput): PreparedExport {
|
|
255
266
|
const { labels } = input
|
|
267
|
+
const canSeeMoney = input.canSeeMoney !== false
|
|
268
|
+
const showRates = input.showRates && canSeeMoney
|
|
256
269
|
const columns = [
|
|
257
270
|
{ field: 'date', header: labels.date },
|
|
258
271
|
{ field: 'project', header: labels.project },
|
|
@@ -263,8 +276,8 @@ export function buildReportTable(input: ReportExportInput): PreparedExport {
|
|
|
263
276
|
{ field: 'roundedMinutes', header: labels.roundedMinutes },
|
|
264
277
|
{ field: 'hours', header: labels.time },
|
|
265
278
|
{ field: 'billable', header: labels.billable },
|
|
266
|
-
...(
|
|
267
|
-
{ field: 'amount', header: labels.amount },
|
|
279
|
+
...(showRates ? [{ field: 'rate', header: labels.rate }] : []),
|
|
280
|
+
...(canSeeMoney ? [{ field: 'amount', header: labels.amount }] : []),
|
|
268
281
|
]
|
|
269
282
|
|
|
270
283
|
const rows = input.rows.map((row) => {
|
|
@@ -278,9 +291,9 @@ export function buildReportTable(input: ReportExportInput): PreparedExport {
|
|
|
278
291
|
roundedMinutes: row.minutes,
|
|
279
292
|
hours: row.hours,
|
|
280
293
|
billable: row.isBillable ? labels.yes : labels.no,
|
|
281
|
-
amount: row.amount === null ? '' : row.amount.toFixed(2),
|
|
282
294
|
}
|
|
283
|
-
if (
|
|
295
|
+
if (canSeeMoney) record.amount = row.amount === null ? '' : row.amount.toFixed(2)
|
|
296
|
+
if (showRates) record.rate = row.rate === null ? '' : row.rate.toFixed(2)
|
|
284
297
|
return record
|
|
285
298
|
})
|
|
286
299
|
|