@open-mercato/core 0.10.1-develop.7857.1.259928eb85 → 0.10.1-develop.7862.1.ee520f7662
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/audit_logs/api/audit-logs/actions/redo/route.js +5 -4
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +3 -3
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js +1 -4
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js.map +2 -2
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js +8 -1
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +18 -3
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +3 -1
- package/src/modules/audit_logs/i18n/de.json +1 -0
- package/src/modules/audit_logs/i18n/en.json +1 -0
- package/src/modules/audit_logs/i18n/es.json +1 -0
- package/src/modules/audit_logs/i18n/ko.json +1 -0
- package/src/modules/audit_logs/i18n/pl.json +1 -0
- package/src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx +0 -3
- package/src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx +8 -1
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +22 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -4,10 +4,10 @@ import { resolveSingleOrganizationIdOrDeny } from "@open-mercato/shared/lib/auth
|
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
6
|
import { isCrudHttpError, translateCrudErrorBody } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
-
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
7
|
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
9
8
|
import { z } from "zod";
|
|
10
9
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
10
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
11
11
|
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
12
12
|
import { authorizeAuditReplayWithEntityManager } from "@open-mercato/core/modules/audit_logs/lib/replayAuthorization";
|
|
13
13
|
import {
|
|
@@ -151,15 +151,16 @@ async function POST(req) {
|
|
|
151
151
|
} catch (err) {
|
|
152
152
|
if (isCrudHttpError(err)) {
|
|
153
153
|
logger.warn("Redo rejected", { err });
|
|
154
|
-
const { translate } = await resolveTranslations();
|
|
155
|
-
return NextResponse.json(translateCrudErrorBody(err.body,
|
|
154
|
+
const { translate: translate2 } = await resolveTranslations();
|
|
155
|
+
return NextResponse.json(translateCrudErrorBody(err.body, translate2), { status: err.status });
|
|
156
156
|
}
|
|
157
157
|
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
158
158
|
if (interceptorRejection) {
|
|
159
159
|
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
160
160
|
}
|
|
161
161
|
logger.error("Redo failed", { err });
|
|
162
|
-
|
|
162
|
+
const { translate } = await resolveTranslations();
|
|
163
|
+
return NextResponse.json({ error: translate("audit_logs.error.redo_failed", "Redo failed") }, { status: 400 });
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
async function createRuntimeContext(container, auth, request) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/audit_logs/api/audit-logs/actions/redo/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : singleOrganizationId\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n const logActorSubject = resolveActionLogActorSubject(log)\n const isLegacyApiKeyOwner = log.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && log.contextJson?.actorSubject === undefined\n if (log.actorUserId && !logActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && !actionLogBelongsToAuth(log, auth) && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant\n ? (logActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.redo_self',\n tenantFeature: 'audit_logs.redo_tenant',\n unavailableMessage: 'Redo target not available',\n })\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry, replaySourceFinalized } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n if (replaySourceFinalized !== true) await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n logger.warn('Redo rejected', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Redo failed', { err })\n return NextResponse.json({ error: 'Redo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA4C;AACrD,SAAS,yCAAyC;AAClD,SAAS,8BAA8B;AACvC,SAAS,4BAA4B,0CAA0C;AAG/E,SAAS,iBAAiB,8BAA8B;
|
|
6
|
-
"names": ["logEntry", "metadata"]
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError, translateCrudErrorBody } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'\nimport {\n actionLogBelongsToAuth,\n resolveCanonicalAuthSubject,\n resolveActionLogActorSubject,\n} from '@open-mercato/core/modules/audit_logs/lib/actorSubject'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const authSubject = resolveCanonicalAuthSubject(auth)\n if (!authSubject) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId, scope } = await resolveFeatureCheckContext({ container, auth, request: req })\n let singleOrganizationId: string | null\n try {\n singleOrganizationId = organizationId ?? resolveSingleOrganizationIdOrDeny(scope, auth)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : singleOrganizationId\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n const logActorSubject = resolveActionLogActorSubject(log)\n const isLegacyApiKeyOwner = log.actorUserId === authSubject.storageId\n && authSubject.kind === 'api_key'\n && log.contextJson?.actorSubject === undefined\n if (log.actorUserId && !logActorSubject && !isLegacyApiKeyOwner) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && !actionLogBelongsToAuth(log, auth) && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant\n ? (logActorSubject ?? (isLegacyApiKeyOwner ? authSubject.subject : auth.sub))\n : authSubject.subject\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (!rbac) return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n const replayRbac = rbac\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n ctx.replayTransactionGuard = ({ logEntry, transactionalEm }) =>\n authorizeAuditReplayWithEntityManager(transactionalEm, replayRbac, logEntry, {\n auth,\n organizationId,\n selfFeature: 'audit_logs.redo_self',\n tenantFeature: 'audit_logs.redo_tenant',\n unavailableMessage: 'Redo target not available',\n })\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry, replaySourceFinalized } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n if (replaySourceFinalized !== true) await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n logger.warn('Redo rejected', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json(translateCrudErrorBody(err.body, translate), { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Redo failed', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json({ error: translate('audit_logs.error.redo_failed', 'Redo failed') }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["logEntry", "metadata", "translate"]
|
|
7
7
|
}
|
|
@@ -125,10 +125,7 @@ const CustomerTodosWidget = ({
|
|
|
125
125
|
/* @__PURE__ */ jsx("span", { children: item.entity.displayName ?? t("customers.widgets.common.unknown") }),
|
|
126
126
|
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: createdLabel || t("customers.widgets.common.unknownDate") })
|
|
127
127
|
] }),
|
|
128
|
-
/* @__PURE__ */
|
|
129
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: item.todoTitle ?? t("customers.widgets.todos.untitled") }),
|
|
130
|
-
item.todoSource ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: item.todoSource }) : null
|
|
131
|
-
] }),
|
|
128
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1 space-y-0.5", children: /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: item.todoTitle ?? t("customers.widgets.todos.untitled") }) }),
|
|
132
129
|
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap gap-3 text-xs", children: [
|
|
133
130
|
href ? /* @__PURE__ */ jsx(Link, { className: "text-primary hover:underline", href, children: t("customers.widgets.common.viewRecord") }) : null,
|
|
134
131
|
taskHref ? /* @__PURE__ */ jsx(Link, { className: "text-primary hover:underline", href: taskHref, children: t("customers.workPlan.customerTodos.table.actions.openTask") }) : null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DEFAULT_SETTINGS, hydrateCustomerTodoSettings, type CustomerTodoWidgetSettings } from './config'\nimport { resolveExampleIntegrationHref } from '../../../lib/interactionCompatibility'\nimport { resolveTodoHref } from '../../../components/detail/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ntype TodoLinkSummary = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n createdAt: string\n _integrations?: {\n example?: {\n href?: string | null\n }\n [key: string]: unknown\n }\n entity: {\n id: string | null\n displayName: string | null\n kind: string | null\n }\n}\n\nasync function loadTodos(settings: CustomerTodoWidgetSettings): Promise<TodoLinkSummary[]> {\n const params = new URLSearchParams({\n limit: String(settings.pageSize),\n })\n const call = await apiCall<{ items?: unknown[]; error?: string }>(\n `/api/customers/dashboard/widgets/customer-todos?${params.toString()}`,\n )\n if (!call.ok) {\n const message =\n typeof (call.result as Record<string, unknown> | null)?.error === 'string'\n ? ((call.result as Record<string, unknown>).error as string)\n : `Request failed with status ${call.status}`\n throw new Error(message)\n }\n const payload = call.result ?? {}\n const rawItems = Array.isArray((payload as { items?: unknown }).items)\n ? ((payload as { items: unknown[] }).items)\n : []\n return rawItems\n .map((item): TodoLinkSummary | null => {\n if (!item || typeof item !== 'object') return null\n const data = item as any\n const entity = data.entity ?? {}\n return {\n id: typeof data.id === 'string' ? data.id : null,\n todoId: typeof data.todoId === 'string' ? data.todoId : '',\n todoSource: typeof data.todoSource === 'string' ? data.todoSource : '',\n todoTitle: typeof data.todoTitle === 'string' ? data.todoTitle : null,\n createdAt: typeof data.createdAt === 'string' ? data.createdAt : '',\n _integrations: data._integrations && typeof data._integrations === 'object'\n ? (data._integrations as TodoLinkSummary['_integrations'])\n : undefined,\n entity: {\n id: typeof entity.id === 'string' ? entity.id : null,\n displayName: typeof entity.displayName === 'string' ? entity.displayName : null,\n kind: typeof entity.kind === 'string' ? entity.kind : null,\n },\n }\n })\n .filter((item): item is TodoLinkSummary => !!item && !!item.id)\n}\n\nfunction formatDate(value: string | null, locale?: string): string {\n if (!value) return ''\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return ''\n return date.toLocaleString(locale ?? undefined)\n}\n\nfunction resolveDetailHref(entity: { id: string | null; kind: string | null } | null | undefined): string | null {\n if (!entity?.id) return null\n if (entity.kind === 'company') return `/backend/customers/companies-v2/${encodeURIComponent(entity.id)}`\n if (entity.kind === 'person') return `/backend/customers/people-v2/${encodeURIComponent(entity.id)}`\n return `/backend/customers/people-v2/${encodeURIComponent(entity.id)}`\n}\n\nconst CustomerTodosWidget: React.FC<DashboardWidgetComponentProps<CustomerTodoWidgetSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateCustomerTodoSettings(settings), [settings])\n const [items, setItems] = React.useState<TodoLinkSummary[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [locale, setLocale] = React.useState<string | undefined>(undefined)\n\n React.useEffect(() => {\n if (typeof navigator !== 'undefined') {\n setLocale(navigator.language)\n }\n }, [])\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await loadTodos(hydrated)\n setItems(data)\n } catch (err) {\n logger.error('Failed to load customer todos widget data', { err })\n setError(t('customers.widgets.todos.error'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <div className=\"space-y-1.5\">\n <label htmlFor=\"customer-todos-page-size\" className=\"text-xs font-semibold uppercase text-muted-foreground\">\n {t('customers.widgets.todos.settings.pageSize')}\n </label>\n <Input\n id=\"customer-todos-page-size\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24\"\n value={hydrated.pageSize}\n onChange={(event) => {\n const next = Number(event.target.value)\n onSettingsChange({ ...hydrated, pageSize: Number.isFinite(next) ? next : hydrated.pageSize })\n }}\n />\n </div>\n <p className=\"text-xs text-muted-foreground\">{t('customers.widgets.todos.settings.help')}</p>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-4\">\n {error ? (\n <p className=\"text-sm text-destructive\">{error}</p>\n ) : loading ? (\n <div className=\"flex h-32 items-center justify-center\">\n <Spinner className=\"h-6 w-6 text-muted-foreground\" />\n </div>\n ) : items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">{t('customers.widgets.todos.empty')}</p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => {\n const createdLabel = formatDate(item.createdAt, locale)\n const href = resolveDetailHref(item.entity)\n const exampleHref = resolveExampleIntegrationHref(item)\n const taskHref = exampleHref ?? resolveTodoHref(item.todoSource, item.todoId)\n return (\n <li key={item.id} className=\"rounded-md border p-3\">\n <div className=\"flex items-start justify-between gap-3 text-sm font-medium\">\n <span>{item.entity.displayName ?? t('customers.widgets.common.unknown')}</span>\n <span className=\"text-xs text-muted-foreground\">{createdLabel || t('customers.widgets.common.unknownDate')}</span>\n </div>\n <div className=\"mt-1 space-y-0.5\">\n <p className=\"text-sm font-medium text-foreground\">\n {item.todoTitle ?? t('customers.widgets.todos.untitled')}\n </p>\n
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DEFAULT_SETTINGS, hydrateCustomerTodoSettings, type CustomerTodoWidgetSettings } from './config'\nimport { resolveExampleIntegrationHref } from '../../../lib/interactionCompatibility'\nimport { resolveTodoHref } from '../../../components/detail/utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ntype TodoLinkSummary = {\n id: string\n todoId: string\n todoSource: string\n todoTitle: string | null\n createdAt: string\n _integrations?: {\n example?: {\n href?: string | null\n }\n [key: string]: unknown\n }\n entity: {\n id: string | null\n displayName: string | null\n kind: string | null\n }\n}\n\nasync function loadTodos(settings: CustomerTodoWidgetSettings): Promise<TodoLinkSummary[]> {\n const params = new URLSearchParams({\n limit: String(settings.pageSize),\n })\n const call = await apiCall<{ items?: unknown[]; error?: string }>(\n `/api/customers/dashboard/widgets/customer-todos?${params.toString()}`,\n )\n if (!call.ok) {\n const message =\n typeof (call.result as Record<string, unknown> | null)?.error === 'string'\n ? ((call.result as Record<string, unknown>).error as string)\n : `Request failed with status ${call.status}`\n throw new Error(message)\n }\n const payload = call.result ?? {}\n const rawItems = Array.isArray((payload as { items?: unknown }).items)\n ? ((payload as { items: unknown[] }).items)\n : []\n return rawItems\n .map((item): TodoLinkSummary | null => {\n if (!item || typeof item !== 'object') return null\n const data = item as any\n const entity = data.entity ?? {}\n return {\n id: typeof data.id === 'string' ? data.id : null,\n todoId: typeof data.todoId === 'string' ? data.todoId : '',\n todoSource: typeof data.todoSource === 'string' ? data.todoSource : '',\n todoTitle: typeof data.todoTitle === 'string' ? data.todoTitle : null,\n createdAt: typeof data.createdAt === 'string' ? data.createdAt : '',\n _integrations: data._integrations && typeof data._integrations === 'object'\n ? (data._integrations as TodoLinkSummary['_integrations'])\n : undefined,\n entity: {\n id: typeof entity.id === 'string' ? entity.id : null,\n displayName: typeof entity.displayName === 'string' ? entity.displayName : null,\n kind: typeof entity.kind === 'string' ? entity.kind : null,\n },\n }\n })\n .filter((item): item is TodoLinkSummary => !!item && !!item.id)\n}\n\nfunction formatDate(value: string | null, locale?: string): string {\n if (!value) return ''\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return ''\n return date.toLocaleString(locale ?? undefined)\n}\n\nfunction resolveDetailHref(entity: { id: string | null; kind: string | null } | null | undefined): string | null {\n if (!entity?.id) return null\n if (entity.kind === 'company') return `/backend/customers/companies-v2/${encodeURIComponent(entity.id)}`\n if (entity.kind === 'person') return `/backend/customers/people-v2/${encodeURIComponent(entity.id)}`\n return `/backend/customers/people-v2/${encodeURIComponent(entity.id)}`\n}\n\nconst CustomerTodosWidget: React.FC<DashboardWidgetComponentProps<CustomerTodoWidgetSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateCustomerTodoSettings(settings), [settings])\n const [items, setItems] = React.useState<TodoLinkSummary[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [locale, setLocale] = React.useState<string | undefined>(undefined)\n\n React.useEffect(() => {\n if (typeof navigator !== 'undefined') {\n setLocale(navigator.language)\n }\n }, [])\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await loadTodos(hydrated)\n setItems(data)\n } catch (err) {\n logger.error('Failed to load customer todos widget data', { err })\n setError(t('customers.widgets.todos.error'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <div className=\"space-y-1.5\">\n <label htmlFor=\"customer-todos-page-size\" className=\"text-xs font-semibold uppercase text-muted-foreground\">\n {t('customers.widgets.todos.settings.pageSize')}\n </label>\n <Input\n id=\"customer-todos-page-size\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24\"\n value={hydrated.pageSize}\n onChange={(event) => {\n const next = Number(event.target.value)\n onSettingsChange({ ...hydrated, pageSize: Number.isFinite(next) ? next : hydrated.pageSize })\n }}\n />\n </div>\n <p className=\"text-xs text-muted-foreground\">{t('customers.widgets.todos.settings.help')}</p>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-4\">\n {error ? (\n <p className=\"text-sm text-destructive\">{error}</p>\n ) : loading ? (\n <div className=\"flex h-32 items-center justify-center\">\n <Spinner className=\"h-6 w-6 text-muted-foreground\" />\n </div>\n ) : items.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">{t('customers.widgets.todos.empty')}</p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => {\n const createdLabel = formatDate(item.createdAt, locale)\n const href = resolveDetailHref(item.entity)\n const exampleHref = resolveExampleIntegrationHref(item)\n const taskHref = exampleHref ?? resolveTodoHref(item.todoSource, item.todoId)\n return (\n <li key={item.id} className=\"rounded-md border p-3\">\n <div className=\"flex items-start justify-between gap-3 text-sm font-medium\">\n <span>{item.entity.displayName ?? t('customers.widgets.common.unknown')}</span>\n <span className=\"text-xs text-muted-foreground\">{createdLabel || t('customers.widgets.common.unknownDate')}</span>\n </div>\n <div className=\"mt-1 space-y-0.5\">\n <p className=\"text-sm font-medium text-foreground\">\n {item.todoTitle ?? t('customers.widgets.todos.untitled')}\n </p>\n </div>\n <div className=\"mt-2 flex flex-wrap gap-3 text-xs\">\n {href ? (\n <Link className=\"text-primary hover:underline\" href={href}>\n {t('customers.widgets.common.viewRecord')}\n </Link>\n ) : null}\n {taskHref ? (\n <Link className=\"text-primary hover:underline\" href={taskHref}>\n {t('customers.workPlan.customerTodos.table.actions.openTask')}\n </Link>\n ) : null}\n </div>\n </li>\n )\n })}\n </ul>\n )}\n </div>\n )\n}\n\nexport default CustomerTodosWidget\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
3
4
|
import { Tabs, TabsList, TabsTrigger } from "@open-mercato/ui/primitives/tabs";
|
|
4
5
|
function WarrantyWorkspace({
|
|
5
6
|
title,
|
|
@@ -32,7 +33,13 @@ function WarrantyWorkspace({
|
|
|
32
33
|
},
|
|
33
34
|
tab.id
|
|
34
35
|
)) }) }) : null,
|
|
35
|
-
/* @__PURE__ */ jsx("div", { className:
|
|
36
|
+
/* @__PURE__ */ jsx("div", { className: cn(
|
|
37
|
+
"min-w-0 [&>[data-component-handle]>div:first-child_[data-slot=button]]:h-8 [&>[data-component-handle]>div:first-child_[data-slot=search-input-wrapper]]:h-8 [&_[data-slot=table-header]_[data-slot=table-row]]:h-9 [&_[data-slot=table-body]_[data-slot=table-row]]:h-16",
|
|
38
|
+
"md:[&_[data-slot=table-head][class*=sticky]]:bg-card md:[&_[data-slot=table-cell][class*=sticky]]:bg-card",
|
|
39
|
+
"md:[&_[data-slot=table-head][class*=sticky]]:bg-linear-to-r md:[&_[data-slot=table-head][class*=sticky]]:from-muted/40 md:[&_[data-slot=table-head][class*=sticky]]:to-muted/40",
|
|
40
|
+
"md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:bg-linear-to-r md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:from-muted/30 md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:to-muted/30",
|
|
41
|
+
contentClassName
|
|
42
|
+
), children })
|
|
36
43
|
] });
|
|
37
44
|
}
|
|
38
45
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\n\nexport type WarrantyWorkspaceTab = {\n id: string\n label: string\n count?: number\n disabled?: boolean\n}\n\ntype WarrantyWorkspaceProps = {\n title: string\n description?: string\n action?: React.ReactNode\n tabs?: WarrantyWorkspaceTab[]\n activeTab?: string\n onTabChange?: (value: string) => void\n summary?: React.ReactNode\n contentClassName?: string\n children: React.ReactNode\n}\n\nexport function WarrantyWorkspace({\n title,\n description,\n action,\n tabs,\n activeTab,\n onTabChange,\n summary,\n contentClassName,\n children,\n}: WarrantyWorkspaceProps) {\n return (\n <section className=\"overflow-hidden rounded-xl border border-border bg-card\">\n <div className=\"flex flex-col gap-4 px-8 pb-6 pt-7 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold tracking-tight text-foreground\">{title}</h1>\n {description ? <p className=\"mt-1 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n </div>\n {summary ?? null}\n {tabs?.length && activeTab && onTabChange ? (\n <Tabs value={activeTab} onValueChange={onTabChange} variant=\"underline\">\n <TabsList className=\"flex h-auto w-full gap-1 overflow-x-auto px-7 pb-2\" aria-label={title}>\n {tabs.map((tab) => (\n <TabsTrigger\n key={tab.id}\n value={tab.id}\n count={tab.count}\n disabled={tab.disabled}\n className=\"shrink-0 gap-2 px-4 pb-2 pt-3 [&_[data-slot=tabs-trigger-count]]:h-auto [&_[data-slot=tabs-trigger-count]]:min-w-0 [&_[data-slot=tabs-trigger-count]]:rounded-md [&_[data-slot=tabs-trigger-count]]:bg-muted [&_[data-slot=tabs-trigger-count]]:px-1.5 [&_[data-slot=tabs-trigger-count]]:py-0.5 [&_[data-slot=tabs-trigger-count]]:text-overline [&_[data-slot=tabs-trigger-count]]:text-muted-foreground\"\n >\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n ) : null}\n <div className={
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\n\nexport type WarrantyWorkspaceTab = {\n id: string\n label: string\n count?: number\n disabled?: boolean\n}\n\ntype WarrantyWorkspaceProps = {\n title: string\n description?: string\n action?: React.ReactNode\n tabs?: WarrantyWorkspaceTab[]\n activeTab?: string\n onTabChange?: (value: string) => void\n summary?: React.ReactNode\n contentClassName?: string\n children: React.ReactNode\n}\n\nexport function WarrantyWorkspace({\n title,\n description,\n action,\n tabs,\n activeTab,\n onTabChange,\n summary,\n contentClassName,\n children,\n}: WarrantyWorkspaceProps) {\n return (\n <section className=\"overflow-hidden rounded-xl border border-border bg-card\">\n <div className=\"flex flex-col gap-4 px-8 pb-6 pt-7 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold tracking-tight text-foreground\">{title}</h1>\n {description ? <p className=\"mt-1 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n </div>\n {summary ?? null}\n {tabs?.length && activeTab && onTabChange ? (\n <Tabs value={activeTab} onValueChange={onTabChange} variant=\"underline\">\n <TabsList className=\"flex h-auto w-full gap-1 overflow-x-auto px-7 pb-2\" aria-label={title}>\n {tabs.map((tab) => (\n <TabsTrigger\n key={tab.id}\n value={tab.id}\n count={tab.count}\n disabled={tab.disabled}\n className=\"shrink-0 gap-2 px-4 pb-2 pt-3 [&_[data-slot=tabs-trigger-count]]:h-auto [&_[data-slot=tabs-trigger-count]]:min-w-0 [&_[data-slot=tabs-trigger-count]]:rounded-md [&_[data-slot=tabs-trigger-count]]:bg-muted [&_[data-slot=tabs-trigger-count]]:px-1.5 [&_[data-slot=tabs-trigger-count]]:py-0.5 [&_[data-slot=tabs-trigger-count]]:text-overline [&_[data-slot=tabs-trigger-count]]:text-muted-foreground\"\n >\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n ) : null}\n <div className={cn(\n 'min-w-0 [&>[data-component-handle]>div:first-child_[data-slot=button]]:h-8 [&>[data-component-handle]>div:first-child_[data-slot=search-input-wrapper]]:h-8 [&_[data-slot=table-header]_[data-slot=table-row]]:h-9 [&_[data-slot=table-body]_[data-slot=table-row]]:h-16',\n 'md:[&_[data-slot=table-head][class*=sticky]]:bg-card md:[&_[data-slot=table-cell][class*=sticky]]:bg-card',\n 'md:[&_[data-slot=table-head][class*=sticky]]:bg-linear-to-r md:[&_[data-slot=table-head][class*=sticky]]:from-muted/40 md:[&_[data-slot=table-head][class*=sticky]]:to-muted/40',\n 'md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:bg-linear-to-r md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:from-muted/30 md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:to-muted/30',\n contentClassName,\n )}>{children}</div>\n </section>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAuCQ,SACE,KADF;AApCR,SAAS,UAAU;AACnB,SAAS,MAAM,UAAU,mBAAmB;AAqBrC,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AACzB,SACE,qBAAC,aAAQ,WAAU,2DACjB;AAAA,yBAAC,SAAI,WAAU,qFACb;AAAA,2BAAC,SAAI,WAAU,WACb;AAAA,4BAAC,QAAG,WAAU,qDAAqD,iBAAM;AAAA,QACxE,cAAc,oBAAC,OAAE,WAAU,sCAAsC,uBAAY,IAAO;AAAA,SACvF;AAAA,MACC,SAAS,oBAAC,SAAI,WAAU,YAAY,kBAAO,IAAS;AAAA,OACvD;AAAA,IACC,WAAW;AAAA,IACX,MAAM,UAAU,aAAa,cAC5B,oBAAC,QAAK,OAAO,WAAW,eAAe,aAAa,SAAQ,aAC1D,8BAAC,YAAS,WAAU,sDAAqD,cAAY,OAClF,eAAK,IAAI,CAAC,QACT;AAAA,MAAC;AAAA;AAAA,QAEC,OAAO,IAAI;AAAA,QACX,OAAO,IAAI;AAAA,QACX,UAAU,IAAI;AAAA,QACd,WAAU;AAAA,QAET,cAAI;AAAA;AAAA,MANA,IAAI;AAAA,IAOX,CACD,GACH,GACF,IACE;AAAA,IACJ,oBAAC,SAAI,WAAW;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,GAAI,UAAS;AAAA,KACf;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -144,10 +144,12 @@ function buildTrackerSteps(claim, t, locale) {
|
|
|
144
144
|
};
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
function buildClaimTitle(claim, t) {
|
|
147
|
+
function buildClaimTitle(claim, reasonOptions, t) {
|
|
148
148
|
const typeLabel = t(`warranty_claims.claimType.${claim.claimType}`);
|
|
149
149
|
if (claim.reasonCode) {
|
|
150
|
-
const
|
|
150
|
+
const reasonCode = claim.reasonCode;
|
|
151
|
+
const dictionaryLabel = reasonOptions.find((option) => option.value === reasonCode)?.label ?? reasonCode;
|
|
152
|
+
const reasonLabel = localizeDictionaryLabel(t, "reason", reasonCode, dictionaryLabel);
|
|
151
153
|
return t("warranty_claims.portal.tracker.titleWithReason", { reason: reasonLabel, type: typeLabel });
|
|
152
154
|
}
|
|
153
155
|
return t("warranty_claims.portal.tracker.titleFallback", { type: typeLabel });
|
|
@@ -221,6 +223,7 @@ function WarrantyClaimPortalDetailPage({ params }) {
|
|
|
221
223
|
const [claim, setClaim] = React.useState(null);
|
|
222
224
|
const [events, setEvents] = React.useState([]);
|
|
223
225
|
const [attachments, setAttachments] = React.useState([]);
|
|
226
|
+
const [reasonOptions, setReasonOptions] = React.useState([]);
|
|
224
227
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
225
228
|
const [error, setError] = React.useState(null);
|
|
226
229
|
const [notFound, setNotFound] = React.useState(false);
|
|
@@ -275,6 +278,18 @@ function WarrantyClaimPortalDetailPage({ params }) {
|
|
|
275
278
|
React.useEffect(() => {
|
|
276
279
|
if (user) void loadClaim();
|
|
277
280
|
}, [loadClaim, user]);
|
|
281
|
+
React.useEffect(() => {
|
|
282
|
+
if (!user) return;
|
|
283
|
+
let cancelled = false;
|
|
284
|
+
apiCall("/api/warranty_claims/portal/options").then((result) => {
|
|
285
|
+
if (cancelled) return;
|
|
286
|
+
setReasonOptions(result.result?.ok ? result.result.result?.reasons ?? [] : []);
|
|
287
|
+
}).catch(() => {
|
|
288
|
+
});
|
|
289
|
+
return () => {
|
|
290
|
+
cancelled = true;
|
|
291
|
+
};
|
|
292
|
+
}, [user]);
|
|
278
293
|
const submitComment = React.useCallback(async () => {
|
|
279
294
|
if (!claim || commentSubmitting) return;
|
|
280
295
|
const body = comment.trim();
|
|
@@ -580,7 +595,7 @@ function WarrantyClaimPortalDetailPage({ params }) {
|
|
|
580
595
|
}
|
|
581
596
|
) : null
|
|
582
597
|
] }),
|
|
583
|
-
/* @__PURE__ */ jsx("h1", { className: "text-xl font-bold text-foreground", children: buildClaimTitle(claim, t) }),
|
|
598
|
+
/* @__PURE__ */ jsx("h1", { className: "text-xl font-bold text-foreground", children: buildClaimTitle(claim, reasonOptions, t) }),
|
|
584
599
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: buildClaimSubtitle(claim, t, locale) })
|
|
585
600
|
] }),
|
|
586
601
|
/* @__PURE__ */ jsx("div", { className: "w-full px-6 pb-6", children: /* @__PURE__ */ jsxs("div", { className: `flex w-full items-start gap-3 rounded-lg px-4 py-3 ${toneClasses.wrapper}`, children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/warranty_claims/frontend/%5BorgSlug%5D/portal/claims/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ArrowLeft, Check, CircleAlert, Info, Paperclip, RefreshCw, ShieldCheck, Trash2, TriangleAlert } from 'lucide-react'\nimport { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\nimport { useLocale, useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { localizeDictionaryLabel } from '@open-mercato/core/modules/warranty_claims/lib/dictionaryLabels'\nimport { formatQuantity } from '@open-mercato/core/modules/warranty_claims/lib/quantity'\nimport {\n ATTACHMENT_ACCEPT_TYPES,\n validateAttachmentFile,\n} from '@open-mercato/core/modules/warranty_claims/lib/portalAttachmentValidation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'\n\ntype Props = { params: { orgSlug: string; id: string } }\n\ntype PortalClaimLine = {\n id: string\n lineNo: number\n sku: string | null\n productName: string | null\n serialNumber: string | null\n faultCode: string | null\n faultDescription: string | null\n qtyClaimed: string\n qtyApproved: string | null\n lineStatus: string\n disposition: string | null\n creditAmount: string | null\n}\n\ntype PortalClaim = {\n id: string\n claimNumber: string\n claimType: string\n status: string\n priority: string\n orderId: string | null\n orderNumber: string | null\n reasonCode: string | null\n rejectionReasonCode: string | null\n resolutionSummary: string | null\n submittedAt: string | null\n resolvedAt: string | null\n closedAt: string | null\n createdAt: string | null\n updatedAt: string | null\n lines: PortalClaimLine[]\n}\n\ntype PortalClaimEvent = {\n id: string\n kind: string\n body: string | null\n payload: Record<string, unknown> | null\n actorCustomerId: string | null\n createdAt: string | null\n}\n\ntype PortalAttachment = {\n id: string\n url: string\n downloadUrl: string\n fileName: string\n fileSize: number\n mimeType: string | null\n thumbnailUrl: string\n createdAt: string | null\n}\n\ntype ClaimResponse = { item: PortalClaim }\ntype EventResponse = { items: PortalClaimEvent[] }\ntype AttachmentResponse = { items: PortalAttachment[] }\ntype MutationOkResponse = { ok: boolean; error?: string }\ntype UploadResponse = MutationOkResponse & { item?: PortalAttachment }\ntype ClaimActionResponse = MutationOkResponse & { claimId?: string; status?: string }\n\ntype PortalClaimAction = 'submit' | 'withdraw'\n\ntype TrackerStepState = 'complete' | 'current' | 'pending'\n\ntype TrackerStep = {\n id: string\n title: string\n description: string\n state: TrackerStepState\n dateLabel: string | null\n}\n\ntype BannerTone = 'info' | 'success' | 'warning' | 'error'\n\ntype ActivityEntry = {\n id: string\n title: string\n description: string\n createdAt: string | null\n href?: string\n attachment?: PortalAttachment\n}\n\nconst TRACKER_STEP_KEYS = ['received', 'review', 'decision', 'return', 'resolved'] as const\n\nconst STATUS_STEP_INDEX: Record<string, number> = {\n draft: 0,\n submitted: 1,\n in_review: 1,\n info_requested: 1,\n rejected: 2,\n cancelled: 2,\n approved: 3,\n awaiting_return: 3,\n received: 4,\n inspecting: 4,\n resolved: 5,\n closed: 5,\n}\n\nconst LINE_STATUS_DOT_CLASSES: Record<string, string> = {\n pending: 'bg-status-neutral-icon',\n approved: 'bg-status-success-icon',\n rejected: 'bg-status-error-icon',\n received: 'bg-status-info-icon',\n inspected: 'bg-status-info-icon',\n resolved: 'bg-status-success-icon',\n}\n\nconst BANNER_TONE_CLASSES: Record<BannerTone, { wrapper: string; text: string; icon: string }> = {\n info: {\n wrapper: 'bg-status-info-bg',\n text: 'text-status-info-text',\n icon: 'text-status-info-icon',\n },\n success: {\n wrapper: 'bg-status-success-bg',\n text: 'text-status-success-text',\n icon: 'text-status-success-icon',\n },\n warning: {\n wrapper: 'bg-status-warning-bg',\n text: 'text-status-warning-text',\n icon: 'text-status-warning-icon',\n },\n error: {\n wrapper: 'bg-status-error-bg',\n text: 'text-status-error-text',\n icon: 'text-status-error-icon',\n },\n}\n\nfunction bannerTone(status: string): BannerTone {\n if (status === 'rejected' || status === 'cancelled') return 'error'\n if (status === 'info_requested') return 'warning'\n if (status === 'approved' || status === 'resolved' || status === 'closed') return 'success'\n return 'info'\n}\n\nfunction bannerMessage(claim: PortalClaim, t: TranslateFn): string {\n switch (claim.status) {\n case 'draft':\n return t('warranty_claims.portal.tracker.banner.draft')\n case 'info_requested':\n return t('warranty_claims.portal.tracker.banner.infoRequested')\n case 'approved':\n return t('warranty_claims.portal.tracker.banner.approved')\n case 'awaiting_return':\n return t('warranty_claims.portal.tracker.banner.awaitingReturn')\n case 'received':\n case 'inspecting':\n return t('warranty_claims.portal.tracker.banner.goodsFlow')\n case 'resolved':\n case 'closed':\n return claim.resolutionSummary ?? t('warranty_claims.portal.tracker.banner.resolved')\n case 'rejected': {\n const reason = claim.rejectionReasonCode\n ? localizeDictionaryLabel(t, 'rejection', claim.rejectionReasonCode, claim.rejectionReasonCode)\n : null\n const base = t('warranty_claims.portal.detail.terminal.rejected')\n const parts = [base, reason, claim.resolutionSummary].filter((part): part is string => Boolean(part))\n return parts.join(' ')\n }\n case 'cancelled':\n return t('warranty_claims.portal.detail.terminal.cancelled')\n default:\n return t('warranty_claims.portal.tracker.banner.review')\n }\n}\n\nfunction formatShortDate(value: string | null, locale: string): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return date.toLocaleDateString(locale || undefined, { month: 'short', day: 'numeric' })\n}\n\nfunction formatLongDate(value: string | null, locale: string): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return date.toLocaleDateString(locale || undefined, { year: 'numeric', month: 'short', day: 'numeric' })\n}\n\nfunction buildTrackerSteps(claim: PortalClaim, t: TranslateFn, locale: string): TrackerStep[] {\n const currentIndex = STATUS_STEP_INDEX[claim.status] ?? 1\n const terminalComplete = claim.status === 'resolved' || claim.status === 'closed'\n return TRACKER_STEP_KEYS.map((key, index) => {\n const state: TrackerStepState = index < currentIndex || (terminalComplete && index === currentIndex)\n ? 'complete'\n : index === currentIndex\n ? 'current'\n : 'pending'\n let dateLabel: string | null = null\n if (index === 0 && state === 'complete') dateLabel = formatShortDate(claim.submittedAt, locale)\n if (index === TRACKER_STEP_KEYS.length - 1 && state === 'complete') {\n dateLabel = formatShortDate(\n claim.status === 'closed'\n ? claim.closedAt ?? claim.resolvedAt\n : claim.resolvedAt ?? claim.closedAt,\n locale,\n )\n }\n if (state === 'current') dateLabel = t('warranty_claims.portal.tracker.step.now')\n return {\n id: key,\n title: t(`warranty_claims.portal.tracker.step.${key}.title`),\n description: t(`warranty_claims.portal.tracker.step.${key}.description`),\n state,\n dateLabel,\n }\n })\n}\n\nfunction buildClaimTitle(claim: PortalClaim, t: TranslateFn): string {\n const typeLabel = t(`warranty_claims.claimType.${claim.claimType}`)\n if (claim.reasonCode) {\n const reasonLabel = localizeDictionaryLabel(t, 'reason', claim.reasonCode, claim.reasonCode)\n return t('warranty_claims.portal.tracker.titleWithReason', { reason: reasonLabel, type: typeLabel })\n }\n return t('warranty_claims.portal.tracker.titleFallback', { type: typeLabel })\n}\n\nfunction buildClaimSubtitle(claim: PortalClaim, t: TranslateFn, locale: string): string {\n if (claim.status === 'draft') {\n const created = formatLongDate(claim.createdAt, locale)\n return created\n ? t('warranty_claims.portal.tracker.draftCreated', { date: created })\n : t('warranty_claims.portal.detail.notSubmitted')\n }\n const submitted = formatLongDate(claim.submittedAt, locale) ?? formatLongDate(claim.createdAt, locale)\n if (!submitted) return t('warranty_claims.portal.value.notAvailable')\n if (claim.orderNumber) {\n return t('warranty_claims.portal.tracker.submittedFromOrder', { date: submitted, order: claim.orderNumber })\n }\n return t('warranty_claims.portal.tracker.submittedOn', { date: submitted })\n}\n\nfunction formatEventBody(event: PortalClaimEvent, t: TranslateFn): string {\n if (event.body) return event.body\n const payload = event.payload\n const from = typeof payload?.from === 'string' ? payload.from : null\n const to = typeof payload?.to === 'string' ? payload.to : null\n if (from && to) {\n return t('warranty_claims.portal.event.transition', {\n from: t(`warranty_claims.status.${from}`),\n to: t(`warranty_claims.status.${to}`),\n })\n }\n return t('warranty_claims.portal.event.noDetails')\n}\n\nfunction eventTitle(event: PortalClaimEvent, t: TranslateFn): string {\n if (event.kind === 'comment') {\n return event.actorCustomerId\n ? t('warranty_claims.portal.tracker.event.youReplied')\n : t('warranty_claims.portal.tracker.event.teamReplied')\n }\n return t(`warranty_claims.portal.event.${event.kind}`)\n}\n\nfunction buildActivityEntries(\n events: PortalClaimEvent[],\n attachments: PortalAttachment[],\n t: TranslateFn,\n): ActivityEntry[] {\n const eventEntries: ActivityEntry[] = events.map((event) => ({\n id: `event-${event.id}`,\n title: eventTitle(event, t),\n description: formatEventBody(event, t),\n createdAt: event.createdAt,\n }))\n const attachmentEntries: ActivityEntry[] = attachments.map((attachment) => ({\n id: `attachment-${attachment.id}`,\n title: t('warranty_claims.portal.tracker.event.attachmentAdded'),\n description: attachment.fileName,\n createdAt: attachment.createdAt,\n href: attachment.downloadUrl,\n attachment,\n }))\n return [...eventEntries, ...attachmentEntries].sort((left, right) => {\n const leftTime = left.createdAt ? new Date(left.createdAt).getTime() : 0\n const rightTime = right.createdAt ? new Date(right.createdAt).getTime() : 0\n return rightTime - leftTime\n })\n}\n\nconst DETAIL_MUTATION_CONTEXT_ID = 'warranty_claims.portal.claim.detail'\n\nconst SECTION_CLASS = 'flex w-full flex-col gap-3 border-t border-border px-6 pb-6 pt-5'\nconst SECTION_HEADING_CLASS = 'text-sm font-semibold text-foreground'\nconst DARK_BUTTON_CLASS = 'rounded-md px-3 py-2 text-sm font-medium'\n\nexport default function WarrantyClaimPortalDetailPage({ params }: Props) {\n const t = useT()\n const locale = useLocale()\n const { auth } = usePortalContext()\n const { user, loading } = auth\n const guardedMutation = useGuardedMutation<Record<string, unknown>>({\n contextId: DETAIL_MUTATION_CONTEXT_ID,\n blockedMessage: t('warranty_claims.portal.detail.blocked'),\n })\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [claim, setClaim] = React.useState<PortalClaim | null>(null)\n const [events, setEvents] = React.useState<PortalClaimEvent[]>([])\n const [attachments, setAttachments] = React.useState<PortalAttachment[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n const [comment, setComment] = React.useState('')\n const [commentSubmitting, setCommentSubmitting] = React.useState(false)\n const [fileInputKey, setFileInputKey] = React.useState(0)\n const [uploading, setUploading] = React.useState(false)\n const [attachmentMutationId, setAttachmentMutationId] = React.useState<string | null>(null)\n const [claimAction, setClaimAction] = React.useState<PortalClaimAction | null>(null)\n const [actionSubmitting, setActionSubmitting] = React.useState(false)\n const replacementInputRef = React.useRef<HTMLInputElement | null>(null)\n const replacementTargetRef = React.useRef<PortalAttachment | null>(null)\n\n // Leaving an authenticated page for the login page crosses the public/authenticated\n // boundary, so it must be a full page load \u2014 see `replaceWithPageReload`.\n React.useEffect(() => {\n if (!loading && !user) {\n replaceWithPageReload(`/${params.orgSlug}/portal/login`)\n }\n }, [loading, user, params.orgSlug])\n\n const refreshEvents = React.useCallback(async (claimId: string) => {\n const result = await apiCall<EventResponse>(`/api/warranty_claims/portal/events?claimId=${encodeURIComponent(claimId)}`)\n if (result.ok && result.result) setEvents(result.result.items)\n }, [])\n\n const refreshAttachments = React.useCallback(async (claimId: string) => {\n const result = await apiCall<AttachmentResponse>(`/api/warranty_claims/portal/attachments?claimId=${encodeURIComponent(claimId)}`)\n if (result.ok && result.result) setAttachments(result.result.items)\n }, [])\n\n const loadClaim = React.useCallback(async () => {\n setIsLoading(true)\n setError(null)\n setNotFound(false)\n try {\n const detail = await apiCall<ClaimResponse>(`/api/warranty_claims/portal/claims/${encodeURIComponent(params.id)}`)\n if (detail.status === 404) {\n setClaim(null)\n setNotFound(true)\n return\n }\n if (!detail.ok || !detail.result) {\n setError(t('warranty_claims.portal.detail.loadError'))\n return\n }\n setClaim(detail.result.item)\n await Promise.all([\n refreshEvents(detail.result.item.id),\n refreshAttachments(detail.result.item.id),\n ])\n } catch {\n setError(t('warranty_claims.portal.detail.loadError'))\n } finally {\n setIsLoading(false)\n }\n }, [params.id, refreshAttachments, refreshEvents, t])\n\n React.useEffect(() => {\n if (user) void loadClaim()\n }, [loadClaim, user])\n\n const submitComment = React.useCallback(async () => {\n if (!claim || commentSubmitting) return\n const body = comment.trim()\n if (!body) return\n setCommentSubmitting(true)\n setError(null)\n try {\n const result = await guardedMutation.runMutation({\n operation: () => apiCall<MutationOkResponse>('/api/warranty_claims/portal/events', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ claimId: claim.id, body }),\n }),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'warranty_claims.claim_event',\n operation: 'portal_comment',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'warranty_claims.claim_event',\n resourceId: claim.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: { claimId: claim.id, body },\n })\n if (!result.ok || !result.result?.ok) {\n setError(t('warranty_claims.portal.detail.commentError'))\n return\n }\n setComment('')\n flash(t('warranty_claims.portal.detail.commentSuccess'), 'success')\n await refreshEvents(claim.id)\n } catch {\n setError(t('warranty_claims.portal.detail.commentError'))\n } finally {\n setCommentSubmitting(false)\n }\n }, [claim, comment, commentSubmitting, guardedMutation, refreshEvents, t])\n\n const uploadAttachment = React.useCallback(async (file: File, replacementTarget?: PortalAttachment) => {\n if (!claim || uploading) return\n const validationError = await validateAttachmentFile(file, t)\n if (validationError) {\n setError(validationError)\n flash(validationError, 'error')\n return\n }\n setUploading(true)\n setAttachmentMutationId(replacementTarget?.id ?? 'new')\n setError(null)\n try {\n const form = new FormData()\n form.set('claimId', claim.id)\n form.set('file', file)\n if (replacementTarget) form.set('attachmentId', replacementTarget.id)\n const result = await guardedMutation.runMutation({\n operation: () => apiCall<UploadResponse>('/api/warranty_claims/portal/attachments', {\n method: replacementTarget ? 'PUT' : 'POST',\n credentials: 'include',\n body: form,\n }),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'attachments.attachment',\n operation: replacementTarget ? 'portal_attachment_replace' : 'portal_attachment_upload',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'attachments.attachment',\n resourceId: replacementTarget?.id ?? claim.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: {\n claimId: claim.id,\n attachmentId: replacementTarget?.id,\n fileName: file.name,\n fileSize: file.size,\n },\n })\n if (!result.ok || !result.result?.ok) {\n setError(replacementTarget\n ? t('warranty_claims.portal.detail.attachmentReplaceError', 'Attachment replacement failed.')\n : t('warranty_claims.portal.detail.attachmentError'))\n return\n }\n flash(\n replacementTarget\n ? t('warranty_claims.portal.detail.attachmentReplaceSuccess', 'Attachment replaced.')\n : t('warranty_claims.portal.detail.attachmentSuccess'),\n 'success',\n )\n await refreshAttachments(claim.id)\n } catch {\n setError(replacementTarget\n ? t('warranty_claims.portal.detail.attachmentReplaceError', 'Attachment replacement failed.')\n : t('warranty_claims.portal.detail.attachmentError'))\n } finally {\n setUploading(false)\n setAttachmentMutationId(null)\n setFileInputKey((current) => current + 1)\n }\n }, [claim, guardedMutation, refreshAttachments, t, uploading])\n\n const deleteAttachment = React.useCallback(async (attachment: PortalAttachment) => {\n if (!claim || uploading) return\n const confirmed = await confirm({\n title: t('warranty_claims.portal.detail.attachmentDelete', 'Delete attachment'),\n text: t(\n 'warranty_claims.portal.detail.attachmentDeleteConfirm',\n 'Delete \"{name}\"? This action cannot be undone.',\n { name: attachment.fileName },\n ),\n confirmText: t('warranty_claims.portal.detail.attachmentDelete', 'Delete attachment'),\n cancelText: t('warranty_claims.portal.detail.dialogCancel', 'Cancel'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setUploading(true)\n setAttachmentMutationId(attachment.id)\n setError(null)\n try {\n const result = await guardedMutation.runMutation({\n operation: () => apiCall<MutationOkResponse>(\n `/api/warranty_claims/portal/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,\n { method: 'DELETE', credentials: 'include' },\n ),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'attachments.attachment',\n operation: 'portal_attachment_delete',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'attachments.attachment',\n resourceId: attachment.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: { claimId: claim.id, attachmentId: attachment.id },\n })\n if (!result.ok || !result.result?.ok) {\n setError(t('warranty_claims.portal.detail.attachmentDeleteError', 'Attachment deletion failed.'))\n return\n }\n flash(t('warranty_claims.portal.detail.attachmentDeleteSuccess', 'Attachment deleted.'), 'success')\n await refreshAttachments(claim.id)\n } catch {\n setError(t('warranty_claims.portal.detail.attachmentDeleteError', 'Attachment deletion failed.'))\n } finally {\n setUploading(false)\n setAttachmentMutationId(null)\n }\n }, [claim, confirm, guardedMutation, refreshAttachments, t, uploading])\n\n const requestAttachmentReplacement = React.useCallback((attachment: PortalAttachment) => {\n if (uploading) return\n replacementTargetRef.current = attachment\n replacementInputRef.current?.click()\n }, [uploading])\n\n const runClaimAction = React.useCallback(async (action: PortalClaimAction) => {\n if (!claim || actionSubmitting) return\n setActionSubmitting(true)\n setError(null)\n try {\n const endpoint = `/api/warranty_claims/portal/claims/${encodeURIComponent(claim.id)}/${action}`\n const result = await guardedMutation.runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(claim.updatedAt),\n () => apiCall<ClaimActionResponse>(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({}),\n }),\n ),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'warranty_claims.claim',\n operation: action === 'submit' ? 'portal_submit' : 'portal_withdraw',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'warranty_claims.claim',\n resourceId: claim.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: { claimId: claim.id, action },\n })\n if (!result.ok || !result.result?.ok) {\n const message = result.status === 409\n ? t('warranty_claims.portal.detail.actionConflict', 'This claim changed in the meantime. It has been reloaded \u2014 please try again.')\n : action === 'submit'\n ? t('warranty_claims.portal.detail.submitError', 'The claim could not be submitted.')\n : t('warranty_claims.portal.detail.withdrawError', 'The claim could not be withdrawn.')\n setError(message)\n flash(message, 'error')\n if (result.status === 409) {\n setClaimAction(null)\n await loadClaim()\n }\n return\n }\n setClaimAction(null)\n flash(\n action === 'submit'\n ? t('warranty_claims.portal.detail.submitSuccess', 'Your claim has been submitted.')\n : t('warranty_claims.portal.detail.withdrawSuccess', 'Your claim has been withdrawn.'),\n 'success',\n )\n await loadClaim()\n } catch {\n const message = action === 'submit'\n ? t('warranty_claims.portal.detail.submitError', 'The claim could not be submitted.')\n : t('warranty_claims.portal.detail.withdrawError', 'The claim could not be withdrawn.')\n setError(message)\n flash(message, 'error')\n } finally {\n setActionSubmitting(false)\n }\n }, [actionSubmitting, claim, guardedMutation, loadClaim, t])\n\n const handleSelectedFileChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0] ?? null\n if (!file) return\n void uploadAttachment(file)\n }, [uploadAttachment])\n\n const handleReplacementFileChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {\n const replacementTarget = replacementTargetRef.current\n replacementTargetRef.current = null\n const file = event.target.files?.[0] ?? null\n event.target.value = ''\n if (!replacementTarget || !file) return\n void uploadAttachment(file, replacementTarget)\n }, [uploadAttachment])\n\n const handleCommentSubmit = React.useCallback((event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n void submitComment()\n }, [submitComment])\n\n const handleCommentKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitComment()\n return\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setComment('')\n }\n }, [submitComment])\n\n if (loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (!user) return null\n\n if (isLoading) {\n return <LoadingMessage label={t('warranty_claims.portal.detail.loading')} />\n }\n\n if (notFound) {\n return (\n <PortalEmptyState\n icon={<ShieldCheck className=\"size-5\" />}\n title={t('warranty_claims.portal.detail.notFoundTitle')}\n description={t('warranty_claims.portal.detail.notFoundDescription')}\n action={\n <Button asChild>\n <Link href={`/${params.orgSlug}/portal/claims`}>\n <ArrowLeft className=\"size-4\" aria-hidden=\"true\" />\n {t('warranty_claims.portal.detail.back')}\n </Link>\n </Button>\n }\n />\n )\n }\n\n if (!claim) {\n return <ErrorMessage label={error ?? t('warranty_claims.portal.detail.loadError')} />\n }\n\n const trackerSteps = buildTrackerSteps(claim, t, locale)\n const isDraft = claim.status === 'draft'\n const canWithdraw = claim.status === 'draft' || claim.status === 'submitted'\n const tone = bannerTone(claim.status)\n const toneClasses = BANNER_TONE_CLASSES[tone]\n const BannerIcon = tone === 'error' ? CircleAlert : tone === 'warning' ? TriangleAlert : Info\n const activityEntries = buildActivityEntries(events, attachments, t)\n\n return (\n <div className=\"mx-auto flex w-full max-w-3xl flex-col gap-4\">\n <Link\n href={`/${params.orgSlug}/portal/claims`}\n className=\"text-sm font-medium text-muted-foreground transition-colors hover:text-foreground\"\n >\n {'\u2190'} {t('warranty_claims.portal.listTitle')}\n </Link>\n\n {error ? <ErrorMessage label={error} /> : null}\n\n <div className=\"w-full overflow-hidden rounded-lg border border-border bg-card\">\n <div className=\"flex w-full flex-col gap-3 px-6 pb-5 pt-6\">\n <div className=\"flex w-full items-center gap-2\">\n <div className=\"flex items-center gap-2 rounded-sm border border-border bg-background px-3 py-1\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n {t(`warranty_claims.claimType.${claim.claimType}`)}\n </span>\n <span className=\"text-sm font-semibold text-foreground\">\n {claim.claimNumber}\n </span>\n </div>\n <div className=\"min-w-px flex-1\" />\n {isDraft ? (\n <Button\n type=\"button\"\n size=\"sm\"\n className={DARK_BUTTON_CLASS}\n onClick={() => setClaimAction('submit')}\n disabled={actionSubmitting}\n >\n {t('warranty_claims.portal.submit')}\n </Button>\n ) : null}\n {canWithdraw ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"destructive\"\n className=\"rounded-md px-3 py-2 text-sm font-medium\"\n onClick={() => setClaimAction('withdraw')}\n disabled={actionSubmitting}\n >\n {t('warranty_claims.portal.detail.withdraw', 'Withdraw claim')}\n </Button>\n ) : null}\n </div>\n <h1 className=\"text-xl font-bold text-foreground\">\n {buildClaimTitle(claim, t)}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {buildClaimSubtitle(claim, t, locale)}\n </p>\n </div>\n\n <div className=\"w-full px-6 pb-6\">\n <div className={`flex w-full items-start gap-3 rounded-lg px-4 py-3 ${toneClasses.wrapper}`}>\n <BannerIcon className={`mt-px size-4 shrink-0 ${toneClasses.icon}`} aria-hidden=\"true\" />\n <p className={`min-w-px flex-1 text-sm ${toneClasses.text}`}>\n {bannerMessage(claim, t)}\n </p>\n </div>\n </div>\n\n <div className=\"flex w-full flex-col px-6 pb-6\">\n {trackerSteps.map((step, index) => (\n <div key={step.id} className=\"flex w-full items-stretch gap-4\">\n <div className=\"flex flex-col items-center\">\n {step.state === 'complete' ? (\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full bg-status-success-icon\">\n <Check className=\"size-3 text-status-success-bg\" strokeWidth={2.5} aria-hidden=\"true\" />\n </span>\n ) : step.state === 'current' ? (\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground\">\n {index + 1}\n </span>\n ) : (\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full border border-border text-xs font-semibold text-muted-foreground\">\n {index + 1}\n </span>\n )}\n {index < trackerSteps.length - 1 ? (\n <span\n className={`min-h-2 w-px flex-1 ${step.state === 'complete' ? 'bg-status-success-icon' : 'bg-border'}`}\n />\n ) : null}\n </div>\n <div className=\"flex min-w-px flex-1 flex-col gap-1 pb-4\">\n <div className=\"flex w-full items-center gap-2\">\n <p\n className={\n step.state === 'pending'\n ? 'text-sm font-medium text-muted-foreground'\n : 'text-sm font-semibold text-foreground'\n }\n >\n {step.title}\n </p>\n <div className=\"min-w-px flex-1\" />\n {step.dateLabel ? (\n <p className=\"text-xs text-muted-foreground\">{step.dateLabel}</p>\n ) : null}\n </div>\n <p className=\"w-full text-xs text-muted-foreground\">{step.description}</p>\n </div>\n </div>\n ))}\n </div>\n\n <div className={SECTION_CLASS}>\n <p className={SECTION_HEADING_CLASS}>{t('warranty_claims.portal.tracker.items')}</p>\n {claim.lines.length > 0 ? (\n claim.lines.map((line) => (\n <div\n key={line.id}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3\"\n >\n <div className=\"flex min-w-px flex-1 flex-col gap-1\">\n <p className=\"text-sm font-medium text-foreground\">\n {line.productName ?? line.sku ?? t('warranty_claims.portal.value.unnamedLine')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {line.sku\n ? t('warranty_claims.portal.tracker.itemMeta', {\n sku: line.sku,\n qty: formatQuantity(line.qtyClaimed, t('warranty_claims.portal.value.notAvailable')),\n })\n : t('warranty_claims.portal.tracker.itemQty', {\n qty: formatQuantity(line.qtyClaimed, t('warranty_claims.portal.value.notAvailable')),\n })}\n </p>\n </div>\n <div className=\"flex shrink-0 items-center gap-2 rounded-sm border border-border bg-background px-2 py-1\">\n <span\n className={`size-2 shrink-0 rounded-full ${LINE_STATUS_DOT_CLASSES[line.lineStatus] ?? 'bg-status-neutral-icon'}`}\n aria-hidden=\"true\"\n />\n <span className=\"text-xs font-medium text-foreground\">\n {t(`warranty_claims.lineStatus.${line.lineStatus}`)}\n </span>\n </div>\n </div>\n ))\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.portal.detail.linesEmpty.description')}\n </p>\n )}\n </div>\n\n <div className={SECTION_CLASS}>\n <p className={SECTION_HEADING_CLASS}>{t('warranty_claims.portal.tracker.activity')}</p>\n {activityEntries.length > 0 ? (\n activityEntries.map((entry) => (\n <div key={entry.id} className=\"flex w-full items-start gap-3\">\n <span className=\"mt-1 size-2 shrink-0 rounded-full bg-status-neutral-icon\" aria-hidden=\"true\" />\n <div className=\"flex min-w-px flex-1 flex-col gap-px\">\n <div className=\"flex w-full items-start gap-2\">\n <p className=\"text-sm font-medium text-foreground\">{entry.title}</p>\n <div className=\"min-w-px flex-1\" />\n {entry.createdAt ? (\n <p className=\"text-xs text-muted-foreground\">{formatShortDate(entry.createdAt, locale)}</p>\n ) : null}\n </div>\n {entry.href ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n <a\n href={entry.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"min-w-0 whitespace-pre-wrap text-xs text-muted-foreground underline-offset-2 hover:underline\"\n >\n {entry.description}\n </a>\n {entry.attachment ? (\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"px-2 text-xs\"\n aria-label={t(\n 'warranty_claims.portal.detail.attachmentReplaceNamed',\n 'Replace {name}',\n { name: entry.attachment.fileName },\n )}\n onClick={() => requestAttachmentReplacement(entry.attachment!)}\n disabled={uploading}\n >\n {attachmentMutationId === entry.attachment.id ? (\n <Spinner className=\"size-3\" />\n ) : (\n <RefreshCw className=\"size-3\" aria-hidden=\"true\" />\n )}\n {t('warranty_claims.portal.detail.attachmentReplace', 'Replace')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"px-2 text-xs text-status-error-text\"\n aria-label={t(\n 'warranty_claims.portal.detail.attachmentDeleteNamed',\n 'Delete {name}',\n { name: entry.attachment.fileName },\n )}\n onClick={() => { void deleteAttachment(entry.attachment!) }}\n disabled={uploading}\n >\n <Trash2 className=\"size-3\" aria-hidden=\"true\" />\n {t('warranty_claims.portal.detail.attachmentDelete', 'Delete attachment')}\n </Button>\n </div>\n ) : null}\n </div>\n ) : (\n <p className=\"whitespace-pre-wrap text-xs text-muted-foreground\">{entry.description}</p>\n )}\n </div>\n </div>\n ))\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.portal.detail.timelineEmpty.description')}\n </p>\n )}\n <input\n ref={replacementInputRef}\n type=\"file\"\n accept={ATTACHMENT_ACCEPT_TYPES}\n onChange={handleReplacementFileChange}\n disabled={uploading}\n className=\"sr-only\"\n aria-label={t('warranty_claims.portal.detail.attachmentChooseReplacement', 'Choose replacement file')}\n />\n </div>\n\n <div className={SECTION_CLASS}>\n <p className={SECTION_HEADING_CLASS}>{t('warranty_claims.portal.tracker.message')}</p>\n <form id=\"warranty-claim-comment-box\" className=\"flex w-full flex-col gap-3\" onSubmit={handleCommentSubmit}>\n <Textarea\n value={comment}\n onChange={(event) => setComment(event.target.value)}\n onKeyDown={handleCommentKeyDown}\n placeholder={t('warranty_claims.portal.tracker.messagePlaceholder')}\n disabled={commentSubmitting}\n maxLength={8000}\n className=\"h-40 resize-none rounded-lg border-input bg-background px-3 py-2 text-sm shadow-xs placeholder:text-muted-foreground\"\n />\n <div className=\"flex w-full items-center gap-2\">\n <label className={`flex items-center gap-2 ${uploading ? 'cursor-default opacity-70' : 'cursor-pointer'}`}>\n {uploading ? (\n <Spinner className=\"size-4\" />\n ) : (\n <Paperclip className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n )}\n <span className=\"text-xs font-medium text-muted-foreground transition-colors hover:text-foreground\">\n {uploading\n ? t('warranty_claims.portal.detail.uploading')\n : t('warranty_claims.portal.tracker.attachFiles')}\n </span>\n <input\n key={fileInputKey}\n type=\"file\"\n accept={ATTACHMENT_ACCEPT_TYPES}\n onChange={handleSelectedFileChange}\n disabled={uploading}\n className=\"sr-only\"\n />\n </label>\n <div className=\"min-w-px flex-1\" />\n <Button type=\"submit\" size=\"sm\" className={DARK_BUTTON_CLASS} disabled={commentSubmitting || !comment.trim()}>\n {commentSubmitting\n ? t('warranty_claims.portal.tracker.sending')\n : t('warranty_claims.portal.tracker.send')}\n </Button>\n </div>\n </form>\n </div>\n </div>\n\n <Dialog\n open={claimAction !== null}\n onOpenChange={(open) => {\n if (!open && !actionSubmitting) setClaimAction(null)\n }}\n >\n <DialogContent\n className=\"max-w-lg\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n if (claimAction && !actionSubmitting) void runClaimAction(claimAction)\n }\n if (event.key === 'Escape' && !actionSubmitting) {\n setClaimAction(null)\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>\n {claimAction === 'withdraw'\n ? t('warranty_claims.portal.detail.withdrawDialog.title', 'Withdraw this claim?')\n : t('warranty_claims.portal.detail.submitDialog.title', 'Submit this claim?')}\n </DialogTitle>\n <DialogDescription>\n {claimAction === 'withdraw'\n ? t('warranty_claims.portal.detail.withdrawDialog.description', 'The claim will be cancelled and cannot be reopened. This cannot be undone.')\n : t('warranty_claims.portal.detail.submitDialog.description', 'Your claim will be sent to our support team for review. You can still add comments and photos afterwards.')}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setClaimAction(null)}\n disabled={actionSubmitting}\n >\n {t('warranty_claims.portal.detail.dialogCancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n variant={claimAction === 'withdraw' ? 'destructive' : 'default'}\n onClick={() => {\n if (claimAction) void runClaimAction(claimAction)\n }}\n disabled={actionSubmitting}\n >\n {claimAction === 'withdraw'\n ? (actionSubmitting\n ? t('warranty_claims.portal.detail.withdrawing', 'Withdrawing...')\n : t('warranty_claims.portal.detail.withdrawConfirm', 'Withdraw claim'))\n : (actionSubmitting\n ? t('warranty_claims.portal.detail.submitting', 'Submitting...')\n : t('warranty_claims.portal.detail.submitConfirm', 'Submit claim'))}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAgpBmE,cAiBvD,YAjBuD;AA9oBnE,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,WAAW,OAAO,aAAa,MAAM,WAAW,WAAW,aAAa,QAAQ,qBAAqB;AAC9G,SAAS,6BAA6B;AACtC,SAAS,WAAW,YAA8B;AAClD,SAAS,+BAA+B;AACxC,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,SAAS,cAAc,sBAAsB;AAC7C,SAAS,aAAa;AACtB,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,SAAS,mCAAmC;AACrD,SAAS,iCAAiC;AAC1C,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;AAwFjC,MAAM,oBAAoB,CAAC,YAAY,UAAU,YAAY,UAAU,UAAU;AAEjF,MAAM,oBAA4C;AAAA,EAChD,OAAO;AAAA,EACP,WAAW;AAAA,EACX,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,WAAW;AAAA,EACX,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AACV;AAEA,MAAM,0BAAkD;AAAA,EACtD,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AAAA,EACX,UAAU;AACZ;AAEA,MAAM,sBAA2F;AAAA,EAC/F,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EACA,OAAO;AAAA,IACL,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACF;AAEA,SAAS,WAAW,QAA4B;AAC9C,MAAI,WAAW,cAAc,WAAW,YAAa,QAAO;AAC5D,MAAI,WAAW,iBAAkB,QAAO;AACxC,MAAI,WAAW,cAAc,WAAW,cAAc,WAAW,SAAU,QAAO;AAClF,SAAO;AACT;AAEA,SAAS,cAAc,OAAoB,GAAwB;AACjE,UAAQ,MAAM,QAAQ;AAAA,IACpB,KAAK;AACH,aAAO,EAAE,6CAA6C;AAAA,IACxD,KAAK;AACH,aAAO,EAAE,qDAAqD;AAAA,IAChE,KAAK;AACH,aAAO,EAAE,gDAAgD;AAAA,IAC3D,KAAK;AACH,aAAO,EAAE,sDAAsD;AAAA,IACjE,KAAK;AAAA,IACL,KAAK;AACH,aAAO,EAAE,iDAAiD;AAAA,IAC5D,KAAK;AAAA,IACL,KAAK;AACH,aAAO,MAAM,qBAAqB,EAAE,gDAAgD;AAAA,IACtF,KAAK,YAAY;AACf,YAAM,SAAS,MAAM,sBACjB,wBAAwB,GAAG,aAAa,MAAM,qBAAqB,MAAM,mBAAmB,IAC5F;AACJ,YAAM,OAAO,EAAE,iDAAiD;AAChE,YAAM,QAAQ,CAAC,MAAM,QAAQ,MAAM,iBAAiB,EAAE,OAAO,CAAC,SAAyB,QAAQ,IAAI,CAAC;AACpG,aAAO,MAAM,KAAK,GAAG;AAAA,IACvB;AAAA,IACA,KAAK;AACH,aAAO,EAAE,kDAAkD;AAAA,IAC7D;AACE,aAAO,EAAE,8CAA8C;AAAA,EAC3D;AACF;AAEA,SAAS,gBAAgB,OAAsB,QAA+B;AAC5E,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,KAAK,mBAAmB,UAAU,QAAW,EAAE,OAAO,SAAS,KAAK,UAAU,CAAC;AACxF;AAEA,SAAS,eAAe,OAAsB,QAA+B;AAC3E,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,SAAO,KAAK,mBAAmB,UAAU,QAAW,EAAE,MAAM,WAAW,OAAO,SAAS,KAAK,UAAU,CAAC;AACzG;AAEA,SAAS,kBAAkB,OAAoB,GAAgB,QAA+B;AAC5F,QAAM,eAAe,kBAAkB,MAAM,MAAM,KAAK;AACxD,QAAM,mBAAmB,MAAM,WAAW,cAAc,MAAM,WAAW;AACzE,SAAO,kBAAkB,IAAI,CAAC,KAAK,UAAU;AAC3C,UAAM,QAA0B,QAAQ,gBAAiB,oBAAoB,UAAU,eACnF,aACA,UAAU,eACR,YACA;AACN,QAAI,YAA2B;AAC/B,QAAI,UAAU,KAAK,UAAU,WAAY,aAAY,gBAAgB,MAAM,aAAa,MAAM;AAC9F,QAAI,UAAU,kBAAkB,SAAS,KAAK,UAAU,YAAY;AAClE,kBAAY;AAAA,QACV,MAAM,WAAW,WACb,MAAM,YAAY,MAAM,aACxB,MAAM,cAAc,MAAM;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,UAAW,aAAY,EAAE,yCAAyC;AAChF,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,EAAE,uCAAuC,GAAG,QAAQ;AAAA,MAC3D,aAAa,EAAE,uCAAuC,GAAG,cAAc;AAAA,MACvE;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,gBAAgB,OAAoB,GAAwB;AACnE,QAAM,YAAY,EAAE,6BAA6B,MAAM,SAAS,EAAE;AAClE,MAAI,MAAM,YAAY;AACpB,UAAM,cAAc,wBAAwB,GAAG,UAAU,MAAM,YAAY,MAAM,UAAU;AAC3F,WAAO,EAAE,kDAAkD,EAAE,QAAQ,aAAa,MAAM,UAAU,CAAC;AAAA,EACrG;AACA,SAAO,EAAE,gDAAgD,EAAE,MAAM,UAAU,CAAC;AAC9E;AAEA,SAAS,mBAAmB,OAAoB,GAAgB,QAAwB;AACtF,MAAI,MAAM,WAAW,SAAS;AAC5B,UAAM,UAAU,eAAe,MAAM,WAAW,MAAM;AACtD,WAAO,UACH,EAAE,+CAA+C,EAAE,MAAM,QAAQ,CAAC,IAClE,EAAE,4CAA4C;AAAA,EACpD;AACA,QAAM,YAAY,eAAe,MAAM,aAAa,MAAM,KAAK,eAAe,MAAM,WAAW,MAAM;AACrG,MAAI,CAAC,UAAW,QAAO,EAAE,2CAA2C;AACpE,MAAI,MAAM,aAAa;AACrB,WAAO,EAAE,qDAAqD,EAAE,MAAM,WAAW,OAAO,MAAM,YAAY,CAAC;AAAA,EAC7G;AACA,SAAO,EAAE,8CAA8C,EAAE,MAAM,UAAU,CAAC;AAC5E;AAEA,SAAS,gBAAgB,OAAyB,GAAwB;AACxE,MAAI,MAAM,KAAM,QAAO,MAAM;AAC7B,QAAM,UAAU,MAAM;AACtB,QAAM,OAAO,OAAO,SAAS,SAAS,WAAW,QAAQ,OAAO;AAChE,QAAM,KAAK,OAAO,SAAS,OAAO,WAAW,QAAQ,KAAK;AAC1D,MAAI,QAAQ,IAAI;AACd,WAAO,EAAE,2CAA2C;AAAA,MAClD,MAAM,EAAE,0BAA0B,IAAI,EAAE;AAAA,MACxC,IAAI,EAAE,0BAA0B,EAAE,EAAE;AAAA,IACtC,CAAC;AAAA,EACH;AACA,SAAO,EAAE,wCAAwC;AACnD;AAEA,SAAS,WAAW,OAAyB,GAAwB;AACnE,MAAI,MAAM,SAAS,WAAW;AAC5B,WAAO,MAAM,kBACT,EAAE,iDAAiD,IACnD,EAAE,kDAAkD;AAAA,EAC1D;AACA,SAAO,EAAE,gCAAgC,MAAM,IAAI,EAAE;AACvD;AAEA,SAAS,qBACP,QACA,aACA,GACiB;AACjB,QAAM,eAAgC,OAAO,IAAI,CAAC,WAAW;AAAA,IAC3D,IAAI,SAAS,MAAM,EAAE;AAAA,IACrB,OAAO,WAAW,OAAO,CAAC;AAAA,IAC1B,aAAa,gBAAgB,OAAO,CAAC;AAAA,IACrC,WAAW,MAAM;AAAA,EACnB,EAAE;AACF,QAAM,oBAAqC,YAAY,IAAI,CAAC,gBAAgB;AAAA,IAC1E,IAAI,cAAc,WAAW,EAAE;AAAA,IAC/B,OAAO,EAAE,sDAAsD;AAAA,IAC/D,aAAa,WAAW;AAAA,IACxB,WAAW,WAAW;AAAA,IACtB,MAAM,WAAW;AAAA,IACjB;AAAA,EACF,EAAE;AACF,SAAO,CAAC,GAAG,cAAc,GAAG,iBAAiB,EAAE,KAAK,CAAC,MAAM,UAAU;AACnE,UAAM,WAAW,KAAK,YAAY,IAAI,KAAK,KAAK,SAAS,EAAE,QAAQ,IAAI;AACvE,UAAM,YAAY,MAAM,YAAY,IAAI,KAAK,MAAM,SAAS,EAAE,QAAQ,IAAI;AAC1E,WAAO,YAAY;AAAA,EACrB,CAAC;AACH;AAEA,MAAM,6BAA6B;AAEnC,MAAM,gBAAgB;AACtB,MAAM,wBAAwB;AAC9B,MAAM,oBAAoB;AAEX,SAAR,8BAA+C,EAAE,OAAO,GAAU;AACvE,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,EAAE,KAAK,IAAI,iBAAiB;AAClC,QAAM,EAAE,MAAM,QAAQ,IAAI;AAC1B,QAAM,kBAAkB,mBAA4C;AAAA,IAClE,WAAW;AAAA,IACX,gBAAgB,EAAE,uCAAuC;AAAA,EAC3D,CAAC;AACD,QAAM,EAAE,SAAS,qBAAqB,IAAI,iBAAiB;AAC3D,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAA6B,IAAI;AACjE,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA6B,CAAC,CAAC;AACjE,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAA6B,CAAC,CAAC;AAC3E,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AACpD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,EAAE;AAC/C,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAS,KAAK;AACtE,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,CAAC;AACxD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,CAAC,sBAAsB,uBAAuB,IAAI,MAAM,SAAwB,IAAI;AAC1F,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAmC,IAAI;AACnF,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,KAAK;AACpE,QAAM,sBAAsB,MAAM,OAAgC,IAAI;AACtE,QAAM,uBAAuB,MAAM,OAAgC,IAAI;AAIvE,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,WAAW,CAAC,MAAM;AACrB,4BAAsB,IAAI,OAAO,OAAO,eAAe;AAAA,IACzD;AAAA,EACF,GAAG,CAAC,SAAS,MAAM,OAAO,OAAO,CAAC;AAElC,QAAM,gBAAgB,MAAM,YAAY,OAAO,YAAoB;AACjE,UAAM,SAAS,MAAM,QAAuB,8CAA8C,mBAAmB,OAAO,CAAC,EAAE;AACvH,QAAI,OAAO,MAAM,OAAO,OAAQ,WAAU,OAAO,OAAO,KAAK;AAAA,EAC/D,GAAG,CAAC,CAAC;AAEL,QAAM,qBAAqB,MAAM,YAAY,OAAO,YAAoB;AACtE,UAAM,SAAS,MAAM,QAA4B,mDAAmD,mBAAmB,OAAO,CAAC,EAAE;AACjI,QAAI,OAAO,MAAM,OAAO,OAAQ,gBAAe,OAAO,OAAO,KAAK;AAAA,EACpE,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY,MAAM,YAAY,YAAY;AAC9C,iBAAa,IAAI;AACjB,aAAS,IAAI;AACb,gBAAY,KAAK;AACjB,QAAI;AACF,YAAM,SAAS,MAAM,QAAuB,sCAAsC,mBAAmB,OAAO,EAAE,CAAC,EAAE;AACjH,UAAI,OAAO,WAAW,KAAK;AACzB,iBAAS,IAAI;AACb,oBAAY,IAAI;AAChB;AAAA,MACF;AACA,UAAI,CAAC,OAAO,MAAM,CAAC,OAAO,QAAQ;AAChC,iBAAS,EAAE,yCAAyC,CAAC;AACrD;AAAA,MACF;AACA,eAAS,OAAO,OAAO,IAAI;AAC3B,YAAM,QAAQ,IAAI;AAAA,QAChB,cAAc,OAAO,OAAO,KAAK,EAAE;AAAA,QACnC,mBAAmB,OAAO,OAAO,KAAK,EAAE;AAAA,MAC1C,CAAC;AAAA,IACH,QAAQ;AACN,eAAS,EAAE,yCAAyC,CAAC;AAAA,IACvD,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,OAAO,IAAI,oBAAoB,eAAe,CAAC,CAAC;AAEpD,QAAM,UAAU,MAAM;AACpB,QAAI,KAAM,MAAK,UAAU;AAAA,EAC3B,GAAG,CAAC,WAAW,IAAI,CAAC;AAEpB,QAAM,gBAAgB,MAAM,YAAY,YAAY;AAClD,QAAI,CAAC,SAAS,kBAAmB;AACjC,UAAM,OAAO,QAAQ,KAAK;AAC1B,QAAI,CAAC,KAAM;AACX,yBAAqB,IAAI;AACzB,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,MAAM,gBAAgB,YAAY;AAAA,QAC/C,WAAW,MAAM,QAA4B,sCAAsC;AAAA,UACjF,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,aAAa;AAAA,UACb,MAAM,KAAK,UAAU,EAAE,SAAS,MAAM,IAAI,KAAK,CAAC;AAAA,QAClD,CAAC;AAAA,QACD,SAAS;AAAA,UACP,UAAU;AAAA,UACV,UAAU;AAAA,UACV,WAAW;AAAA,UACX,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY,MAAM;AAAA,UAClB,mBAAmB,gBAAgB;AAAA,QACrC;AAAA,QACA,iBAAiB,EAAE,SAAS,MAAM,IAAI,KAAK;AAAA,MAC7C,CAAC;AACD,UAAI,CAAC,OAAO,MAAM,CAAC,OAAO,QAAQ,IAAI;AACpC,iBAAS,EAAE,4CAA4C,CAAC;AACxD;AAAA,MACF;AACA,iBAAW,EAAE;AACb,YAAM,EAAE,8CAA8C,GAAG,SAAS;AAClE,YAAM,cAAc,MAAM,EAAE;AAAA,IAC9B,QAAQ;AACN,eAAS,EAAE,4CAA4C,CAAC;AAAA,IAC1D,UAAE;AACA,2BAAqB,KAAK;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,mBAAmB,iBAAiB,eAAe,CAAC,CAAC;AAEzE,QAAM,mBAAmB,MAAM,YAAY,OAAO,MAAY,sBAAyC;AACrG,QAAI,CAAC,SAAS,UAAW;AACzB,UAAM,kBAAkB,MAAM,uBAAuB,MAAM,CAAC;AAC5D,QAAI,iBAAiB;AACnB,eAAS,eAAe;AACxB,YAAM,iBAAiB,OAAO;AAC9B;AAAA,IACF;AACA,iBAAa,IAAI;AACjB,4BAAwB,mBAAmB,MAAM,KAAK;AACtD,aAAS,IAAI;AACb,QAAI;AACF,YAAM,OAAO,IAAI,SAAS;AAC1B,WAAK,IAAI,WAAW,MAAM,EAAE;AAC5B,WAAK,IAAI,QAAQ,IAAI;AACrB,UAAI,kBAAmB,MAAK,IAAI,gBAAgB,kBAAkB,EAAE;AACpE,YAAM,SAAS,MAAM,gBAAgB,YAAY;AAAA,QAC/C,WAAW,MAAM,QAAwB,2CAA2C;AAAA,UAClF,QAAQ,oBAAoB,QAAQ;AAAA,UACpC,aAAa;AAAA,UACb,MAAM;AAAA,QACR,CAAC;AAAA,QACD,SAAS;AAAA,UACP,UAAU;AAAA,UACV,UAAU;AAAA,UACV,WAAW,oBAAoB,8BAA8B;AAAA,UAC7D,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY,mBAAmB,MAAM,MAAM;AAAA,UAC3C,mBAAmB,gBAAgB;AAAA,QACrC;AAAA,QACA,iBAAiB;AAAA,UACf,SAAS,MAAM;AAAA,UACf,cAAc,mBAAmB;AAAA,UACjC,UAAU,KAAK;AAAA,UACf,UAAU,KAAK;AAAA,QACjB;AAAA,MACF,CAAC;AACD,UAAI,CAAC,OAAO,MAAM,CAAC,OAAO,QAAQ,IAAI;AACpC,iBAAS,oBACL,EAAE,wDAAwD,gCAAgC,IAC1F,EAAE,+CAA+C,CAAC;AACtD;AAAA,MACF;AACA;AAAA,QACE,oBACI,EAAE,0DAA0D,sBAAsB,IAClF,EAAE,iDAAiD;AAAA,QACvD;AAAA,MACF;AACA,YAAM,mBAAmB,MAAM,EAAE;AAAA,IACnC,QAAQ;AACN,eAAS,oBACL,EAAE,wDAAwD,gCAAgC,IAC1F,EAAE,+CAA+C,CAAC;AAAA,IACxD,UAAE;AACA,mBAAa,KAAK;AAClB,8BAAwB,IAAI;AAC5B,sBAAgB,CAAC,YAAY,UAAU,CAAC;AAAA,IAC1C;AAAA,EACF,GAAG,CAAC,OAAO,iBAAiB,oBAAoB,GAAG,SAAS,CAAC;AAE7D,QAAM,mBAAmB,MAAM,YAAY,OAAO,eAAiC;AACjF,QAAI,CAAC,SAAS,UAAW;AACzB,UAAM,YAAY,MAAM,QAAQ;AAAA,MAC9B,OAAO,EAAE,kDAAkD,mBAAmB;AAAA,MAC9E,MAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA,EAAE,MAAM,WAAW,SAAS;AAAA,MAC9B;AAAA,MACA,aAAa,EAAE,kDAAkD,mBAAmB;AAAA,MACpF,YAAY,EAAE,8CAA8C,QAAQ;AAAA,MACpE,SAAS;AAAA,IACX,CAAC;AACD,QAAI,CAAC,UAAW;AAChB,iBAAa,IAAI;AACjB,4BAAwB,WAAW,EAAE;AACrC,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,MAAM,gBAAgB,YAAY;AAAA,QAC/C,WAAW,MAAM;AAAA,UACf,wDAAwD,mBAAmB,WAAW,EAAE,CAAC;AAAA,UACzF,EAAE,QAAQ,UAAU,aAAa,UAAU;AAAA,QAC7C;AAAA,QACA,SAAS;AAAA,UACP,UAAU;AAAA,UACV,UAAU;AAAA,UACV,WAAW;AAAA,UACX,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY,WAAW;AAAA,UACvB,mBAAmB,gBAAgB;AAAA,QACrC;AAAA,QACA,iBAAiB,EAAE,SAAS,MAAM,IAAI,cAAc,WAAW,GAAG;AAAA,MACpE,CAAC;AACD,UAAI,CAAC,OAAO,MAAM,CAAC,OAAO,QAAQ,IAAI;AACpC,iBAAS,EAAE,uDAAuD,6BAA6B,CAAC;AAChG;AAAA,MACF;AACA,YAAM,EAAE,yDAAyD,qBAAqB,GAAG,SAAS;AAClG,YAAM,mBAAmB,MAAM,EAAE;AAAA,IACnC,QAAQ;AACN,eAAS,EAAE,uDAAuD,6BAA6B,CAAC;AAAA,IAClG,UAAE;AACA,mBAAa,KAAK;AAClB,8BAAwB,IAAI;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,iBAAiB,oBAAoB,GAAG,SAAS,CAAC;AAEtE,QAAM,+BAA+B,MAAM,YAAY,CAAC,eAAiC;AACvF,QAAI,UAAW;AACf,yBAAqB,UAAU;AAC/B,wBAAoB,SAAS,MAAM;AAAA,EACrC,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,iBAAiB,MAAM,YAAY,OAAO,WAA8B;AAC5E,QAAI,CAAC,SAAS,iBAAkB;AAChC,wBAAoB,IAAI;AACxB,aAAS,IAAI;AACb,QAAI;AACF,YAAM,WAAW,sCAAsC,mBAAmB,MAAM,EAAE,CAAC,IAAI,MAAM;AAC7F,YAAM,SAAS,MAAM,gBAAgB,YAAY;AAAA,QAC/C,WAAW,MAAM;AAAA,UACf,0BAA0B,MAAM,SAAS;AAAA,UACzC,MAAM,QAA6B,UAAU;AAAA,YAC3C,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,aAAa;AAAA,YACb,MAAM,KAAK,UAAU,CAAC,CAAC;AAAA,UACzB,CAAC;AAAA,QACH;AAAA,QACA,SAAS;AAAA,UACP,UAAU;AAAA,UACV,UAAU;AAAA,UACV,WAAW,WAAW,WAAW,kBAAkB;AAAA,UACnD,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,cAAc;AAAA,UACd,YAAY,MAAM;AAAA,UAClB,mBAAmB,gBAAgB;AAAA,QACrC;AAAA,QACA,iBAAiB,EAAE,SAAS,MAAM,IAAI,OAAO;AAAA,MAC/C,CAAC;AACD,UAAI,CAAC,OAAO,MAAM,CAAC,OAAO,QAAQ,IAAI;AACpC,cAAM,UAAU,OAAO,WAAW,MAC9B,EAAE,gDAAgD,mFAA8E,IAChI,WAAW,WACT,EAAE,6CAA6C,mCAAmC,IAClF,EAAE,+CAA+C,mCAAmC;AAC1F,iBAAS,OAAO;AAChB,cAAM,SAAS,OAAO;AACtB,YAAI,OAAO,WAAW,KAAK;AACzB,yBAAe,IAAI;AACnB,gBAAM,UAAU;AAAA,QAClB;AACA;AAAA,MACF;AACA,qBAAe,IAAI;AACnB;AAAA,QACE,WAAW,WACP,EAAE,+CAA+C,gCAAgC,IACjF,EAAE,iDAAiD,gCAAgC;AAAA,QACvF;AAAA,MACF;AACA,YAAM,UAAU;AAAA,IAClB,QAAQ;AACN,YAAM,UAAU,WAAW,WACvB,EAAE,6CAA6C,mCAAmC,IAClF,EAAE,+CAA+C,mCAAmC;AACxF,eAAS,OAAO;AAChB,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,0BAAoB,KAAK;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,kBAAkB,OAAO,iBAAiB,WAAW,CAAC,CAAC;AAE3D,QAAM,2BAA2B,MAAM,YAAY,CAAC,UAA+C;AACjG,UAAM,OAAO,MAAM,OAAO,QAAQ,CAAC,KAAK;AACxC,QAAI,CAAC,KAAM;AACX,SAAK,iBAAiB,IAAI;AAAA,EAC5B,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,8BAA8B,MAAM,YAAY,CAAC,UAA+C;AACpG,UAAM,oBAAoB,qBAAqB;AAC/C,yBAAqB,UAAU;AAC/B,UAAM,OAAO,MAAM,OAAO,QAAQ,CAAC,KAAK;AACxC,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,qBAAqB,CAAC,KAAM;AACjC,SAAK,iBAAiB,MAAM,iBAAiB;AAAA,EAC/C,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,sBAAsB,MAAM,YAAY,CAAC,UAA4C;AACzF,UAAM,eAAe;AACrB,SAAK,cAAc;AAAA,EACrB,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,uBAAuB,MAAM,YAAY,CAAC,UAAoD;AAClG,SAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,YAAM,eAAe;AACrB,WAAK,cAAc;AACnB;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,UAAU;AAC1B,YAAM,eAAe;AACrB,iBAAW,EAAE;AAAA,IACf;AAAA,EACF,GAAG,CAAC,aAAa,CAAC;AAElB,MAAI,SAAS;AACX,WAAO,oBAAC,SAAI,WAAU,0CAAyC,8BAAC,WAAQ,GAAE;AAAA,EAC5E;AAEA,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,WAAW;AACb,WAAO,oBAAC,kBAAe,OAAO,EAAE,uCAAuC,GAAG;AAAA,EAC5E;AAEA,MAAI,UAAU;AACZ,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,oBAAC,eAAY,WAAU,UAAS;AAAA,QACtC,OAAO,EAAE,6CAA6C;AAAA,QACtD,aAAa,EAAE,mDAAmD;AAAA,QAClE,QACE,oBAAC,UAAO,SAAO,MACb,+BAAC,QAAK,MAAM,IAAI,OAAO,OAAO,kBAC5B;AAAA,8BAAC,aAAU,WAAU,UAAS,eAAY,QAAO;AAAA,UAChD,EAAE,oCAAoC;AAAA,WACzC,GACF;AAAA;AAAA,IAEJ;AAAA,EAEJ;AAEA,MAAI,CAAC,OAAO;AACV,WAAO,oBAAC,gBAAa,OAAO,SAAS,EAAE,yCAAyC,GAAG;AAAA,EACrF;AAEA,QAAM,eAAe,kBAAkB,OAAO,GAAG,MAAM;AACvD,QAAM,UAAU,MAAM,WAAW;AACjC,QAAM,cAAc,MAAM,WAAW,WAAW,MAAM,WAAW;AACjE,QAAM,OAAO,WAAW,MAAM,MAAM;AACpC,QAAM,cAAc,oBAAoB,IAAI;AAC5C,QAAM,aAAa,SAAS,UAAU,cAAc,SAAS,YAAY,gBAAgB;AACzF,QAAM,kBAAkB,qBAAqB,QAAQ,aAAa,CAAC;AAEnE,SACE,qBAAC,SAAI,WAAU,gDACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,IAAI,OAAO,OAAO;AAAA,QACxB,WAAU;AAAA,QAET;AAAA;AAAA,UAAI;AAAA,UAAG,EAAE,kCAAkC;AAAA;AAAA;AAAA,IAC9C;AAAA,IAEC,QAAQ,oBAAC,gBAAa,OAAO,OAAO,IAAK;AAAA,IAE1C,qBAAC,SAAI,WAAU,kEACb;AAAA,2BAAC,SAAI,WAAU,6CACb;AAAA,6BAAC,SAAI,WAAU,kCACb;AAAA,+BAAC,SAAI,WAAU,mFACb;AAAA,gCAAC,UAAK,WAAU,6CACb,YAAE,6BAA6B,MAAM,SAAS,EAAE,GACnD;AAAA,YACA,oBAAC,UAAK,WAAU,yCACb,gBAAM,aACT;AAAA,aACF;AAAA,UACA,oBAAC,SAAI,WAAU,mBAAkB;AAAA,UAChC,UACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,WAAW;AAAA,cACX,SAAS,MAAM,eAAe,QAAQ;AAAA,cACtC,UAAU;AAAA,cAET,YAAE,+BAA+B;AAAA;AAAA,UACpC,IACE;AAAA,UACH,cACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,WAAU;AAAA,cACV,SAAS,MAAM,eAAe,UAAU;AAAA,cACxC,UAAU;AAAA,cAET,YAAE,0CAA0C,gBAAgB;AAAA;AAAA,UAC/D,IACE;AAAA,WACN;AAAA,QACA,oBAAC,QAAG,WAAU,qCACX,0BAAgB,OAAO,CAAC,GAC3B;AAAA,QACA,oBAAC,OAAE,WAAU,iCACV,6BAAmB,OAAO,GAAG,MAAM,GACtC;AAAA,SACF;AAAA,MAEA,oBAAC,SAAI,WAAU,oBACb,+BAAC,SAAI,WAAW,sDAAsD,YAAY,OAAO,IACvF;AAAA,4BAAC,cAAW,WAAW,yBAAyB,YAAY,IAAI,IAAI,eAAY,QAAO;AAAA,QACvF,oBAAC,OAAE,WAAW,2BAA2B,YAAY,IAAI,IACtD,wBAAc,OAAO,CAAC,GACzB;AAAA,SACF,GACF;AAAA,MAEA,oBAAC,SAAI,WAAU,kCACZ,uBAAa,IAAI,CAAC,MAAM,UACvB,qBAAC,SAAkB,WAAU,mCAC3B;AAAA,6BAAC,SAAI,WAAU,8BACZ;AAAA,eAAK,UAAU,aACd,oBAAC,UAAK,WAAU,wFACd,8BAAC,SAAM,WAAU,iCAAgC,aAAa,KAAK,eAAY,QAAO,GACxF,IACE,KAAK,UAAU,YACjB,oBAAC,UAAK,WAAU,0HACb,kBAAQ,GACX,IAEA,oBAAC,UAAK,WAAU,kIACb,kBAAQ,GACX;AAAA,UAED,QAAQ,aAAa,SAAS,IAC7B;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,uBAAuB,KAAK,UAAU,aAAa,2BAA2B,WAAW;AAAA;AAAA,UACtG,IACE;AAAA,WACN;AAAA,QACA,qBAAC,SAAI,WAAU,4CACb;AAAA,+BAAC,SAAI,WAAU,kCACb;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,WACE,KAAK,UAAU,YACX,8CACA;AAAA,gBAGL,eAAK;AAAA;AAAA,YACR;AAAA,YACA,oBAAC,SAAI,WAAU,mBAAkB;AAAA,YAChC,KAAK,YACJ,oBAAC,OAAE,WAAU,iCAAiC,eAAK,WAAU,IAC3D;AAAA,aACN;AAAA,UACA,oBAAC,OAAE,WAAU,wCAAwC,eAAK,aAAY;AAAA,WACxE;AAAA,WAtCQ,KAAK,EAuCf,CACD,GACH;AAAA,MAEA,qBAAC,SAAI,WAAW,eACd;AAAA,4BAAC,OAAE,WAAW,uBAAwB,YAAE,sCAAsC,GAAE;AAAA,QAC/E,MAAM,MAAM,SAAS,IACpB,MAAM,MAAM,IAAI,CAAC,SACf;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YAEV;AAAA,mCAAC,SAAI,WAAU,uCACb;AAAA,oCAAC,OAAE,WAAU,uCACV,eAAK,eAAe,KAAK,OAAO,EAAE,0CAA0C,GAC/E;AAAA,gBACA,oBAAC,OAAE,WAAU,iCACV,eAAK,MACF,EAAE,2CAA2C;AAAA,kBAC3C,KAAK,KAAK;AAAA,kBACV,KAAK,eAAe,KAAK,YAAY,EAAE,2CAA2C,CAAC;AAAA,gBACrF,CAAC,IACD,EAAE,0CAA0C;AAAA,kBAC1C,KAAK,eAAe,KAAK,YAAY,EAAE,2CAA2C,CAAC;AAAA,gBACrF,CAAC,GACP;AAAA,iBACF;AAAA,cACA,qBAAC,SAAI,WAAU,4FACb;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,gCAAgC,wBAAwB,KAAK,UAAU,KAAK,wBAAwB;AAAA,oBAC/G,eAAY;AAAA;AAAA,gBACd;AAAA,gBACA,oBAAC,UAAK,WAAU,uCACb,YAAE,8BAA8B,KAAK,UAAU,EAAE,GACpD;AAAA,iBACF;AAAA;AAAA;AAAA,UA1BK,KAAK;AAAA,QA2BZ,CACD,IAED,oBAAC,OAAE,WAAU,iCACV,YAAE,sDAAsD,GAC3D;AAAA,SAEJ;AAAA,MAEA,qBAAC,SAAI,WAAW,eACd;AAAA,4BAAC,OAAE,WAAW,uBAAwB,YAAE,yCAAyC,GAAE;AAAA,QAClF,gBAAgB,SAAS,IACxB,gBAAgB,IAAI,CAAC,UACnB,qBAAC,SAAmB,WAAU,iCAC5B;AAAA,8BAAC,UAAK,WAAU,4DAA2D,eAAY,QAAO;AAAA,UAC9F,qBAAC,SAAI,WAAU,wCACb;AAAA,iCAAC,SAAI,WAAU,iCACb;AAAA,kCAAC,OAAE,WAAU,uCAAuC,gBAAM,OAAM;AAAA,cAChE,oBAAC,SAAI,WAAU,mBAAkB;AAAA,cAChC,MAAM,YACL,oBAAC,OAAE,WAAU,iCAAiC,0BAAgB,MAAM,WAAW,MAAM,GAAE,IACrF;AAAA,eACN;AAAA,YACC,MAAM,OACL,qBAAC,SAAI,WAAU,qCACb;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAM,MAAM;AAAA,kBACZ,QAAO;AAAA,kBACP,KAAI;AAAA,kBACJ,WAAU;AAAA,kBAET,gBAAM;AAAA;AAAA,cACT;AAAA,cACC,MAAM,aACL,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAQ;AAAA,oBACR,MAAK;AAAA,oBACL,WAAU;AAAA,oBACV,cAAY;AAAA,sBACV;AAAA,sBACA;AAAA,sBACA,EAAE,MAAM,MAAM,WAAW,SAAS;AAAA,oBACpC;AAAA,oBACA,SAAS,MAAM,6BAA6B,MAAM,UAAW;AAAA,oBAC7D,UAAU;AAAA,oBAET;AAAA,+CAAyB,MAAM,WAAW,KACzC,oBAAC,WAAQ,WAAU,UAAS,IAE5B,oBAAC,aAAU,WAAU,UAAS,eAAY,QAAO;AAAA,sBAElD,EAAE,mDAAmD,SAAS;AAAA;AAAA;AAAA,gBACjE;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAQ;AAAA,oBACR,MAAK;AAAA,oBACL,WAAU;AAAA,oBACV,cAAY;AAAA,sBACV;AAAA,sBACA;AAAA,sBACA,EAAE,MAAM,MAAM,WAAW,SAAS;AAAA,oBACpC;AAAA,oBACA,SAAS,MAAM;AAAE,2BAAK,iBAAiB,MAAM,UAAW;AAAA,oBAAE;AAAA,oBAC1D,UAAU;AAAA,oBAEV;AAAA,0CAAC,UAAO,WAAU,UAAS,eAAY,QAAO;AAAA,sBAC7C,EAAE,kDAAkD,mBAAmB;AAAA;AAAA;AAAA,gBAC1E;AAAA,iBACF,IACE;AAAA,eACN,IAEA,oBAAC,OAAE,WAAU,qDAAqD,gBAAM,aAAY;AAAA,aAExF;AAAA,aAhEQ,MAAM,EAiEhB,CACD,IAED,oBAAC,OAAE,WAAU,iCACV,YAAE,yDAAyD,GAC9D;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,QAAQ;AAAA,YACR,UAAU;AAAA,YACV,UAAU;AAAA,YACV,WAAU;AAAA,YACV,cAAY,EAAE,6DAA6D,yBAAyB;AAAA;AAAA,QACtG;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAW,eACd;AAAA,4BAAC,OAAE,WAAW,uBAAwB,YAAE,wCAAwC,GAAE;AAAA,QAClF,qBAAC,UAAK,IAAG,8BAA6B,WAAU,8BAA6B,UAAU,qBACrF;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU,CAAC,UAAU,WAAW,MAAM,OAAO,KAAK;AAAA,cAClD,WAAW;AAAA,cACX,aAAa,EAAE,mDAAmD;AAAA,cAClE,UAAU;AAAA,cACV,WAAW;AAAA,cACX,WAAU;AAAA;AAAA,UACZ;AAAA,UACA,qBAAC,SAAI,WAAU,kCACb;AAAA,iCAAC,WAAM,WAAW,2BAA2B,YAAY,8BAA8B,gBAAgB,IACpG;AAAA,0BACC,oBAAC,WAAQ,WAAU,UAAS,IAE5B,oBAAC,aAAU,WAAU,gCAA+B,eAAY,QAAO;AAAA,cAEzE,oBAAC,UAAK,WAAU,qFACb,sBACG,EAAE,yCAAyC,IAC3C,EAAE,4CAA4C,GACpD;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBAEC,MAAK;AAAA,kBACL,QAAQ;AAAA,kBACR,UAAU;AAAA,kBACV,UAAU;AAAA,kBACV,WAAU;AAAA;AAAA,gBALL;AAAA,cAMP;AAAA,eACF;AAAA,YACA,oBAAC,SAAI,WAAU,mBAAkB;AAAA,YACjC,oBAAC,UAAO,MAAK,UAAS,MAAK,MAAK,WAAW,mBAAmB,UAAU,qBAAqB,CAAC,QAAQ,KAAK,GACxG,8BACG,EAAE,wCAAwC,IAC1C,EAAE,qCAAqC,GAC7C;AAAA,aACF;AAAA,WACF;AAAA,SACF;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,gBAAgB;AAAA,QACtB,cAAc,CAAC,SAAS;AACtB,cAAI,CAAC,QAAQ,CAAC,iBAAkB,gBAAe,IAAI;AAAA,QACrD;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,WAAW,CAAC,UAAU;AACpB,mBAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,sBAAM,eAAe;AACrB,oBAAI,eAAe,CAAC,iBAAkB,MAAK,eAAe,WAAW;AAAA,cACvE;AACA,kBAAI,MAAM,QAAQ,YAAY,CAAC,kBAAkB;AAC/C,+BAAe,IAAI;AAAA,cACrB;AAAA,YACF;AAAA,YAEA;AAAA,mCAAC,gBACC;AAAA,oCAAC,eACE,0BAAgB,aACb,EAAE,sDAAsD,sBAAsB,IAC9E,EAAE,oDAAoD,oBAAoB,GAChF;AAAA,gBACA,oBAAC,qBACE,0BAAgB,aACb,EAAE,4DAA4D,4EAA4E,IAC1I,EAAE,0DAA0D,2GAA2G,GAC7K;AAAA,iBACF;AAAA,cACA,qBAAC,gBACC;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAQ;AAAA,oBACR,SAAS,MAAM,eAAe,IAAI;AAAA,oBAClC,UAAU;AAAA,oBAET,YAAE,8CAA8C,QAAQ;AAAA;AAAA,gBAC3D;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS,gBAAgB,aAAa,gBAAgB;AAAA,oBACtD,SAAS,MAAM;AACb,0BAAI,YAAa,MAAK,eAAe,WAAW;AAAA,oBAClD;AAAA,oBACA,UAAU;AAAA,oBAET,0BAAgB,aACZ,mBACC,EAAE,6CAA6C,gBAAgB,IAC/D,EAAE,iDAAiD,gBAAgB,IACpE,mBACC,EAAE,4CAA4C,eAAe,IAC7D,EAAE,+CAA+C,cAAc;AAAA;AAAA,gBACvE;AAAA,iBACF;AAAA;AAAA;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IACC;AAAA,KACH;AAEJ;",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ArrowLeft, Check, CircleAlert, Info, Paperclip, RefreshCw, ShieldCheck, Trash2, TriangleAlert } from 'lucide-react'\nimport { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\nimport { useLocale, useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { localizeDictionaryLabel } from '@open-mercato/core/modules/warranty_claims/lib/dictionaryLabels'\nimport { formatQuantity } from '@open-mercato/core/modules/warranty_claims/lib/quantity'\nimport {\n ATTACHMENT_ACCEPT_TYPES,\n validateAttachmentFile,\n} from '@open-mercato/core/modules/warranty_claims/lib/portalAttachmentValidation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'\n\ntype Props = { params: { orgSlug: string; id: string } }\n\ntype PortalClaimLine = {\n id: string\n lineNo: number\n sku: string | null\n productName: string | null\n serialNumber: string | null\n faultCode: string | null\n faultDescription: string | null\n qtyClaimed: string\n qtyApproved: string | null\n lineStatus: string\n disposition: string | null\n creditAmount: string | null\n}\n\ntype PortalClaim = {\n id: string\n claimNumber: string\n claimType: string\n status: string\n priority: string\n orderId: string | null\n orderNumber: string | null\n reasonCode: string | null\n rejectionReasonCode: string | null\n resolutionSummary: string | null\n submittedAt: string | null\n resolvedAt: string | null\n closedAt: string | null\n createdAt: string | null\n updatedAt: string | null\n lines: PortalClaimLine[]\n}\n\ntype PortalClaimEvent = {\n id: string\n kind: string\n body: string | null\n payload: Record<string, unknown> | null\n actorCustomerId: string | null\n createdAt: string | null\n}\n\ntype PortalAttachment = {\n id: string\n url: string\n downloadUrl: string\n fileName: string\n fileSize: number\n mimeType: string | null\n thumbnailUrl: string\n createdAt: string | null\n}\n\ntype ClaimResponse = { item: PortalClaim }\ntype EventResponse = { items: PortalClaimEvent[] }\ntype AttachmentResponse = { items: PortalAttachment[] }\ntype MutationOkResponse = { ok: boolean; error?: string }\ntype UploadResponse = MutationOkResponse & { item?: PortalAttachment }\ntype ClaimActionResponse = MutationOkResponse & { claimId?: string; status?: string }\ntype PortalOption = { value: string; label: string }\ntype PortalOptionsResponse = { ok: boolean; result?: { reasons: PortalOption[] } }\n\ntype PortalClaimAction = 'submit' | 'withdraw'\n\ntype TrackerStepState = 'complete' | 'current' | 'pending'\n\ntype TrackerStep = {\n id: string\n title: string\n description: string\n state: TrackerStepState\n dateLabel: string | null\n}\n\ntype BannerTone = 'info' | 'success' | 'warning' | 'error'\n\ntype ActivityEntry = {\n id: string\n title: string\n description: string\n createdAt: string | null\n href?: string\n attachment?: PortalAttachment\n}\n\nconst TRACKER_STEP_KEYS = ['received', 'review', 'decision', 'return', 'resolved'] as const\n\nconst STATUS_STEP_INDEX: Record<string, number> = {\n draft: 0,\n submitted: 1,\n in_review: 1,\n info_requested: 1,\n rejected: 2,\n cancelled: 2,\n approved: 3,\n awaiting_return: 3,\n received: 4,\n inspecting: 4,\n resolved: 5,\n closed: 5,\n}\n\nconst LINE_STATUS_DOT_CLASSES: Record<string, string> = {\n pending: 'bg-status-neutral-icon',\n approved: 'bg-status-success-icon',\n rejected: 'bg-status-error-icon',\n received: 'bg-status-info-icon',\n inspected: 'bg-status-info-icon',\n resolved: 'bg-status-success-icon',\n}\n\nconst BANNER_TONE_CLASSES: Record<BannerTone, { wrapper: string; text: string; icon: string }> = {\n info: {\n wrapper: 'bg-status-info-bg',\n text: 'text-status-info-text',\n icon: 'text-status-info-icon',\n },\n success: {\n wrapper: 'bg-status-success-bg',\n text: 'text-status-success-text',\n icon: 'text-status-success-icon',\n },\n warning: {\n wrapper: 'bg-status-warning-bg',\n text: 'text-status-warning-text',\n icon: 'text-status-warning-icon',\n },\n error: {\n wrapper: 'bg-status-error-bg',\n text: 'text-status-error-text',\n icon: 'text-status-error-icon',\n },\n}\n\nfunction bannerTone(status: string): BannerTone {\n if (status === 'rejected' || status === 'cancelled') return 'error'\n if (status === 'info_requested') return 'warning'\n if (status === 'approved' || status === 'resolved' || status === 'closed') return 'success'\n return 'info'\n}\n\nfunction bannerMessage(claim: PortalClaim, t: TranslateFn): string {\n switch (claim.status) {\n case 'draft':\n return t('warranty_claims.portal.tracker.banner.draft')\n case 'info_requested':\n return t('warranty_claims.portal.tracker.banner.infoRequested')\n case 'approved':\n return t('warranty_claims.portal.tracker.banner.approved')\n case 'awaiting_return':\n return t('warranty_claims.portal.tracker.banner.awaitingReturn')\n case 'received':\n case 'inspecting':\n return t('warranty_claims.portal.tracker.banner.goodsFlow')\n case 'resolved':\n case 'closed':\n return claim.resolutionSummary ?? t('warranty_claims.portal.tracker.banner.resolved')\n case 'rejected': {\n const reason = claim.rejectionReasonCode\n ? localizeDictionaryLabel(t, 'rejection', claim.rejectionReasonCode, claim.rejectionReasonCode)\n : null\n const base = t('warranty_claims.portal.detail.terminal.rejected')\n const parts = [base, reason, claim.resolutionSummary].filter((part): part is string => Boolean(part))\n return parts.join(' ')\n }\n case 'cancelled':\n return t('warranty_claims.portal.detail.terminal.cancelled')\n default:\n return t('warranty_claims.portal.tracker.banner.review')\n }\n}\n\nfunction formatShortDate(value: string | null, locale: string): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return date.toLocaleDateString(locale || undefined, { month: 'short', day: 'numeric' })\n}\n\nfunction formatLongDate(value: string | null, locale: string): string | null {\n if (!value) return null\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return null\n return date.toLocaleDateString(locale || undefined, { year: 'numeric', month: 'short', day: 'numeric' })\n}\n\nfunction buildTrackerSteps(claim: PortalClaim, t: TranslateFn, locale: string): TrackerStep[] {\n const currentIndex = STATUS_STEP_INDEX[claim.status] ?? 1\n const terminalComplete = claim.status === 'resolved' || claim.status === 'closed'\n return TRACKER_STEP_KEYS.map((key, index) => {\n const state: TrackerStepState = index < currentIndex || (terminalComplete && index === currentIndex)\n ? 'complete'\n : index === currentIndex\n ? 'current'\n : 'pending'\n let dateLabel: string | null = null\n if (index === 0 && state === 'complete') dateLabel = formatShortDate(claim.submittedAt, locale)\n if (index === TRACKER_STEP_KEYS.length - 1 && state === 'complete') {\n dateLabel = formatShortDate(\n claim.status === 'closed'\n ? claim.closedAt ?? claim.resolvedAt\n : claim.resolvedAt ?? claim.closedAt,\n locale,\n )\n }\n if (state === 'current') dateLabel = t('warranty_claims.portal.tracker.step.now')\n return {\n id: key,\n title: t(`warranty_claims.portal.tracker.step.${key}.title`),\n description: t(`warranty_claims.portal.tracker.step.${key}.description`),\n state,\n dateLabel,\n }\n })\n}\n\nfunction buildClaimTitle(claim: PortalClaim, reasonOptions: PortalOption[], t: TranslateFn): string {\n const typeLabel = t(`warranty_claims.claimType.${claim.claimType}`)\n if (claim.reasonCode) {\n const reasonCode = claim.reasonCode\n const dictionaryLabel = reasonOptions.find((option) => option.value === reasonCode)?.label ?? reasonCode\n const reasonLabel = localizeDictionaryLabel(t, 'reason', reasonCode, dictionaryLabel)\n return t('warranty_claims.portal.tracker.titleWithReason', { reason: reasonLabel, type: typeLabel })\n }\n return t('warranty_claims.portal.tracker.titleFallback', { type: typeLabel })\n}\n\nfunction buildClaimSubtitle(claim: PortalClaim, t: TranslateFn, locale: string): string {\n if (claim.status === 'draft') {\n const created = formatLongDate(claim.createdAt, locale)\n return created\n ? t('warranty_claims.portal.tracker.draftCreated', { date: created })\n : t('warranty_claims.portal.detail.notSubmitted')\n }\n const submitted = formatLongDate(claim.submittedAt, locale) ?? formatLongDate(claim.createdAt, locale)\n if (!submitted) return t('warranty_claims.portal.value.notAvailable')\n if (claim.orderNumber) {\n return t('warranty_claims.portal.tracker.submittedFromOrder', { date: submitted, order: claim.orderNumber })\n }\n return t('warranty_claims.portal.tracker.submittedOn', { date: submitted })\n}\n\nfunction formatEventBody(event: PortalClaimEvent, t: TranslateFn): string {\n if (event.body) return event.body\n const payload = event.payload\n const from = typeof payload?.from === 'string' ? payload.from : null\n const to = typeof payload?.to === 'string' ? payload.to : null\n if (from && to) {\n return t('warranty_claims.portal.event.transition', {\n from: t(`warranty_claims.status.${from}`),\n to: t(`warranty_claims.status.${to}`),\n })\n }\n return t('warranty_claims.portal.event.noDetails')\n}\n\nfunction eventTitle(event: PortalClaimEvent, t: TranslateFn): string {\n if (event.kind === 'comment') {\n return event.actorCustomerId\n ? t('warranty_claims.portal.tracker.event.youReplied')\n : t('warranty_claims.portal.tracker.event.teamReplied')\n }\n return t(`warranty_claims.portal.event.${event.kind}`)\n}\n\nfunction buildActivityEntries(\n events: PortalClaimEvent[],\n attachments: PortalAttachment[],\n t: TranslateFn,\n): ActivityEntry[] {\n const eventEntries: ActivityEntry[] = events.map((event) => ({\n id: `event-${event.id}`,\n title: eventTitle(event, t),\n description: formatEventBody(event, t),\n createdAt: event.createdAt,\n }))\n const attachmentEntries: ActivityEntry[] = attachments.map((attachment) => ({\n id: `attachment-${attachment.id}`,\n title: t('warranty_claims.portal.tracker.event.attachmentAdded'),\n description: attachment.fileName,\n createdAt: attachment.createdAt,\n href: attachment.downloadUrl,\n attachment,\n }))\n return [...eventEntries, ...attachmentEntries].sort((left, right) => {\n const leftTime = left.createdAt ? new Date(left.createdAt).getTime() : 0\n const rightTime = right.createdAt ? new Date(right.createdAt).getTime() : 0\n return rightTime - leftTime\n })\n}\n\nconst DETAIL_MUTATION_CONTEXT_ID = 'warranty_claims.portal.claim.detail'\n\nconst SECTION_CLASS = 'flex w-full flex-col gap-3 border-t border-border px-6 pb-6 pt-5'\nconst SECTION_HEADING_CLASS = 'text-sm font-semibold text-foreground'\nconst DARK_BUTTON_CLASS = 'rounded-md px-3 py-2 text-sm font-medium'\n\nexport default function WarrantyClaimPortalDetailPage({ params }: Props) {\n const t = useT()\n const locale = useLocale()\n const { auth } = usePortalContext()\n const { user, loading } = auth\n const guardedMutation = useGuardedMutation<Record<string, unknown>>({\n contextId: DETAIL_MUTATION_CONTEXT_ID,\n blockedMessage: t('warranty_claims.portal.detail.blocked'),\n })\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [claim, setClaim] = React.useState<PortalClaim | null>(null)\n const [events, setEvents] = React.useState<PortalClaimEvent[]>([])\n const [attachments, setAttachments] = React.useState<PortalAttachment[]>([])\n const [reasonOptions, setReasonOptions] = React.useState<PortalOption[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n const [comment, setComment] = React.useState('')\n const [commentSubmitting, setCommentSubmitting] = React.useState(false)\n const [fileInputKey, setFileInputKey] = React.useState(0)\n const [uploading, setUploading] = React.useState(false)\n const [attachmentMutationId, setAttachmentMutationId] = React.useState<string | null>(null)\n const [claimAction, setClaimAction] = React.useState<PortalClaimAction | null>(null)\n const [actionSubmitting, setActionSubmitting] = React.useState(false)\n const replacementInputRef = React.useRef<HTMLInputElement | null>(null)\n const replacementTargetRef = React.useRef<PortalAttachment | null>(null)\n\n // Leaving an authenticated page for the login page crosses the public/authenticated\n // boundary, so it must be a full page load \u2014 see `replaceWithPageReload`.\n React.useEffect(() => {\n if (!loading && !user) {\n replaceWithPageReload(`/${params.orgSlug}/portal/login`)\n }\n }, [loading, user, params.orgSlug])\n\n const refreshEvents = React.useCallback(async (claimId: string) => {\n const result = await apiCall<EventResponse>(`/api/warranty_claims/portal/events?claimId=${encodeURIComponent(claimId)}`)\n if (result.ok && result.result) setEvents(result.result.items)\n }, [])\n\n const refreshAttachments = React.useCallback(async (claimId: string) => {\n const result = await apiCall<AttachmentResponse>(`/api/warranty_claims/portal/attachments?claimId=${encodeURIComponent(claimId)}`)\n if (result.ok && result.result) setAttachments(result.result.items)\n }, [])\n\n const loadClaim = React.useCallback(async () => {\n setIsLoading(true)\n setError(null)\n setNotFound(false)\n try {\n const detail = await apiCall<ClaimResponse>(`/api/warranty_claims/portal/claims/${encodeURIComponent(params.id)}`)\n if (detail.status === 404) {\n setClaim(null)\n setNotFound(true)\n return\n }\n if (!detail.ok || !detail.result) {\n setError(t('warranty_claims.portal.detail.loadError'))\n return\n }\n setClaim(detail.result.item)\n await Promise.all([\n refreshEvents(detail.result.item.id),\n refreshAttachments(detail.result.item.id),\n ])\n } catch {\n setError(t('warranty_claims.portal.detail.loadError'))\n } finally {\n setIsLoading(false)\n }\n }, [params.id, refreshAttachments, refreshEvents, t])\n\n React.useEffect(() => {\n if (user) void loadClaim()\n }, [loadClaim, user])\n\n React.useEffect(() => {\n if (!user) return\n let cancelled = false\n apiCall<PortalOptionsResponse>('/api/warranty_claims/portal/options')\n .then((result) => {\n if (cancelled) return\n setReasonOptions(result.result?.ok ? result.result.result?.reasons ?? [] : [])\n })\n .catch(() => {})\n return () => {\n cancelled = true\n }\n }, [user])\n\n const submitComment = React.useCallback(async () => {\n if (!claim || commentSubmitting) return\n const body = comment.trim()\n if (!body) return\n setCommentSubmitting(true)\n setError(null)\n try {\n const result = await guardedMutation.runMutation({\n operation: () => apiCall<MutationOkResponse>('/api/warranty_claims/portal/events', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ claimId: claim.id, body }),\n }),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'warranty_claims.claim_event',\n operation: 'portal_comment',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'warranty_claims.claim_event',\n resourceId: claim.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: { claimId: claim.id, body },\n })\n if (!result.ok || !result.result?.ok) {\n setError(t('warranty_claims.portal.detail.commentError'))\n return\n }\n setComment('')\n flash(t('warranty_claims.portal.detail.commentSuccess'), 'success')\n await refreshEvents(claim.id)\n } catch {\n setError(t('warranty_claims.portal.detail.commentError'))\n } finally {\n setCommentSubmitting(false)\n }\n }, [claim, comment, commentSubmitting, guardedMutation, refreshEvents, t])\n\n const uploadAttachment = React.useCallback(async (file: File, replacementTarget?: PortalAttachment) => {\n if (!claim || uploading) return\n const validationError = await validateAttachmentFile(file, t)\n if (validationError) {\n setError(validationError)\n flash(validationError, 'error')\n return\n }\n setUploading(true)\n setAttachmentMutationId(replacementTarget?.id ?? 'new')\n setError(null)\n try {\n const form = new FormData()\n form.set('claimId', claim.id)\n form.set('file', file)\n if (replacementTarget) form.set('attachmentId', replacementTarget.id)\n const result = await guardedMutation.runMutation({\n operation: () => apiCall<UploadResponse>('/api/warranty_claims/portal/attachments', {\n method: replacementTarget ? 'PUT' : 'POST',\n credentials: 'include',\n body: form,\n }),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'attachments.attachment',\n operation: replacementTarget ? 'portal_attachment_replace' : 'portal_attachment_upload',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'attachments.attachment',\n resourceId: replacementTarget?.id ?? claim.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: {\n claimId: claim.id,\n attachmentId: replacementTarget?.id,\n fileName: file.name,\n fileSize: file.size,\n },\n })\n if (!result.ok || !result.result?.ok) {\n setError(replacementTarget\n ? t('warranty_claims.portal.detail.attachmentReplaceError', 'Attachment replacement failed.')\n : t('warranty_claims.portal.detail.attachmentError'))\n return\n }\n flash(\n replacementTarget\n ? t('warranty_claims.portal.detail.attachmentReplaceSuccess', 'Attachment replaced.')\n : t('warranty_claims.portal.detail.attachmentSuccess'),\n 'success',\n )\n await refreshAttachments(claim.id)\n } catch {\n setError(replacementTarget\n ? t('warranty_claims.portal.detail.attachmentReplaceError', 'Attachment replacement failed.')\n : t('warranty_claims.portal.detail.attachmentError'))\n } finally {\n setUploading(false)\n setAttachmentMutationId(null)\n setFileInputKey((current) => current + 1)\n }\n }, [claim, guardedMutation, refreshAttachments, t, uploading])\n\n const deleteAttachment = React.useCallback(async (attachment: PortalAttachment) => {\n if (!claim || uploading) return\n const confirmed = await confirm({\n title: t('warranty_claims.portal.detail.attachmentDelete', 'Delete attachment'),\n text: t(\n 'warranty_claims.portal.detail.attachmentDeleteConfirm',\n 'Delete \"{name}\"? This action cannot be undone.',\n { name: attachment.fileName },\n ),\n confirmText: t('warranty_claims.portal.detail.attachmentDelete', 'Delete attachment'),\n cancelText: t('warranty_claims.portal.detail.dialogCancel', 'Cancel'),\n variant: 'destructive',\n })\n if (!confirmed) return\n setUploading(true)\n setAttachmentMutationId(attachment.id)\n setError(null)\n try {\n const result = await guardedMutation.runMutation({\n operation: () => apiCall<MutationOkResponse>(\n `/api/warranty_claims/portal/attachments?attachmentId=${encodeURIComponent(attachment.id)}`,\n { method: 'DELETE', credentials: 'include' },\n ),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'attachments.attachment',\n operation: 'portal_attachment_delete',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'attachments.attachment',\n resourceId: attachment.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: { claimId: claim.id, attachmentId: attachment.id },\n })\n if (!result.ok || !result.result?.ok) {\n setError(t('warranty_claims.portal.detail.attachmentDeleteError', 'Attachment deletion failed.'))\n return\n }\n flash(t('warranty_claims.portal.detail.attachmentDeleteSuccess', 'Attachment deleted.'), 'success')\n await refreshAttachments(claim.id)\n } catch {\n setError(t('warranty_claims.portal.detail.attachmentDeleteError', 'Attachment deletion failed.'))\n } finally {\n setUploading(false)\n setAttachmentMutationId(null)\n }\n }, [claim, confirm, guardedMutation, refreshAttachments, t, uploading])\n\n const requestAttachmentReplacement = React.useCallback((attachment: PortalAttachment) => {\n if (uploading) return\n replacementTargetRef.current = attachment\n replacementInputRef.current?.click()\n }, [uploading])\n\n const runClaimAction = React.useCallback(async (action: PortalClaimAction) => {\n if (!claim || actionSubmitting) return\n setActionSubmitting(true)\n setError(null)\n try {\n const endpoint = `/api/warranty_claims/portal/claims/${encodeURIComponent(claim.id)}/${action}`\n const result = await guardedMutation.runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(claim.updatedAt),\n () => apiCall<ClaimActionResponse>(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({}),\n }),\n ),\n context: {\n moduleId: 'warranty_claims',\n entityId: 'warranty_claims.claim',\n operation: action === 'submit' ? 'portal_submit' : 'portal_withdraw',\n claimId: claim.id,\n formId: DETAIL_MUTATION_CONTEXT_ID,\n resourceKind: 'warranty_claims.claim',\n resourceId: claim.id,\n retryLastMutation: guardedMutation.retryLastMutation,\n },\n mutationPayload: { claimId: claim.id, action },\n })\n if (!result.ok || !result.result?.ok) {\n const message = result.status === 409\n ? t('warranty_claims.portal.detail.actionConflict', 'This claim changed in the meantime. It has been reloaded \u2014 please try again.')\n : action === 'submit'\n ? t('warranty_claims.portal.detail.submitError', 'The claim could not be submitted.')\n : t('warranty_claims.portal.detail.withdrawError', 'The claim could not be withdrawn.')\n setError(message)\n flash(message, 'error')\n if (result.status === 409) {\n setClaimAction(null)\n await loadClaim()\n }\n return\n }\n setClaimAction(null)\n flash(\n action === 'submit'\n ? t('warranty_claims.portal.detail.submitSuccess', 'Your claim has been submitted.')\n : t('warranty_claims.portal.detail.withdrawSuccess', 'Your claim has been withdrawn.'),\n 'success',\n )\n await loadClaim()\n } catch {\n const message = action === 'submit'\n ? t('warranty_claims.portal.detail.submitError', 'The claim could not be submitted.')\n : t('warranty_claims.portal.detail.withdrawError', 'The claim could not be withdrawn.')\n setError(message)\n flash(message, 'error')\n } finally {\n setActionSubmitting(false)\n }\n }, [actionSubmitting, claim, guardedMutation, loadClaim, t])\n\n const handleSelectedFileChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0] ?? null\n if (!file) return\n void uploadAttachment(file)\n }, [uploadAttachment])\n\n const handleReplacementFileChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {\n const replacementTarget = replacementTargetRef.current\n replacementTargetRef.current = null\n const file = event.target.files?.[0] ?? null\n event.target.value = ''\n if (!replacementTarget || !file) return\n void uploadAttachment(file, replacementTarget)\n }, [uploadAttachment])\n\n const handleCommentSubmit = React.useCallback((event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n void submitComment()\n }, [submitComment])\n\n const handleCommentKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submitComment()\n return\n }\n if (event.key === 'Escape') {\n event.preventDefault()\n setComment('')\n }\n }, [submitComment])\n\n if (loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (!user) return null\n\n if (isLoading) {\n return <LoadingMessage label={t('warranty_claims.portal.detail.loading')} />\n }\n\n if (notFound) {\n return (\n <PortalEmptyState\n icon={<ShieldCheck className=\"size-5\" />}\n title={t('warranty_claims.portal.detail.notFoundTitle')}\n description={t('warranty_claims.portal.detail.notFoundDescription')}\n action={\n <Button asChild>\n <Link href={`/${params.orgSlug}/portal/claims`}>\n <ArrowLeft className=\"size-4\" aria-hidden=\"true\" />\n {t('warranty_claims.portal.detail.back')}\n </Link>\n </Button>\n }\n />\n )\n }\n\n if (!claim) {\n return <ErrorMessage label={error ?? t('warranty_claims.portal.detail.loadError')} />\n }\n\n const trackerSteps = buildTrackerSteps(claim, t, locale)\n const isDraft = claim.status === 'draft'\n const canWithdraw = claim.status === 'draft' || claim.status === 'submitted'\n const tone = bannerTone(claim.status)\n const toneClasses = BANNER_TONE_CLASSES[tone]\n const BannerIcon = tone === 'error' ? CircleAlert : tone === 'warning' ? TriangleAlert : Info\n const activityEntries = buildActivityEntries(events, attachments, t)\n\n return (\n <div className=\"mx-auto flex w-full max-w-3xl flex-col gap-4\">\n <Link\n href={`/${params.orgSlug}/portal/claims`}\n className=\"text-sm font-medium text-muted-foreground transition-colors hover:text-foreground\"\n >\n {'\u2190'} {t('warranty_claims.portal.listTitle')}\n </Link>\n\n {error ? <ErrorMessage label={error} /> : null}\n\n <div className=\"w-full overflow-hidden rounded-lg border border-border bg-card\">\n <div className=\"flex w-full flex-col gap-3 px-6 pb-5 pt-6\">\n <div className=\"flex w-full items-center gap-2\">\n <div className=\"flex items-center gap-2 rounded-sm border border-border bg-background px-3 py-1\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n {t(`warranty_claims.claimType.${claim.claimType}`)}\n </span>\n <span className=\"text-sm font-semibold text-foreground\">\n {claim.claimNumber}\n </span>\n </div>\n <div className=\"min-w-px flex-1\" />\n {isDraft ? (\n <Button\n type=\"button\"\n size=\"sm\"\n className={DARK_BUTTON_CLASS}\n onClick={() => setClaimAction('submit')}\n disabled={actionSubmitting}\n >\n {t('warranty_claims.portal.submit')}\n </Button>\n ) : null}\n {canWithdraw ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"destructive\"\n className=\"rounded-md px-3 py-2 text-sm font-medium\"\n onClick={() => setClaimAction('withdraw')}\n disabled={actionSubmitting}\n >\n {t('warranty_claims.portal.detail.withdraw', 'Withdraw claim')}\n </Button>\n ) : null}\n </div>\n <h1 className=\"text-xl font-bold text-foreground\">\n {buildClaimTitle(claim, reasonOptions, t)}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {buildClaimSubtitle(claim, t, locale)}\n </p>\n </div>\n\n <div className=\"w-full px-6 pb-6\">\n <div className={`flex w-full items-start gap-3 rounded-lg px-4 py-3 ${toneClasses.wrapper}`}>\n <BannerIcon className={`mt-px size-4 shrink-0 ${toneClasses.icon}`} aria-hidden=\"true\" />\n <p className={`min-w-px flex-1 text-sm ${toneClasses.text}`}>\n {bannerMessage(claim, t)}\n </p>\n </div>\n </div>\n\n <div className=\"flex w-full flex-col px-6 pb-6\">\n {trackerSteps.map((step, index) => (\n <div key={step.id} className=\"flex w-full items-stretch gap-4\">\n <div className=\"flex flex-col items-center\">\n {step.state === 'complete' ? (\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full bg-status-success-icon\">\n <Check className=\"size-3 text-status-success-bg\" strokeWidth={2.5} aria-hidden=\"true\" />\n </span>\n ) : step.state === 'current' ? (\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground\">\n {index + 1}\n </span>\n ) : (\n <span className=\"flex size-6 shrink-0 items-center justify-center rounded-full border border-border text-xs font-semibold text-muted-foreground\">\n {index + 1}\n </span>\n )}\n {index < trackerSteps.length - 1 ? (\n <span\n className={`min-h-2 w-px flex-1 ${step.state === 'complete' ? 'bg-status-success-icon' : 'bg-border'}`}\n />\n ) : null}\n </div>\n <div className=\"flex min-w-px flex-1 flex-col gap-1 pb-4\">\n <div className=\"flex w-full items-center gap-2\">\n <p\n className={\n step.state === 'pending'\n ? 'text-sm font-medium text-muted-foreground'\n : 'text-sm font-semibold text-foreground'\n }\n >\n {step.title}\n </p>\n <div className=\"min-w-px flex-1\" />\n {step.dateLabel ? (\n <p className=\"text-xs text-muted-foreground\">{step.dateLabel}</p>\n ) : null}\n </div>\n <p className=\"w-full text-xs text-muted-foreground\">{step.description}</p>\n </div>\n </div>\n ))}\n </div>\n\n <div className={SECTION_CLASS}>\n <p className={SECTION_HEADING_CLASS}>{t('warranty_claims.portal.tracker.items')}</p>\n {claim.lines.length > 0 ? (\n claim.lines.map((line) => (\n <div\n key={line.id}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3\"\n >\n <div className=\"flex min-w-px flex-1 flex-col gap-1\">\n <p className=\"text-sm font-medium text-foreground\">\n {line.productName ?? line.sku ?? t('warranty_claims.portal.value.unnamedLine')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {line.sku\n ? t('warranty_claims.portal.tracker.itemMeta', {\n sku: line.sku,\n qty: formatQuantity(line.qtyClaimed, t('warranty_claims.portal.value.notAvailable')),\n })\n : t('warranty_claims.portal.tracker.itemQty', {\n qty: formatQuantity(line.qtyClaimed, t('warranty_claims.portal.value.notAvailable')),\n })}\n </p>\n </div>\n <div className=\"flex shrink-0 items-center gap-2 rounded-sm border border-border bg-background px-2 py-1\">\n <span\n className={`size-2 shrink-0 rounded-full ${LINE_STATUS_DOT_CLASSES[line.lineStatus] ?? 'bg-status-neutral-icon'}`}\n aria-hidden=\"true\"\n />\n <span className=\"text-xs font-medium text-foreground\">\n {t(`warranty_claims.lineStatus.${line.lineStatus}`)}\n </span>\n </div>\n </div>\n ))\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.portal.detail.linesEmpty.description')}\n </p>\n )}\n </div>\n\n <div className={SECTION_CLASS}>\n <p className={SECTION_HEADING_CLASS}>{t('warranty_claims.portal.tracker.activity')}</p>\n {activityEntries.length > 0 ? (\n activityEntries.map((entry) => (\n <div key={entry.id} className=\"flex w-full items-start gap-3\">\n <span className=\"mt-1 size-2 shrink-0 rounded-full bg-status-neutral-icon\" aria-hidden=\"true\" />\n <div className=\"flex min-w-px flex-1 flex-col gap-px\">\n <div className=\"flex w-full items-start gap-2\">\n <p className=\"text-sm font-medium text-foreground\">{entry.title}</p>\n <div className=\"min-w-px flex-1\" />\n {entry.createdAt ? (\n <p className=\"text-xs text-muted-foreground\">{formatShortDate(entry.createdAt, locale)}</p>\n ) : null}\n </div>\n {entry.href ? (\n <div className=\"flex flex-wrap items-center gap-2\">\n <a\n href={entry.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"min-w-0 whitespace-pre-wrap text-xs text-muted-foreground underline-offset-2 hover:underline\"\n >\n {entry.description}\n </a>\n {entry.attachment ? (\n <div className=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"px-2 text-xs\"\n aria-label={t(\n 'warranty_claims.portal.detail.attachmentReplaceNamed',\n 'Replace {name}',\n { name: entry.attachment.fileName },\n )}\n onClick={() => requestAttachmentReplacement(entry.attachment!)}\n disabled={uploading}\n >\n {attachmentMutationId === entry.attachment.id ? (\n <Spinner className=\"size-3\" />\n ) : (\n <RefreshCw className=\"size-3\" aria-hidden=\"true\" />\n )}\n {t('warranty_claims.portal.detail.attachmentReplace', 'Replace')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"px-2 text-xs text-status-error-text\"\n aria-label={t(\n 'warranty_claims.portal.detail.attachmentDeleteNamed',\n 'Delete {name}',\n { name: entry.attachment.fileName },\n )}\n onClick={() => { void deleteAttachment(entry.attachment!) }}\n disabled={uploading}\n >\n <Trash2 className=\"size-3\" aria-hidden=\"true\" />\n {t('warranty_claims.portal.detail.attachmentDelete', 'Delete attachment')}\n </Button>\n </div>\n ) : null}\n </div>\n ) : (\n <p className=\"whitespace-pre-wrap text-xs text-muted-foreground\">{entry.description}</p>\n )}\n </div>\n </div>\n ))\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('warranty_claims.portal.detail.timelineEmpty.description')}\n </p>\n )}\n <input\n ref={replacementInputRef}\n type=\"file\"\n accept={ATTACHMENT_ACCEPT_TYPES}\n onChange={handleReplacementFileChange}\n disabled={uploading}\n className=\"sr-only\"\n aria-label={t('warranty_claims.portal.detail.attachmentChooseReplacement', 'Choose replacement file')}\n />\n </div>\n\n <div className={SECTION_CLASS}>\n <p className={SECTION_HEADING_CLASS}>{t('warranty_claims.portal.tracker.message')}</p>\n <form id=\"warranty-claim-comment-box\" className=\"flex w-full flex-col gap-3\" onSubmit={handleCommentSubmit}>\n <Textarea\n value={comment}\n onChange={(event) => setComment(event.target.value)}\n onKeyDown={handleCommentKeyDown}\n placeholder={t('warranty_claims.portal.tracker.messagePlaceholder')}\n disabled={commentSubmitting}\n maxLength={8000}\n className=\"h-40 resize-none rounded-lg border-input bg-background px-3 py-2 text-sm shadow-xs placeholder:text-muted-foreground\"\n />\n <div className=\"flex w-full items-center gap-2\">\n <label className={`flex items-center gap-2 ${uploading ? 'cursor-default opacity-70' : 'cursor-pointer'}`}>\n {uploading ? (\n <Spinner className=\"size-4\" />\n ) : (\n <Paperclip className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n )}\n <span className=\"text-xs font-medium text-muted-foreground transition-colors hover:text-foreground\">\n {uploading\n ? t('warranty_claims.portal.detail.uploading')\n : t('warranty_claims.portal.tracker.attachFiles')}\n </span>\n <input\n key={fileInputKey}\n type=\"file\"\n accept={ATTACHMENT_ACCEPT_TYPES}\n onChange={handleSelectedFileChange}\n disabled={uploading}\n className=\"sr-only\"\n />\n </label>\n <div className=\"min-w-px flex-1\" />\n <Button type=\"submit\" size=\"sm\" className={DARK_BUTTON_CLASS} disabled={commentSubmitting || !comment.trim()}>\n {commentSubmitting\n ? t('warranty_claims.portal.tracker.sending')\n : t('warranty_claims.portal.tracker.send')}\n </Button>\n </div>\n </form>\n </div>\n </div>\n\n <Dialog\n open={claimAction !== null}\n onOpenChange={(open) => {\n if (!open && !actionSubmitting) setClaimAction(null)\n }}\n >\n <DialogContent\n className=\"max-w-lg\"\n onKeyDown={(event) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n if (claimAction && !actionSubmitting) void runClaimAction(claimAction)\n }\n if (event.key === 'Escape' && !actionSubmitting) {\n setClaimAction(null)\n }\n }}\n >\n <DialogHeader>\n <DialogTitle>\n {claimAction === 'withdraw'\n ? t('warranty_claims.portal.detail.withdrawDialog.title', 'Withdraw this claim?')\n : t('warranty_claims.portal.detail.submitDialog.title', 'Submit this claim?')}\n </DialogTitle>\n <DialogDescription>\n {claimAction === 'withdraw'\n ? t('warranty_claims.portal.detail.withdrawDialog.description', 'The claim will be cancelled and cannot be reopened. This cannot be undone.')\n : t('warranty_claims.portal.detail.submitDialog.description', 'Your claim will be sent to our support team for review. You can still add comments and photos afterwards.')}\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setClaimAction(null)}\n disabled={actionSubmitting}\n >\n {t('warranty_claims.portal.detail.dialogCancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n variant={claimAction === 'withdraw' ? 'destructive' : 'default'}\n onClick={() => {\n if (claimAction) void runClaimAction(claimAction)\n }}\n disabled={actionSubmitting}\n >\n {claimAction === 'withdraw'\n ? (actionSubmitting\n ? t('warranty_claims.portal.detail.withdrawing', 'Withdrawing...')\n : t('warranty_claims.portal.detail.withdrawConfirm', 'Withdraw claim'))\n : (actionSubmitting\n ? t('warranty_claims.portal.detail.submitting', 'Submitting...')\n : t('warranty_claims.portal.detail.submitConfirm', 'Submit claim'))}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </div>\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.7862.1.ee520f7662",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.6.6"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7862.1.ee520f7662",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7862.1.ee520f7662",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7862.1.ee520f7662",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7862.1.ee520f7662",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7862.1.ee520f7662",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7862.1.ee520f7662",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -15,6 +15,7 @@ import type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entit
|
|
|
15
15
|
import { z } from 'zod'
|
|
16
16
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
17
17
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
18
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
18
19
|
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
19
20
|
import { authorizeAuditReplayWithEntityManager } from '@open-mercato/core/modules/audit_logs/lib/replayAuthorization'
|
|
20
21
|
import {
|
|
@@ -208,7 +209,8 @@ export async function POST(req: Request) {
|
|
|
208
209
|
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })
|
|
209
210
|
}
|
|
210
211
|
logger.error('Redo failed', { err })
|
|
211
|
-
|
|
212
|
+
const { translate } = await resolveTranslations()
|
|
213
|
+
return NextResponse.json({ error: translate('audit_logs.error.redo_failed', 'Redo failed') }, { status: 400 })
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"audit_logs.common.refreshing": "Aktualisiere…",
|
|
43
43
|
"audit_logs.error.load": "Audit-Logs konnten nicht geladen werden",
|
|
44
44
|
"audit_logs.error.redo": "Aktion kann nicht wiederholt werden",
|
|
45
|
+
"audit_logs.error.redo_failed": "Wiederholen fehlgeschlagen",
|
|
45
46
|
"audit_logs.error.undo": "Aktion konnte nicht rückgängig gemacht werden",
|
|
46
47
|
"audit_logs.error.undo_failed": "Rückgängigmachen fehlgeschlagen",
|
|
47
48
|
"audit_logs.filters.undoable_only": "Nur rückgängig machbar",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"audit_logs.common.refreshing": "Refreshing…",
|
|
43
43
|
"audit_logs.error.load": "Failed to load audit logs",
|
|
44
44
|
"audit_logs.error.redo": "Unable to redo action",
|
|
45
|
+
"audit_logs.error.redo_failed": "Redo failed",
|
|
45
46
|
"audit_logs.error.undo": "Unable to undo action",
|
|
46
47
|
"audit_logs.error.undo_failed": "Undo failed",
|
|
47
48
|
"audit_logs.filters.undoable_only": "Undoable only",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"audit_logs.common.refreshing": "Actualizando…",
|
|
43
43
|
"audit_logs.error.load": "No se pudieron cargar los registros",
|
|
44
44
|
"audit_logs.error.redo": "No se pudo rehacer la acción",
|
|
45
|
+
"audit_logs.error.redo_failed": "No se pudo rehacer la acción",
|
|
45
46
|
"audit_logs.error.undo": "No se pudo deshacer la acción",
|
|
46
47
|
"audit_logs.error.undo_failed": "No se pudo deshacer la acción",
|
|
47
48
|
"audit_logs.filters.undoable_only": "Solo deshacibles",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"audit_logs.common.refreshing": "새로고침 중…",
|
|
43
43
|
"audit_logs.error.load": "감사 로그를 불러오지 못했습니다",
|
|
44
44
|
"audit_logs.error.redo": "작업을 다시 실행할 수 없습니다",
|
|
45
|
+
"audit_logs.error.redo_failed": "다시 실행에 실패했습니다",
|
|
45
46
|
"audit_logs.error.undo": "작업을 실행 취소할 수 없습니다",
|
|
46
47
|
"audit_logs.error.undo_failed": "실행 취소에 실패했습니다",
|
|
47
48
|
"audit_logs.filters.undoable_only": "실행 취소 가능한 항목만",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"audit_logs.common.refreshing": "Odświeżanie…",
|
|
43
43
|
"audit_logs.error.load": "Nie udało się załadować logów",
|
|
44
44
|
"audit_logs.error.redo": "Nie udało się przywrócić działania",
|
|
45
|
+
"audit_logs.error.redo_failed": "Nie udało się przywrócić operacji",
|
|
45
46
|
"audit_logs.error.undo": "Nie udało się cofnąć działania",
|
|
46
47
|
"audit_logs.error.undo_failed": "Nie udało się cofnąć operacji",
|
|
47
48
|
"audit_logs.filters.undoable_only": "Tylko do cofnięcia",
|
|
@@ -181,9 +181,6 @@ const CustomerTodosWidget: React.FC<DashboardWidgetComponentProps<CustomerTodoWi
|
|
|
181
181
|
<p className="text-sm font-medium text-foreground">
|
|
182
182
|
{item.todoTitle ?? t('customers.widgets.todos.untitled')}
|
|
183
183
|
</p>
|
|
184
|
-
{item.todoSource ? (
|
|
185
|
-
<p className="text-xs text-muted-foreground">{item.todoSource}</p>
|
|
186
|
-
) : null}
|
|
187
184
|
</div>
|
|
188
185
|
<div className="mt-2 flex flex-wrap gap-3 text-xs">
|
|
189
186
|
{href ? (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
4
5
|
import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
5
6
|
|
|
6
7
|
export type WarrantyWorkspaceTab = {
|
|
@@ -60,7 +61,13 @@ export function WarrantyWorkspace({
|
|
|
60
61
|
</TabsList>
|
|
61
62
|
</Tabs>
|
|
62
63
|
) : null}
|
|
63
|
-
<div className={
|
|
64
|
+
<div className={cn(
|
|
65
|
+
'min-w-0 [&>[data-component-handle]>div:first-child_[data-slot=button]]:h-8 [&>[data-component-handle]>div:first-child_[data-slot=search-input-wrapper]]:h-8 [&_[data-slot=table-header]_[data-slot=table-row]]:h-9 [&_[data-slot=table-body]_[data-slot=table-row]]:h-16',
|
|
66
|
+
'md:[&_[data-slot=table-head][class*=sticky]]:bg-card md:[&_[data-slot=table-cell][class*=sticky]]:bg-card',
|
|
67
|
+
'md:[&_[data-slot=table-head][class*=sticky]]:bg-linear-to-r md:[&_[data-slot=table-head][class*=sticky]]:from-muted/40 md:[&_[data-slot=table-head][class*=sticky]]:to-muted/40',
|
|
68
|
+
'md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:bg-linear-to-r md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:from-muted/30 md:[&_[data-slot=table-row]:hover_[data-slot=table-cell][class*=sticky]]:to-muted/30',
|
|
69
|
+
contentClassName,
|
|
70
|
+
)}>{children}</div>
|
|
64
71
|
</section>
|
|
65
72
|
)
|
|
66
73
|
}
|
|
@@ -93,6 +93,8 @@ type AttachmentResponse = { items: PortalAttachment[] }
|
|
|
93
93
|
type MutationOkResponse = { ok: boolean; error?: string }
|
|
94
94
|
type UploadResponse = MutationOkResponse & { item?: PortalAttachment }
|
|
95
95
|
type ClaimActionResponse = MutationOkResponse & { claimId?: string; status?: string }
|
|
96
|
+
type PortalOption = { value: string; label: string }
|
|
97
|
+
type PortalOptionsResponse = { ok: boolean; result?: { reasons: PortalOption[] } }
|
|
96
98
|
|
|
97
99
|
type PortalClaimAction = 'submit' | 'withdraw'
|
|
98
100
|
|
|
@@ -248,10 +250,12 @@ function buildTrackerSteps(claim: PortalClaim, t: TranslateFn, locale: string):
|
|
|
248
250
|
})
|
|
249
251
|
}
|
|
250
252
|
|
|
251
|
-
function buildClaimTitle(claim: PortalClaim, t: TranslateFn): string {
|
|
253
|
+
function buildClaimTitle(claim: PortalClaim, reasonOptions: PortalOption[], t: TranslateFn): string {
|
|
252
254
|
const typeLabel = t(`warranty_claims.claimType.${claim.claimType}`)
|
|
253
255
|
if (claim.reasonCode) {
|
|
254
|
-
const
|
|
256
|
+
const reasonCode = claim.reasonCode
|
|
257
|
+
const dictionaryLabel = reasonOptions.find((option) => option.value === reasonCode)?.label ?? reasonCode
|
|
258
|
+
const reasonLabel = localizeDictionaryLabel(t, 'reason', reasonCode, dictionaryLabel)
|
|
255
259
|
return t('warranty_claims.portal.tracker.titleWithReason', { reason: reasonLabel, type: typeLabel })
|
|
256
260
|
}
|
|
257
261
|
return t('warranty_claims.portal.tracker.titleFallback', { type: typeLabel })
|
|
@@ -340,6 +344,7 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
340
344
|
const [claim, setClaim] = React.useState<PortalClaim | null>(null)
|
|
341
345
|
const [events, setEvents] = React.useState<PortalClaimEvent[]>([])
|
|
342
346
|
const [attachments, setAttachments] = React.useState<PortalAttachment[]>([])
|
|
347
|
+
const [reasonOptions, setReasonOptions] = React.useState<PortalOption[]>([])
|
|
343
348
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
344
349
|
const [error, setError] = React.useState<string | null>(null)
|
|
345
350
|
const [notFound, setNotFound] = React.useState(false)
|
|
@@ -402,6 +407,20 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
402
407
|
if (user) void loadClaim()
|
|
403
408
|
}, [loadClaim, user])
|
|
404
409
|
|
|
410
|
+
React.useEffect(() => {
|
|
411
|
+
if (!user) return
|
|
412
|
+
let cancelled = false
|
|
413
|
+
apiCall<PortalOptionsResponse>('/api/warranty_claims/portal/options')
|
|
414
|
+
.then((result) => {
|
|
415
|
+
if (cancelled) return
|
|
416
|
+
setReasonOptions(result.result?.ok ? result.result.result?.reasons ?? [] : [])
|
|
417
|
+
})
|
|
418
|
+
.catch(() => {})
|
|
419
|
+
return () => {
|
|
420
|
+
cancelled = true
|
|
421
|
+
}
|
|
422
|
+
}, [user])
|
|
423
|
+
|
|
405
424
|
const submitComment = React.useCallback(async () => {
|
|
406
425
|
if (!claim || commentSubmitting) return
|
|
407
426
|
const body = comment.trim()
|
|
@@ -741,7 +760,7 @@ export default function WarrantyClaimPortalDetailPage({ params }: Props) {
|
|
|
741
760
|
) : null}
|
|
742
761
|
</div>
|
|
743
762
|
<h1 className="text-xl font-bold text-foreground">
|
|
744
|
-
{buildClaimTitle(claim, t)}
|
|
763
|
+
{buildClaimTitle(claim, reasonOptions, t)}
|
|
745
764
|
</h1>
|
|
746
765
|
<p className="text-sm text-muted-foreground">
|
|
747
766
|
{buildClaimSubtitle(claim, t, locale)}
|