@open-mercato/core 0.6.8-develop.6998.1.d2bc46c56c → 0.6.8-develop.7001.1.03608ab252
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 +2 -2
- package/dist/generated/entities/warranty_claim/index.js +105 -0
- package/dist/generated/entities/warranty_claim/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_event/index.js +25 -0
- package/dist/generated/entities/warranty_claim_event/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_line/index.js +77 -0
- package/dist/generated/entities/warranty_claim_line/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_registration/index.js +43 -0
- package/dist/generated/entities/warranty_claim_registration/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_sequence/index.js +17 -0
- package/dist/generated/entities/warranty_claim_sequence/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_settings/index.js +43 -0
- package/dist/generated/entities/warranty_claim_settings/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_sla_signal/index.js +25 -0
- package/dist/generated/entities/warranty_claim_sla_signal/index.js.map +7 -0
- package/dist/generated/entities/warranty_troubleshooting_guide/index.js +25 -0
- package/dist/generated/entities/warranty_troubleshooting_guide/index.js.map +7 -0
- package/dist/generated/entities/warranty_vendor_policy/index.js +31 -0
- package/dist/generated/entities/warranty_vendor_policy/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +12 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +200 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/api.js +3 -2
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +13 -6
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/modules/attachments/di.js +18 -0
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/fields/attachment.js +186 -26
- package/dist/modules/attachments/fields/attachment.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +202 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +7 -0
- package/dist/modules/attachments/lib/target-access-service.js +39 -0
- package/dist/modules/attachments/lib/target-access-service.js.map +7 -0
- package/dist/modules/auth/api/logout.js +37 -10
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/reset/confirm.js +7 -0
- package/dist/modules/auth/api/reset/confirm.js.map +2 -2
- package/dist/modules/auth/api/reset.js +7 -0
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/commands/users.js +13 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/events.js +15 -0
- package/dist/modules/auth/events.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +1 -0
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +6 -69
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/lib/assignableStaff.js +76 -0
- package/dist/modules/customers/lib/assignableStaff.js.map +7 -0
- package/dist/modules/data_sync/lib/batch-stream.js +56 -0
- package/dist/modules/data_sync/lib/batch-stream.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +165 -103
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +4 -0
- package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/extract/route.js +4 -0
- package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/webhook/inbound.js +4 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -8
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/warranty_claims/acl.js +75 -0
- package/dist/modules/warranty_claims/acl.js.map +7 -0
- package/dist/modules/warranty_claims/ai-agents.js +161 -0
- package/dist/modules/warranty_claims/ai-agents.js.map +7 -0
- package/dist/modules/warranty_claims/ai-tools.js +591 -0
- package/dist/modules/warranty_claims/ai-tools.js.map +7 -0
- package/dist/modules/warranty_claims/api/ai/assess/route.js +304 -0
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +127 -0
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/ai/suggest/route.js +177 -0
- package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/assign/route.js +124 -0
- package/dist/modules/warranty_claims/api/assign/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/assignees/route.js +97 -0
- package/dist/modules/warranty_claims/api/assignees/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/credit-memo/route.js +152 -0
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/entitlement/route.js +145 -0
- package/dist/modules/warranty_claims/api/entitlement/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/events/route.js +199 -0
- package/dist/modules/warranty_claims/api/events/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/external/claims/route.js +446 -0
- package/dist/modules/warranty_claims/api/external/claims/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/lines/route.js +258 -0
- package/dist/modules/warranty_claims/api/lines/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/openapi.js +27 -0
- package/dist/modules/warranty_claims/api/openapi.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/attachments/route.js +490 -0
- package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js +178 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js +101 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +78 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +81 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/route.js +462 -0
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/events/route.js +228 -0
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/options/route.js +91 -0
- package/dist/modules/warranty_claims/api/portal/options/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/orders/lines/route.js +172 -0
- package/dist/modules/warranty_claims/api/portal/orders/lines/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/orders/route.js +151 -0
- package/dist/modules/warranty_claims/api/portal/orders/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js +121 -0
- package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/receiving/route.js +176 -0
- package/dist/modules/warranty_claims/api/receiving/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/registrations/route.js +382 -0
- package/dist/modules/warranty_claims/api/registrations/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/replacement-order/route.js +150 -0
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/return-label/route.js +235 -0
- package/dist/modules/warranty_claims/api/return-label/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/risk/route.js +101 -0
- package/dist/modules/warranty_claims/api/risk/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/route.js +482 -0
- package/dist/modules/warranty_claims/api/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/sales-return/route.js +151 -0
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/settings-general/route.js +195 -0
- package/dist/modules/warranty_claims/api/settings-general/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/stats/route.js +199 -0
- package/dist/modules/warranty_claims/api/stats/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/submit/route.js +120 -0
- package/dist/modules/warranty_claims/api/submit/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/transition/route.js +126 -0
- package/dist/modules/warranty_claims/api/transition/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js +261 -0
- package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/vendor-policies/route.js +311 -0
- package/dist/modules/warranty_claims/api/vendor-policies/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js +130 -0
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +153 -0
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js +268 -0
- package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js +54 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js +87 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js +120 -0
- package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js +135 -0
- package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js +218 -0
- package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js +231 -0
- package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js +82 -0
- package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js +136 -0
- package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js +41 -0
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js +45 -0
- package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/claimSla.js +92 -0
- package/dist/modules/warranty_claims/backend/components/claimSla.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/orderLookup.js +81 -0
- package/dist/modules/warranty_claims/backend/components/orderLookup.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/productLookup.js +317 -0
- package/dist/modules/warranty_claims/backend/components/productLookup.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js +61 -0
- package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js.map +7 -0
- package/dist/modules/warranty_claims/backend/page.js +1088 -0
- package/dist/modules/warranty_claims/backend/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js +427 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +2101 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js +1249 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js +110 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js +17 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js +49 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js +17 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +485 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js +253 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +1565 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js +472 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js +115 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js +54 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +414 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js +21 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js +171 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js +111 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js +48 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +429 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js +21 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js +182 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js.map +7 -0
- package/dist/modules/warranty_claims/ce.js +48 -0
- package/dist/modules/warranty_claims/ce.js.map +7 -0
- package/dist/modules/warranty_claims/commands/claim-lines.js +911 -0
- package/dist/modules/warranty_claims/commands/claim-lines.js.map +7 -0
- package/dist/modules/warranty_claims/commands/claims.js +2577 -0
- package/dist/modules/warranty_claims/commands/claims.js.map +7 -0
- package/dist/modules/warranty_claims/commands/index.js +4 -0
- package/dist/modules/warranty_claims/commands/index.js.map +7 -0
- package/dist/modules/warranty_claims/commands/settings.js +169 -0
- package/dist/modules/warranty_claims/commands/settings.js.map +7 -0
- package/dist/modules/warranty_claims/commands/shared.js +162 -0
- package/dist/modules/warranty_claims/commands/shared.js.map +7 -0
- package/dist/modules/warranty_claims/data/constants.js +35 -0
- package/dist/modules/warranty_claims/data/constants.js.map +7 -0
- package/dist/modules/warranty_claims/data/enrichers.js +97 -0
- package/dist/modules/warranty_claims/data/enrichers.js.map +7 -0
- package/dist/modules/warranty_claims/data/entities.js +731 -0
- package/dist/modules/warranty_claims/data/entities.js.map +7 -0
- package/dist/modules/warranty_claims/data/validators.js +488 -0
- package/dist/modules/warranty_claims/data/validators.js.map +7 -0
- package/dist/modules/warranty_claims/di.js +29 -0
- package/dist/modules/warranty_claims/di.js.map +7 -0
- package/dist/modules/warranty_claims/encryption.js +28 -0
- package/dist/modules/warranty_claims/encryption.js.map +7 -0
- package/dist/modules/warranty_claims/events.js +81 -0
- package/dist/modules/warranty_claims/events.js.map +7 -0
- package/dist/modules/warranty_claims/extension-points.js +19 -0
- package/dist/modules/warranty_claims/extension-points.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +825 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js +11 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +1139 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js +11 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +253 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js +18 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/index.js +15 -0
- package/dist/modules/warranty_claims/index.js.map +7 -0
- package/dist/modules/warranty_claims/lib/aiAssist.js +496 -0
- package/dist/modules/warranty_claims/lib/aiAssist.js.map +7 -0
- package/dist/modules/warranty_claims/lib/apiIdsFilter.js +11 -0
- package/dist/modules/warranty_claims/lib/apiIdsFilter.js.map +7 -0
- package/dist/modules/warranty_claims/lib/assigneeNames.js +153 -0
- package/dist/modules/warranty_claims/lib/assigneeNames.js.map +7 -0
- package/dist/modules/warranty_claims/lib/attachmentVisibility.js +9 -0
- package/dist/modules/warranty_claims/lib/attachmentVisibility.js.map +7 -0
- package/dist/modules/warranty_claims/lib/bulkFeedback.js +8 -0
- package/dist/modules/warranty_claims/lib/bulkFeedback.js.map +7 -0
- package/dist/modules/warranty_claims/lib/businessHours.js +250 -0
- package/dist/modules/warranty_claims/lib/businessHours.js.map +7 -0
- package/dist/modules/warranty_claims/lib/businessHoursForm.js +146 -0
- package/dist/modules/warranty_claims/lib/businessHoursForm.js.map +7 -0
- package/dist/modules/warranty_claims/lib/claimTypeConfig.js +39 -0
- package/dist/modules/warranty_claims/lib/claimTypeConfig.js.map +7 -0
- package/dist/modules/warranty_claims/lib/decryptionSafety.js +17 -0
- package/dist/modules/warranty_claims/lib/decryptionSafety.js.map +7 -0
- package/dist/modules/warranty_claims/lib/deskFilters.js +88 -0
- package/dist/modules/warranty_claims/lib/deskFilters.js.map +7 -0
- package/dist/modules/warranty_claims/lib/dictionaries.js +157 -0
- package/dist/modules/warranty_claims/lib/dictionaries.js.map +7 -0
- package/dist/modules/warranty_claims/lib/dictionaryLabels.js +12 -0
- package/dist/modules/warranty_claims/lib/dictionaryLabels.js.map +7 -0
- package/dist/modules/warranty_claims/lib/emailIntake.js +87 -0
- package/dist/modules/warranty_claims/lib/emailIntake.js.map +7 -0
- package/dist/modules/warranty_claims/lib/escalation.js +58 -0
- package/dist/modules/warranty_claims/lib/escalation.js.map +7 -0
- package/dist/modules/warranty_claims/lib/externalIntake.js +233 -0
- package/dist/modules/warranty_claims/lib/externalIntake.js.map +7 -0
- package/dist/modules/warranty_claims/lib/grading.js +21 -0
- package/dist/modules/warranty_claims/lib/grading.js.map +7 -0
- package/dist/modules/warranty_claims/lib/listSegments.js +18 -0
- package/dist/modules/warranty_claims/lib/listSegments.js.map +7 -0
- package/dist/modules/warranty_claims/lib/orderOwnership.js +25 -0
- package/dist/modules/warranty_claims/lib/orderOwnership.js.map +7 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js +11 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js.map +7 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js +108 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/warranty_claims/lib/portalClaimAccess.js +26 -0
- package/dist/modules/warranty_claims/lib/portalClaimAccess.js.map +7 -0
- package/dist/modules/warranty_claims/lib/quantity.js +19 -0
- package/dist/modules/warranty_claims/lib/quantity.js.map +7 -0
- package/dist/modules/warranty_claims/lib/risk.js +204 -0
- package/dist/modules/warranty_claims/lib/risk.js.map +7 -0
- package/dist/modules/warranty_claims/lib/settings.js +55 -0
- package/dist/modules/warranty_claims/lib/settings.js.map +7 -0
- package/dist/modules/warranty_claims/lib/stateMachine.js +88 -0
- package/dist/modules/warranty_claims/lib/stateMachine.js.map +7 -0
- package/dist/modules/warranty_claims/lib/timelineFilters.js +16 -0
- package/dist/modules/warranty_claims/lib/timelineFilters.js.map +7 -0
- package/dist/modules/warranty_claims/lib/triage.js +241 -0
- package/dist/modules/warranty_claims/lib/triage.js.map +7 -0
- package/dist/modules/warranty_claims/lib/troubleshooting.js +111 -0
- package/dist/modules/warranty_claims/lib/troubleshooting.js.map +7 -0
- package/dist/modules/warranty_claims/lib/tryResolve.js +11 -0
- package/dist/modules/warranty_claims/lib/tryResolve.js.map +7 -0
- package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js +101 -0
- package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js.map +7 -0
- package/dist/modules/warranty_claims/lib/warrantyPreview.js +18 -0
- package/dist/modules/warranty_claims/lib/warrantyPreview.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js +32 -0
- package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js +16 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js +17 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js +29 -0
- package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js +25 -0
- package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js +13 -0
- package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js +15 -0
- package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js +13 -0
- package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js +13 -0
- package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js +12 -0
- package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/notifications.client.js +67 -0
- package/dist/modules/warranty_claims/notifications.client.js.map +7 -0
- package/dist/modules/warranty_claims/notifications.js +103 -0
- package/dist/modules/warranty_claims/notifications.js.map +7 -0
- package/dist/modules/warranty_claims/search.js +174 -0
- package/dist/modules/warranty_claims/search.js.map +7 -0
- package/dist/modules/warranty_claims/services/adjudicationEvaluator.js +202 -0
- package/dist/modules/warranty_claims/services/adjudicationEvaluator.js.map +7 -0
- package/dist/modules/warranty_claims/services/claimNumberGenerator.js +58 -0
- package/dist/modules/warranty_claims/services/claimNumberGenerator.js.map +7 -0
- package/dist/modules/warranty_claims/services/entitlementResolver.js +69 -0
- package/dist/modules/warranty_claims/services/entitlementResolver.js.map +7 -0
- package/dist/modules/warranty_claims/services/returnLabelProvider.js +14 -0
- package/dist/modules/warranty_claims/services/returnLabelProvider.js.map +7 -0
- package/dist/modules/warranty_claims/setup.js +121 -0
- package/dist/modules/warranty_claims/setup.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js +119 -0
- package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js +47 -0
- package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js +60 -0
- package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-status-notification.js +83 -0
- package/dist/modules/warranty_claims/subscribers/claim-status-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js +46 -0
- package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/email-to-claim.js +135 -0
- package/dist/modules/warranty_claims/subscribers/email-to-claim.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +164 -0
- package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js +24 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js +26 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js +15 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js +197 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js +28 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js +418 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js +18 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js +17 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js +146 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js +17 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js +147 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection-table.js +38 -0
- package/dist/modules/warranty_claims/widgets/injection-table.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js +223 -0
- package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/notifications/index.js +5 -0
- package/dist/modules/warranty_claims/widgets/notifications/index.js.map +7 -0
- package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js +334 -0
- package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js.map +7 -0
- package/generated/entities/warranty_claim/index.ts +51 -0
- package/generated/entities/warranty_claim_event/index.ts +11 -0
- package/generated/entities/warranty_claim_line/index.ts +37 -0
- package/generated/entities/warranty_claim_registration/index.ts +20 -0
- package/generated/entities/warranty_claim_sequence/index.ts +7 -0
- package/generated/entities/warranty_claim_settings/index.ts +20 -0
- package/generated/entities/warranty_claim_sla_signal/index.ts +11 -0
- package/generated/entities/warranty_troubleshooting_guide/index.ts +11 -0
- package/generated/entities/warranty_vendor_policy/index.ts +14 -0
- package/generated/entities.ids.generated.ts +12 -0
- package/generated/entity-fields-registry.ts +200 -0
- package/package.json +7 -7
- package/src/helpers/integration/api.ts +10 -3
- package/src/helpers/integration/salesFixtures.ts +17 -7
- package/src/modules/attachments/di.ts +30 -1
- package/src/modules/attachments/fields/attachment.tsx +215 -23
- package/src/modules/attachments/i18n/de.json +5 -0
- package/src/modules/attachments/i18n/en.json +5 -0
- package/src/modules/attachments/i18n/es.json +5 -0
- package/src/modules/attachments/i18n/ko.json +5 -0
- package/src/modules/attachments/i18n/pl.json +5 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +257 -0
- package/src/modules/attachments/lib/target-access-service.ts +57 -0
- package/src/modules/auth/api/logout.ts +46 -10
- package/src/modules/auth/api/reset/confirm.ts +7 -0
- package/src/modules/auth/api/reset.ts +7 -0
- package/src/modules/auth/commands/users.ts +22 -0
- package/src/modules/auth/events.ts +15 -0
- package/src/modules/customers/api/companies/route.ts +1 -0
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +4 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +4 -2
- package/src/modules/customers/components/detail/assignableStaff.ts +14 -177
- package/src/modules/customers/lib/assignableStaff.ts +177 -0
- package/src/modules/data_sync/AGENTS.md +3 -0
- package/src/modules/data_sync/lib/batch-stream.ts +117 -0
- package/src/modules/data_sync/lib/sync-engine.ts +190 -115
- package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +4 -0
- package/src/modules/inbox_ops/api/extract/route.ts +4 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +4 -0
- package/src/modules/sales/commands/documents.ts +16 -8
- package/src/modules/warranty_claims/AGENTS.md +49 -0
- package/src/modules/warranty_claims/acl.ts +71 -0
- package/src/modules/warranty_claims/ai-agents.ts +189 -0
- package/src/modules/warranty_claims/ai-tools.ts +711 -0
- package/src/modules/warranty_claims/api/ai/assess/route.ts +371 -0
- package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +149 -0
- package/src/modules/warranty_claims/api/ai/suggest/route.ts +190 -0
- package/src/modules/warranty_claims/api/assign/route.ts +142 -0
- package/src/modules/warranty_claims/api/assignees/route.ts +102 -0
- package/src/modules/warranty_claims/api/credit-memo/route.ts +191 -0
- package/src/modules/warranty_claims/api/entitlement/route.ts +224 -0
- package/src/modules/warranty_claims/api/events/route.ts +226 -0
- package/src/modules/warranty_claims/api/external/claims/route.ts +528 -0
- package/src/modules/warranty_claims/api/lines/route.ts +275 -0
- package/src/modules/warranty_claims/api/openapi.ts +28 -0
- package/src/modules/warranty_claims/api/portal/attachments/route.ts +547 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/route.ts +195 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/shared.ts +125 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +82 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +87 -0
- package/src/modules/warranty_claims/api/portal/claims/route.ts +579 -0
- package/src/modules/warranty_claims/api/portal/events/route.ts +260 -0
- package/src/modules/warranty_claims/api/portal/options/route.ts +113 -0
- package/src/modules/warranty_claims/api/portal/orders/lines/route.ts +258 -0
- package/src/modules/warranty_claims/api/portal/orders/route.ts +206 -0
- package/src/modules/warranty_claims/api/portal/troubleshooting/route.ts +151 -0
- package/src/modules/warranty_claims/api/receiving/route.ts +215 -0
- package/src/modules/warranty_claims/api/registrations/route.ts +417 -0
- package/src/modules/warranty_claims/api/replacement-order/route.ts +188 -0
- package/src/modules/warranty_claims/api/return-label/route.ts +291 -0
- package/src/modules/warranty_claims/api/risk/route.ts +112 -0
- package/src/modules/warranty_claims/api/route.ts +543 -0
- package/src/modules/warranty_claims/api/sales-return/route.ts +183 -0
- package/src/modules/warranty_claims/api/settings-general/route.ts +230 -0
- package/src/modules/warranty_claims/api/stats/route.ts +306 -0
- package/src/modules/warranty_claims/api/submit/route.ts +146 -0
- package/src/modules/warranty_claims/api/transition/route.ts +144 -0
- package/src/modules/warranty_claims/api/troubleshooting-guides/route.ts +295 -0
- package/src/modules/warranty_claims/api/vendor-policies/route.ts +351 -0
- package/src/modules/warranty_claims/api/vendor-recovery/route.ts +148 -0
- package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +167 -0
- package/src/modules/warranty_claims/backend/components/AiAssessButtons.tsx +389 -0
- package/src/modules/warranty_claims/backend/components/ClaimStageProgress.tsx +65 -0
- package/src/modules/warranty_claims/backend/components/ClaimStatusBadge.tsx +137 -0
- package/src/modules/warranty_claims/backend/components/ClaimsKpiStrip.tsx +169 -0
- package/src/modules/warranty_claims/backend/components/EntitlementLookupBadge.tsx +203 -0
- package/src/modules/warranty_claims/backend/components/ReceivingPanel.tsx +300 -0
- package/src/modules/warranty_claims/backend/components/ReturnLabelPanel.tsx +292 -0
- package/src/modules/warranty_claims/backend/components/TroubleshootingWalker.tsx +125 -0
- package/src/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.tsx +217 -0
- package/src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx +66 -0
- package/src/modules/warranty_claims/backend/components/claimReasonOptions.ts +57 -0
- package/src/modules/warranty_claims/backend/components/claimSla.tsx +127 -0
- package/src/modules/warranty_claims/backend/components/orderLookup.ts +109 -0
- package/src/modules/warranty_claims/backend/components/productLookup.tsx +433 -0
- package/src/modules/warranty_claims/backend/components/useUserDisplayNames.ts +66 -0
- package/src/modules/warranty_claims/backend/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/page.tsx +1302 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.tsx +531 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +2599 -0
- package/src/modules/warranty_claims/backend/warranty_claims/create/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/warranty_claims/create/page.tsx +1533 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.ts +13 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.tsx +136 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.ts +13 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.tsx +52 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +555 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.tsx +295 -0
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +1903 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.tsx +550 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.tsx +141 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.tsx +57 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.ts +17 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +477 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.tsx +214 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.tsx +137 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.tsx +51 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.ts +17 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +506 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.tsx +220 -0
- package/src/modules/warranty_claims/ce.ts +44 -0
- package/src/modules/warranty_claims/commands/claim-lines.ts +1082 -0
- package/src/modules/warranty_claims/commands/claims.ts +3364 -0
- package/src/modules/warranty_claims/commands/index.ts +3 -0
- package/src/modules/warranty_claims/commands/settings.ts +198 -0
- package/src/modules/warranty_claims/commands/shared.ts +212 -0
- package/src/modules/warranty_claims/data/constants.ts +37 -0
- package/src/modules/warranty_claims/data/enrichers.ts +165 -0
- package/src/modules/warranty_claims/data/entities.ts +663 -0
- package/src/modules/warranty_claims/data/validators.ts +634 -0
- package/src/modules/warranty_claims/di.ts +34 -0
- package/src/modules/warranty_claims/encryption.ts +26 -0
- package/src/modules/warranty_claims/events.ts +81 -0
- package/src/modules/warranty_claims/extension-points.ts +16 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.ts +9 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +1037 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.ts +9 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +1409 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.ts +16 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +304 -0
- package/src/modules/warranty_claims/i18n/de.json +1270 -0
- package/src/modules/warranty_claims/i18n/en.json +1270 -0
- package/src/modules/warranty_claims/i18n/es.json +1270 -0
- package/src/modules/warranty_claims/i18n/ko.json +1270 -0
- package/src/modules/warranty_claims/i18n/pl.json +1270 -0
- package/src/modules/warranty_claims/index.ts +13 -0
- package/src/modules/warranty_claims/lib/aiAssist.ts +677 -0
- package/src/modules/warranty_claims/lib/apiIdsFilter.ts +12 -0
- package/src/modules/warranty_claims/lib/assigneeNames.ts +184 -0
- package/src/modules/warranty_claims/lib/attachmentVisibility.ts +5 -0
- package/src/modules/warranty_claims/lib/bulkFeedback.ts +6 -0
- package/src/modules/warranty_claims/lib/businessHours.ts +333 -0
- package/src/modules/warranty_claims/lib/businessHoursForm.ts +206 -0
- package/src/modules/warranty_claims/lib/claimTypeConfig.ts +49 -0
- package/src/modules/warranty_claims/lib/decryptionSafety.ts +19 -0
- package/src/modules/warranty_claims/lib/deskFilters.ts +131 -0
- package/src/modules/warranty_claims/lib/dictionaries.ts +212 -0
- package/src/modules/warranty_claims/lib/dictionaryLabels.ts +16 -0
- package/src/modules/warranty_claims/lib/emailIntake.ts +123 -0
- package/src/modules/warranty_claims/lib/escalation.ts +83 -0
- package/src/modules/warranty_claims/lib/externalIntake.ts +362 -0
- package/src/modules/warranty_claims/lib/grading.ts +23 -0
- package/src/modules/warranty_claims/lib/listSegments.ts +19 -0
- package/src/modules/warranty_claims/lib/orderOwnership.ts +58 -0
- package/src/modules/warranty_claims/lib/portalAttachmentUpload.ts +11 -0
- package/src/modules/warranty_claims/lib/portalAttachmentValidation.ts +115 -0
- package/src/modules/warranty_claims/lib/portalClaimAccess.ts +37 -0
- package/src/modules/warranty_claims/lib/quantity.ts +15 -0
- package/src/modules/warranty_claims/lib/risk.ts +371 -0
- package/src/modules/warranty_claims/lib/settings.ts +76 -0
- package/src/modules/warranty_claims/lib/stateMachine.ts +130 -0
- package/src/modules/warranty_claims/lib/timelineFilters.ts +22 -0
- package/src/modules/warranty_claims/lib/triage.ts +366 -0
- package/src/modules/warranty_claims/lib/troubleshooting.ts +157 -0
- package/src/modules/warranty_claims/lib/tryResolve.ts +7 -0
- package/src/modules/warranty_claims/lib/vendorPolicyRecovery.ts +182 -0
- package/src/modules/warranty_claims/lib/warrantyPreview.ts +20 -0
- package/src/modules/warranty_claims/migrations/.snapshot-open-mercato.json +3525 -0
- package/src/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.ts +38 -0
- package/src/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.ts +17 -0
- package/src/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.ts +17 -0
- package/src/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.ts +37 -0
- package/src/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.ts +25 -0
- package/src/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.ts +13 -0
- package/src/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.ts +15 -0
- package/src/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.ts +13 -0
- package/src/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.ts +13 -0
- package/src/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.ts +11 -0
- package/src/modules/warranty_claims/notifications.client.ts +78 -0
- package/src/modules/warranty_claims/notifications.ts +101 -0
- package/src/modules/warranty_claims/search.ts +199 -0
- package/src/modules/warranty_claims/services/adjudicationEvaluator.ts +344 -0
- package/src/modules/warranty_claims/services/claimNumberGenerator.ts +69 -0
- package/src/modules/warranty_claims/services/entitlementResolver.ts +102 -0
- package/src/modules/warranty_claims/services/returnLabelProvider.ts +30 -0
- package/src/modules/warranty_claims/setup.ts +154 -0
- package/src/modules/warranty_claims/subscribers/auto-vendor-recovery.ts +140 -0
- package/src/modules/warranty_claims/subscribers/claim-assigned-notification.ts +54 -0
- package/src/modules/warranty_claims/subscribers/claim-customer-reply-notification.ts +68 -0
- package/src/modules/warranty_claims/subscribers/claim-status-notification.ts +100 -0
- package/src/modules/warranty_claims/subscribers/claim-submitted-notification.ts +53 -0
- package/src/modules/warranty_claims/subscribers/email-to-claim.ts +175 -0
- package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +226 -0
- package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.ts +28 -0
- package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.ts +31 -0
- package/src/modules/warranty_claims/widgets/dashboard/claims-queue/config.ts +15 -0
- package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.tsx +237 -0
- package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.ts +27 -0
- package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.tsx +527 -0
- package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.ts +37 -0
- package/src/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.ts +21 -0
- package/src/modules/warranty_claims/widgets/injection/customer-claims-tab.tsx +210 -0
- package/src/modules/warranty_claims/widgets/injection/order-claims-tab/widget.ts +21 -0
- package/src/modules/warranty_claims/widgets/injection/order-claims-tab.tsx +210 -0
- package/src/modules/warranty_claims/widgets/injection-table.ts +36 -0
- package/src/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.tsx +253 -0
- package/src/modules/warranty_claims/widgets/notifications/index.ts +1 -0
- package/src/modules/warranty_claims/workers/sla-escalation-sweep.ts +450 -0
|
@@ -0,0 +1,2599 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { useRouter } from 'next/navigation'
|
|
6
|
+
import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
|
|
7
|
+
import { Copy, Hash, Info, MessageSquare, Pencil, RefreshCw, Trash2, UserRound } from 'lucide-react'
|
|
8
|
+
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
9
|
+
import { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'
|
|
10
|
+
import type { TranslateFn, TranslateParams } from '@open-mercato/shared/lib/i18n/context'
|
|
11
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
12
|
+
import { formatDateTime, formatRelativeTime } from '@open-mercato/shared/lib/time'
|
|
13
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
14
|
+
import { DataTable } from '@open-mercato/ui/backend/DataTable'
|
|
15
|
+
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
16
|
+
import { ActionsDropdown } from '@open-mercato/ui/backend/forms/ActionsDropdown'
|
|
17
|
+
import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
18
|
+
import { CrudForm, type CrudField, type CrudFieldOption, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
19
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
20
|
+
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
21
|
+
import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
22
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
23
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
24
|
+
import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
|
|
25
|
+
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
26
|
+
import { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'
|
|
27
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'
|
|
28
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
29
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
30
|
+
// Lazy-load the chat surface so the claim detail route chunk does not carry the AI
|
|
31
|
+
// SDK runtime for every operator — it only renders once a triage chat session is open.
|
|
32
|
+
const LazyAiChat = React.lazy(async () => {
|
|
33
|
+
const mod = await import('@open-mercato/ui/ai/AiChat')
|
|
34
|
+
return { default: mod.AiChat }
|
|
35
|
+
})
|
|
36
|
+
import { AiIcon } from '@open-mercato/ui/ai/AiIcon'
|
|
37
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
38
|
+
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
39
|
+
import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
|
|
40
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
41
|
+
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
42
|
+
import { createCrud, deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
43
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
44
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
45
|
+
import { mapCrudServerErrorToFormErrors } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
46
|
+
import { useCurrentUserId } from '@open-mercato/ui/backend/utils/useCurrentUserId'
|
|
47
|
+
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
48
|
+
import { AttachmentInput } from '@open-mercato/core/modules/attachments/fields/attachment'
|
|
49
|
+
import {
|
|
50
|
+
fetchAssignableStaffMembersPage,
|
|
51
|
+
type AssignableStaffMember,
|
|
52
|
+
} from '@open-mercato/core/modules/customers/lib/assignableStaff'
|
|
53
|
+
import { CLAIM_STATUS_TRANSITIONS } from '../../../data/constants'
|
|
54
|
+
import {
|
|
55
|
+
ClaimStatusBadge,
|
|
56
|
+
type ClaimLineStatus,
|
|
57
|
+
type ClaimStatus,
|
|
58
|
+
} from '../../components/ClaimStatusBadge'
|
|
59
|
+
import { ClaimSlaIndicator } from '../../components/claimSla'
|
|
60
|
+
import {
|
|
61
|
+
ClaimLineProductPicker,
|
|
62
|
+
EntitlementChip,
|
|
63
|
+
computeEntitlementPreview,
|
|
64
|
+
type ClaimProductPick,
|
|
65
|
+
} from '../../components/productLookup'
|
|
66
|
+
import { AiAssessButtons } from '../../components/AiAssessButtons'
|
|
67
|
+
import { useUserDisplayNames } from '../../components/useUserDisplayNames'
|
|
68
|
+
import { ReceivingPanel } from '../../components/ReceivingPanel'
|
|
69
|
+
import { ReturnLabelPanel } from '../../components/ReturnLabelPanel'
|
|
70
|
+
import { VendorRecoverySuggestionsPanel } from '../../components/VendorRecoverySuggestionsPanel'
|
|
71
|
+
import { ClaimStageProgress } from '../../components/ClaimStageProgress'
|
|
72
|
+
import { claimTypeAllowsLineFinancialAdjustments, resolveClaimTypeUiConfig } from '../../../lib/claimTypeConfig'
|
|
73
|
+
import { isTerminal } from '../../../lib/stateMachine'
|
|
74
|
+
import { isCustomerVisibleAttachment } from '../../../lib/attachmentVisibility'
|
|
75
|
+
import { formatQuantity, parseQuantity, quantityInputValue } from '../../../lib/quantity'
|
|
76
|
+
import { localizeDictionaryLabel, type DictionaryLabelKind } from '../../../lib/dictionaryLabels'
|
|
77
|
+
import {
|
|
78
|
+
TIMELINE_FILTERS,
|
|
79
|
+
filterTimelineEvents,
|
|
80
|
+
isTimelineFilterId,
|
|
81
|
+
type TimelineFilterId,
|
|
82
|
+
} from '../../../lib/timelineFilters'
|
|
83
|
+
|
|
84
|
+
type ClaimType = 'warranty' | 'return' | 'core_return' | 'vendor_recovery'
|
|
85
|
+
type ClaimChannel = 'staff' | 'portal' | 'api'
|
|
86
|
+
type ClaimPriority = 'low' | 'normal' | 'high' | 'urgent'
|
|
87
|
+
type Disposition =
|
|
88
|
+
| 'restock'
|
|
89
|
+
| 'repair'
|
|
90
|
+
| 'replace'
|
|
91
|
+
| 'credit'
|
|
92
|
+
| 'refund'
|
|
93
|
+
| 'field_destroy'
|
|
94
|
+
| 'scrap'
|
|
95
|
+
| 'return_to_vendor'
|
|
96
|
+
| 'deny'
|
|
97
|
+
|
|
98
|
+
type TriageMessage = {
|
|
99
|
+
messageKey: string
|
|
100
|
+
params?: TranslateParams
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type TriageMessageValue = TriageMessage | string
|
|
104
|
+
|
|
105
|
+
type ClaimRiskLevel = 'none' | 'low' | 'medium' | 'high'
|
|
106
|
+
|
|
107
|
+
type ClaimRiskSignal = {
|
|
108
|
+
id: string
|
|
109
|
+
level: 'low' | 'medium' | 'high'
|
|
110
|
+
messageKey: string
|
|
111
|
+
params?: TranslateParams
|
|
112
|
+
relatedClaimNumbers?: string[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type ClaimRiskAssessment = {
|
|
116
|
+
level: ClaimRiskLevel
|
|
117
|
+
signals: ClaimRiskSignal[]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type RiskApiResponse = {
|
|
121
|
+
ok?: boolean
|
|
122
|
+
result?: ClaimRiskAssessment
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type FeatureCheckResponse = {
|
|
126
|
+
ok?: boolean
|
|
127
|
+
granted?: string[]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type ClaimRecord = {
|
|
131
|
+
id: string
|
|
132
|
+
claimNumber: string | null
|
|
133
|
+
claimType: ClaimType | string | null
|
|
134
|
+
channel: ClaimChannel | string | null
|
|
135
|
+
status: ClaimStatus | string | null
|
|
136
|
+
priority: ClaimPriority | string | null
|
|
137
|
+
customerId: string | null
|
|
138
|
+
customerName: string | null
|
|
139
|
+
orderId: string | null
|
|
140
|
+
orderNumber: string | null
|
|
141
|
+
salesReturnId: string | null
|
|
142
|
+
replacementOrderId: string | null
|
|
143
|
+
creditMemoId: string | null
|
|
144
|
+
awaitingStaffReply: boolean
|
|
145
|
+
vendorName: string | null
|
|
146
|
+
vendorRef: string | null
|
|
147
|
+
totalClaimedAmount: string | null
|
|
148
|
+
totalApprovedAmount: string | null
|
|
149
|
+
totalRecoveredAmount: string | null
|
|
150
|
+
slaDueAt: string | null
|
|
151
|
+
slaPausedAt: string | null
|
|
152
|
+
submittedAt: string | null
|
|
153
|
+
assigneeUserId: string | null
|
|
154
|
+
assigneeName: string | null
|
|
155
|
+
createdAt: string | null
|
|
156
|
+
updatedAt: string | null
|
|
157
|
+
currencyCode: string | null
|
|
158
|
+
notes: string | null
|
|
159
|
+
reasonCode: string | null
|
|
160
|
+
rejectionReasonCode: string | null
|
|
161
|
+
resolutionSummary: string | null
|
|
162
|
+
returnLabelUrl: string | null
|
|
163
|
+
returnTrackingNumber: string | null
|
|
164
|
+
returnCarrier: string | null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type ClaimLine = {
|
|
168
|
+
id: string
|
|
169
|
+
claimId: string | null
|
|
170
|
+
lineNo: number | null
|
|
171
|
+
productId: string | null
|
|
172
|
+
variantId: string | null
|
|
173
|
+
productName: string | null
|
|
174
|
+
orderLineId: string | null
|
|
175
|
+
sku: string | null
|
|
176
|
+
serialNumber: string | null
|
|
177
|
+
purchaseDate: string | null
|
|
178
|
+
warrantyMonths: number | null
|
|
179
|
+
faultCode: string | null
|
|
180
|
+
faultDescription: string | null
|
|
181
|
+
qtyClaimed: string | null
|
|
182
|
+
qtyApproved: string | null
|
|
183
|
+
qtyReceived: string | null
|
|
184
|
+
disposition: Disposition | string | null
|
|
185
|
+
lineStatus: ClaimLineStatus | string | null
|
|
186
|
+
creditAmount: string | null
|
|
187
|
+
restockingFee: string | null
|
|
188
|
+
coreChargeAmount: string | null
|
|
189
|
+
coreCreditAmount: string | null
|
|
190
|
+
vendorClaimLineId: string | null
|
|
191
|
+
conditionOnReceipt: string | null
|
|
192
|
+
conditionGrade: string | null
|
|
193
|
+
quarantineStatus: string | null
|
|
194
|
+
inspectionNotes: string | null
|
|
195
|
+
assessmentPayload: Record<string, unknown> | null
|
|
196
|
+
updatedAt: string | null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type ClaimEvent = {
|
|
200
|
+
id: string
|
|
201
|
+
kind: string
|
|
202
|
+
visibility: 'internal' | 'customer' | string
|
|
203
|
+
body: string | null
|
|
204
|
+
payload: Record<string, unknown> | null
|
|
205
|
+
actorUserId: string | null
|
|
206
|
+
actorCustomerId: string | null
|
|
207
|
+
createdAt: string | null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type ClaimTriageSuggestion = {
|
|
211
|
+
priority: {
|
|
212
|
+
currentPriority: string
|
|
213
|
+
suggestedPriority: string
|
|
214
|
+
overdue: boolean
|
|
215
|
+
reason: TriageMessageValue
|
|
216
|
+
}
|
|
217
|
+
lines: Array<{
|
|
218
|
+
lineId: string
|
|
219
|
+
lineNo: number
|
|
220
|
+
sku: string | null
|
|
221
|
+
productName: string | null
|
|
222
|
+
serialNumber: string | null
|
|
223
|
+
eligibility: {
|
|
224
|
+
status: string
|
|
225
|
+
reason: TriageMessageValue
|
|
226
|
+
}
|
|
227
|
+
suggestedDisposition: string
|
|
228
|
+
reason: TriageMessageValue
|
|
229
|
+
}>
|
|
230
|
+
risk?: ClaimRiskAssessment
|
|
231
|
+
generatedAt: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
type InlineLineUpdateField = 'qtyApproved' | 'disposition' | 'lineStatus'
|
|
235
|
+
type InlineLineUpdateValue = string | null
|
|
236
|
+
type InlineLineSaveHandler = (
|
|
237
|
+
line: ClaimLine,
|
|
238
|
+
field: InlineLineUpdateField,
|
|
239
|
+
value: InlineLineUpdateValue,
|
|
240
|
+
) => Promise<void>
|
|
241
|
+
|
|
242
|
+
type SelectOption = {
|
|
243
|
+
value: string
|
|
244
|
+
label: string
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function eligibilityBadgeVariant(status: string): 'success' | 'error' | 'neutral' {
|
|
248
|
+
if (status === 'in_warranty') return 'success'
|
|
249
|
+
if (status === 'out_of_warranty') return 'error'
|
|
250
|
+
return 'neutral'
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type ClaimFormValues = Partial<ClaimRecord> & Record<string, unknown>
|
|
254
|
+
type ClaimLineFormValues = Partial<ClaimLine> & Record<string, unknown>
|
|
255
|
+
type TransitionFormValues = {
|
|
256
|
+
rejectionReasonCode?: string | null
|
|
257
|
+
resolutionSummary?: string | null
|
|
258
|
+
}
|
|
259
|
+
type VendorRecoveryFormValues = {
|
|
260
|
+
lineIds?: string[]
|
|
261
|
+
vendorName?: string | null
|
|
262
|
+
vendorRef?: string | null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type TabId = 'lines' | 'timeline' | 'attachments' | 'ai'
|
|
266
|
+
|
|
267
|
+
const CLAIM_LINE_STATUSES: ClaimLineStatus[] = ['pending', 'approved', 'rejected', 'received', 'inspected', 'resolved']
|
|
268
|
+
const CLAIM_DISPOSITIONS: Disposition[] = [
|
|
269
|
+
'restock',
|
|
270
|
+
'repair',
|
|
271
|
+
'replace',
|
|
272
|
+
'credit',
|
|
273
|
+
'refund',
|
|
274
|
+
'field_destroy',
|
|
275
|
+
'scrap',
|
|
276
|
+
'return_to_vendor',
|
|
277
|
+
'deny',
|
|
278
|
+
]
|
|
279
|
+
const DICTIONARY_KEYS = {
|
|
280
|
+
faultCodes: 'warranty_claims.warranty_claim_fault_code',
|
|
281
|
+
rejectionReasons: 'warranty_claims.warranty_claim_rejection_reason',
|
|
282
|
+
} as const
|
|
283
|
+
const LINE_EDITABLE_CLAIM_STATUSES = new Set<string>([
|
|
284
|
+
'draft',
|
|
285
|
+
'submitted',
|
|
286
|
+
'in_review',
|
|
287
|
+
'info_requested',
|
|
288
|
+
'approved',
|
|
289
|
+
'received',
|
|
290
|
+
'inspecting',
|
|
291
|
+
])
|
|
292
|
+
// Must match the server-side receiving gate (`assertReceivingStatus` accepts only received /
|
|
293
|
+
// inspecting). Showing the controls at approved / awaiting_return guided agents into an
|
|
294
|
+
// operation the API rejects with a wrong-state error (WQA-006 / FLOW-02).
|
|
295
|
+
const RECEIVING_CAPABLE_CLAIM_STATUSES = new Set<string>([
|
|
296
|
+
'received',
|
|
297
|
+
'inspecting',
|
|
298
|
+
])
|
|
299
|
+
const DELETABLE_CLAIM_STATUSES = new Set<string>(['draft', 'cancelled'])
|
|
300
|
+
const LINE_STATUS_TRANSITIONS: Record<ClaimLineStatus, ClaimLineStatus[]> = {
|
|
301
|
+
pending: ['approved', 'rejected'],
|
|
302
|
+
approved: ['received', 'resolved'],
|
|
303
|
+
rejected: [],
|
|
304
|
+
received: ['inspected'],
|
|
305
|
+
inspected: ['resolved'],
|
|
306
|
+
resolved: [],
|
|
307
|
+
}
|
|
308
|
+
const EMPTY_RISK_ASSESSMENT: ClaimRiskAssessment = { level: 'none', signals: [] }
|
|
309
|
+
const UNASSIGNED_SELECT_VALUE = '__unassigned__'
|
|
310
|
+
const CLEAR_SELECT_VALUE = '__clear__'
|
|
311
|
+
const AGENT_ID = 'warranty_claims.claims_assistant'
|
|
312
|
+
|
|
313
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
314
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function toStringOrNull(value: unknown): string | null {
|
|
318
|
+
return typeof value === 'string' && value.trim().length ? value.trim() : null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function toNumberOrNull(value: unknown): number | null {
|
|
322
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
323
|
+
if (typeof value === 'string' && value.trim().length) {
|
|
324
|
+
const parsed = Number(value)
|
|
325
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
326
|
+
}
|
|
327
|
+
return null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toTranslateParams(value: unknown): TranslateParams | undefined {
|
|
331
|
+
if (!isRecord(value)) return undefined
|
|
332
|
+
const params: TranslateParams = {}
|
|
333
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
334
|
+
if (typeof entry === 'string' || typeof entry === 'number') {
|
|
335
|
+
params[key] = entry
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return Object.keys(params).length ? params : undefined
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeRiskSignal(value: unknown): ClaimRiskSignal | null {
|
|
342
|
+
if (!isRecord(value)) return null
|
|
343
|
+
const id = toStringOrNull(value.id)
|
|
344
|
+
const level = toStringOrNull(value.level)
|
|
345
|
+
const messageKey = toStringOrNull(value.messageKey)
|
|
346
|
+
if (!id || !messageKey || (level !== 'low' && level !== 'medium' && level !== 'high')) return null
|
|
347
|
+
const relatedClaimNumbers = Array.isArray(value.relatedClaimNumbers)
|
|
348
|
+
? value.relatedClaimNumbers.filter((claimNumber): claimNumber is string => typeof claimNumber === 'string' && claimNumber.length > 0)
|
|
349
|
+
: undefined
|
|
350
|
+
return {
|
|
351
|
+
id,
|
|
352
|
+
level,
|
|
353
|
+
messageKey,
|
|
354
|
+
params: toTranslateParams(value.params),
|
|
355
|
+
relatedClaimNumbers: relatedClaimNumbers?.length ? relatedClaimNumbers : undefined,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function normalizeRiskAssessment(value: unknown): ClaimRiskAssessment {
|
|
360
|
+
if (!isRecord(value)) return EMPTY_RISK_ASSESSMENT
|
|
361
|
+
const level = toStringOrNull(value.level)
|
|
362
|
+
const normalizedLevel: ClaimRiskLevel =
|
|
363
|
+
level === 'low' || level === 'medium' || level === 'high' || level === 'none'
|
|
364
|
+
? level
|
|
365
|
+
: 'none'
|
|
366
|
+
const signals = Array.isArray(value.signals)
|
|
367
|
+
? value.signals.map(normalizeRiskSignal).filter((signal): signal is ClaimRiskSignal => signal !== null)
|
|
368
|
+
: []
|
|
369
|
+
return { level: normalizedLevel, signals }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeRiskResponse(value: unknown): ClaimRiskAssessment {
|
|
373
|
+
if (!isRecord(value)) return EMPTY_RISK_ASSESSMENT
|
|
374
|
+
if ('result' in value) return normalizeRiskAssessment(value.result)
|
|
375
|
+
return normalizeRiskAssessment(value)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isClaimLineStatusValue(value: string | null | undefined): value is ClaimLineStatus {
|
|
379
|
+
return typeof value === 'string' && CLAIM_LINE_STATUSES.includes(value as ClaimLineStatus)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function buildLineStatusOptions(status: string | null | undefined, t: TranslateFn): SelectOption[] {
|
|
383
|
+
const current = isClaimLineStatusValue(status) ? status : 'pending'
|
|
384
|
+
const values = Array.from(new Set<ClaimLineStatus>([current, ...LINE_STATUS_TRANSITIONS[current]]))
|
|
385
|
+
return values.map((value) => ({ value, label: t(`warranty_claims.lineStatus.${value}`) }))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildDispositionOptions(
|
|
389
|
+
allowedDispositions: readonly string[],
|
|
390
|
+
currentDisposition: string | null | undefined,
|
|
391
|
+
t: TranslateFn,
|
|
392
|
+
): SelectOption[] {
|
|
393
|
+
const values = new Set<string>(
|
|
394
|
+
CLAIM_DISPOSITIONS.filter((disposition) => allowedDispositions.includes(disposition)),
|
|
395
|
+
)
|
|
396
|
+
if (currentDisposition) values.add(currentDisposition)
|
|
397
|
+
return Array.from(values).map((disposition) => ({
|
|
398
|
+
value: disposition,
|
|
399
|
+
label: t(`warranty_claims.disposition.${disposition}`, disposition),
|
|
400
|
+
}))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function formatTriageMessage(value: TriageMessageValue, t: TranslateFn): string {
|
|
404
|
+
if (typeof value === 'string') return t(value)
|
|
405
|
+
return t(value.messageKey, value.params)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function triageLineHeading(
|
|
409
|
+
suggestedLine: ClaimTriageSuggestion['lines'][number],
|
|
410
|
+
lines: ClaimLine[],
|
|
411
|
+
t: TranslateFn,
|
|
412
|
+
): string {
|
|
413
|
+
const name = suggestedLine.productName ?? suggestedLine.sku ?? suggestedLine.serialNumber ?? null
|
|
414
|
+
const lineNo = toNumberOrNull(suggestedLine.lineNo)
|
|
415
|
+
?? lines.find((line) => line.id === suggestedLine.lineId)?.lineNo
|
|
416
|
+
?? null
|
|
417
|
+
const label = name ?? t('warranty_claims.detail.lines.unnamed', 'Unnamed line')
|
|
418
|
+
return lineNo !== null ? `#${lineNo} ${label}` : label
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function formatTimelineBody(event: ClaimEvent, t: TranslateFn, userNames: Record<string, string>): string | null {
|
|
422
|
+
if (event.body) return event.body
|
|
423
|
+
const payload = event.payload
|
|
424
|
+
const action = payload ? toStringOrNull(payload.action) : null
|
|
425
|
+
if (event.kind === 'system' && action === 'sla_paused') return t('warranty_claims.timeline.slaPaused')
|
|
426
|
+
if (event.kind === 'system' && action === 'sla_resumed') return t('warranty_claims.timeline.slaResumed')
|
|
427
|
+
if (event.kind === 'system' && action === 'auto_approved') return t('warranty_claims.timeline.autoApproved')
|
|
428
|
+
if (event.kind === 'system' && action === 'ai_assessment_recorded') return t('warranty_claims.timeline.aiAssessmentRecorded')
|
|
429
|
+
if (event.kind === 'system' && action === 'sales_return_created') return t('warranty_claims.timeline.salesReturnCreated')
|
|
430
|
+
if (event.kind === 'system' && action === 'sales_return_orphaned') return t('warranty_claims.timeline.salesReturnOrphaned')
|
|
431
|
+
if (event.kind === 'system' && action === 'undo_sales_return_created') return t('warranty_claims.timeline.undoSalesReturnCreated')
|
|
432
|
+
if (event.kind === 'system' && action === 'replacement_order_created') return t('warranty_claims.timeline.replacementOrderCreated')
|
|
433
|
+
if (event.kind === 'system' && action === 'replacement_order_orphaned') return t('warranty_claims.timeline.replacementOrderOrphaned')
|
|
434
|
+
if (event.kind === 'system' && action === 'undo_replacement_order_created') return t('warranty_claims.timeline.undoReplacementOrderCreated')
|
|
435
|
+
if (event.kind === 'system' && action === 'credit_memo_created') return t('warranty_claims.timeline.creditMemoCreated')
|
|
436
|
+
if (event.kind === 'system' && action === 'credit_memo_orphaned') return t('warranty_claims.timeline.creditMemoOrphaned')
|
|
437
|
+
if (event.kind === 'system' && action === 'undo_credit_memo_created') return t('warranty_claims.timeline.undoCreditMemoCreated')
|
|
438
|
+
if (event.kind === 'assignment') {
|
|
439
|
+
const assigneeUserId = payload ? toStringOrNull(payload.assigneeUserId) : null
|
|
440
|
+
if (!assigneeUserId) return t('warranty_claims.timeline.unassigned')
|
|
441
|
+
return t('warranty_claims.timeline.assignedTo', { name: userNames[assigneeUserId] ?? t('warranty_claims.detail.unknownUser') })
|
|
442
|
+
}
|
|
443
|
+
const from = payload ? toStringOrNull(payload.from) ?? toStringOrNull(payload.fromStatus) : null
|
|
444
|
+
const to = payload ? toStringOrNull(payload.to) ?? toStringOrNull(payload.toStatus) : null
|
|
445
|
+
if (event.kind === 'status_changed' && from && to) {
|
|
446
|
+
const statusLine = `${t(`warranty_claims.status.${from}`)} → ${t(`warranty_claims.status.${to}`)}`
|
|
447
|
+
const systemNote = payload ? toStringOrNull(payload.systemNote) : null
|
|
448
|
+
if (systemNote && systemNote.startsWith('warranty_claims.')) {
|
|
449
|
+
return `${statusLine} — ${t(systemNote)}`
|
|
450
|
+
}
|
|
451
|
+
return statusLine
|
|
452
|
+
}
|
|
453
|
+
return null
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function resolveTimelineActor(event: ClaimEvent, claim: ClaimRecord, userNames: Record<string, string>, t: TranslateFn): string {
|
|
457
|
+
if (event.actorUserId) return userNames[event.actorUserId] ?? t('warranty_claims.detail.unknownUser')
|
|
458
|
+
if (event.actorCustomerId) return claim.customerName ?? t('warranty_claims.detail.customerActor')
|
|
459
|
+
return t('warranty_claims.detail.systemActor')
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function translateErrorMessage(err: unknown, t: TranslateFn, fallbackKey: string): string {
|
|
463
|
+
const mappedMessage = mapCrudServerErrorToFormErrors(err).message
|
|
464
|
+
if (typeof mappedMessage === 'string' && mappedMessage.trim().length) return t(mappedMessage, mappedMessage)
|
|
465
|
+
if (err instanceof Error && err.message) return t(err.message, err.message)
|
|
466
|
+
return t(fallbackKey)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function buildRiskSignalTitle(signal: ClaimRiskSignal): string | undefined {
|
|
470
|
+
return signal.relatedClaimNumbers?.length ? signal.relatedClaimNumbers.join(', ') : undefined
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function riskSignalVariant(signal: ClaimRiskSignal): 'warning' | 'error' {
|
|
474
|
+
return signal.level === 'high' ? 'error' : 'warning'
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizeClaim(value: unknown): ClaimRecord | null {
|
|
478
|
+
if (!isRecord(value)) return null
|
|
479
|
+
const id = toStringOrNull(value.id)
|
|
480
|
+
if (!id) return null
|
|
481
|
+
return {
|
|
482
|
+
id,
|
|
483
|
+
claimNumber: toStringOrNull(value.claimNumber),
|
|
484
|
+
claimType: toStringOrNull(value.claimType),
|
|
485
|
+
channel: toStringOrNull(value.channel),
|
|
486
|
+
status: toStringOrNull(value.status),
|
|
487
|
+
priority: toStringOrNull(value.priority),
|
|
488
|
+
customerId: toStringOrNull(value.customerId),
|
|
489
|
+
customerName: toStringOrNull(value.customerName),
|
|
490
|
+
orderId: toStringOrNull(value.orderId),
|
|
491
|
+
orderNumber: toStringOrNull(value.orderNumber),
|
|
492
|
+
salesReturnId: toStringOrNull(value.salesReturnId),
|
|
493
|
+
replacementOrderId: toStringOrNull(value.replacementOrderId),
|
|
494
|
+
creditMemoId: toStringOrNull(value.creditMemoId),
|
|
495
|
+
awaitingStaffReply: parseBooleanFromUnknown(value.awaitingStaffReply) ?? false,
|
|
496
|
+
vendorName: toStringOrNull(value.vendorName),
|
|
497
|
+
vendorRef: toStringOrNull(value.vendorRef),
|
|
498
|
+
totalClaimedAmount: toStringOrNull(value.totalClaimedAmount),
|
|
499
|
+
totalApprovedAmount: toStringOrNull(value.totalApprovedAmount),
|
|
500
|
+
totalRecoveredAmount: toStringOrNull(value.totalRecoveredAmount),
|
|
501
|
+
slaDueAt: toStringOrNull(value.slaDueAt),
|
|
502
|
+
slaPausedAt: toStringOrNull(value.slaPausedAt),
|
|
503
|
+
submittedAt: toStringOrNull(value.submittedAt),
|
|
504
|
+
assigneeUserId: toStringOrNull(value.assigneeUserId),
|
|
505
|
+
assigneeName: toStringOrNull(value.assigneeName),
|
|
506
|
+
createdAt: toStringOrNull(value.createdAt),
|
|
507
|
+
updatedAt: toStringOrNull(value.updatedAt),
|
|
508
|
+
currencyCode: toStringOrNull(value.currencyCode),
|
|
509
|
+
notes: toStringOrNull(value.notes),
|
|
510
|
+
reasonCode: toStringOrNull(value.reasonCode),
|
|
511
|
+
rejectionReasonCode: toStringOrNull(value.rejectionReasonCode),
|
|
512
|
+
resolutionSummary: toStringOrNull(value.resolutionSummary),
|
|
513
|
+
returnLabelUrl: toStringOrNull(value.returnLabelUrl),
|
|
514
|
+
returnTrackingNumber: toStringOrNull(value.returnTrackingNumber),
|
|
515
|
+
returnCarrier: toStringOrNull(value.returnCarrier),
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function normalizeLine(value: unknown): ClaimLine | null {
|
|
520
|
+
if (!isRecord(value)) return null
|
|
521
|
+
const id = toStringOrNull(value.id)
|
|
522
|
+
if (!id) return null
|
|
523
|
+
return {
|
|
524
|
+
id,
|
|
525
|
+
claimId: toStringOrNull(value.claimId),
|
|
526
|
+
lineNo: toNumberOrNull(value.lineNo),
|
|
527
|
+
productId: toStringOrNull(value.productId),
|
|
528
|
+
variantId: toStringOrNull(value.variantId),
|
|
529
|
+
productName: toStringOrNull(value.productName),
|
|
530
|
+
orderLineId: toStringOrNull(value.orderLineId),
|
|
531
|
+
sku: toStringOrNull(value.sku),
|
|
532
|
+
serialNumber: toStringOrNull(value.serialNumber),
|
|
533
|
+
purchaseDate: toStringOrNull(value.purchaseDate),
|
|
534
|
+
warrantyMonths: toNumberOrNull(value.warrantyMonths),
|
|
535
|
+
faultCode: toStringOrNull(value.faultCode),
|
|
536
|
+
faultDescription: toStringOrNull(value.faultDescription),
|
|
537
|
+
qtyClaimed: toStringOrNull(value.qtyClaimed),
|
|
538
|
+
qtyApproved: toStringOrNull(value.qtyApproved),
|
|
539
|
+
qtyReceived: toStringOrNull(value.qtyReceived),
|
|
540
|
+
disposition: toStringOrNull(value.disposition),
|
|
541
|
+
lineStatus: toStringOrNull(value.lineStatus),
|
|
542
|
+
creditAmount: toStringOrNull(value.creditAmount),
|
|
543
|
+
restockingFee: toStringOrNull(value.restockingFee),
|
|
544
|
+
coreChargeAmount: toStringOrNull(value.coreChargeAmount),
|
|
545
|
+
coreCreditAmount: toStringOrNull(value.coreCreditAmount),
|
|
546
|
+
vendorClaimLineId: toStringOrNull(value.vendorClaimLineId),
|
|
547
|
+
conditionOnReceipt: toStringOrNull(value.conditionOnReceipt),
|
|
548
|
+
conditionGrade: toStringOrNull(value.conditionGrade),
|
|
549
|
+
quarantineStatus: toStringOrNull(value.quarantineStatus),
|
|
550
|
+
inspectionNotes: toStringOrNull(value.inspectionNotes),
|
|
551
|
+
assessmentPayload: isRecord(value.assessmentPayload) ? value.assessmentPayload : null,
|
|
552
|
+
updatedAt: toStringOrNull(value.updatedAt),
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function normalizeEvent(value: unknown): ClaimEvent | null {
|
|
557
|
+
if (!isRecord(value)) return null
|
|
558
|
+
const id = toStringOrNull(value.id)
|
|
559
|
+
if (!id) return null
|
|
560
|
+
return {
|
|
561
|
+
id,
|
|
562
|
+
kind: toStringOrNull(value.kind) ?? 'system',
|
|
563
|
+
visibility: toStringOrNull(value.visibility) ?? 'internal',
|
|
564
|
+
body: toStringOrNull(value.body),
|
|
565
|
+
payload: isRecord(value.payload) ? value.payload : null,
|
|
566
|
+
actorUserId: toStringOrNull(value.actorUserId),
|
|
567
|
+
actorCustomerId: toStringOrNull(value.actorCustomerId),
|
|
568
|
+
createdAt: toStringOrNull(value.createdAt),
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function normalizeDictionaryOption(item: unknown): CrudFieldOption | null {
|
|
573
|
+
if (!isRecord(item)) return null
|
|
574
|
+
const value = toStringOrNull(item.value)
|
|
575
|
+
if (!value) return null
|
|
576
|
+
return { value, label: toStringOrNull(item.label) ?? value }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function nullableText(value: unknown): string | null {
|
|
580
|
+
return toStringOrNull(value)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildConflictError(
|
|
584
|
+
call: { status: number; result: unknown },
|
|
585
|
+
fallbackMessage: string,
|
|
586
|
+
t?: TranslateFn,
|
|
587
|
+
): Error & Record<string, unknown> {
|
|
588
|
+
const payload = isRecord(call.result) ? call.result : {}
|
|
589
|
+
const rawMessage = typeof payload.error === 'string' ? payload.error : fallbackMessage
|
|
590
|
+
const message = t ? t(rawMessage) : rawMessage
|
|
591
|
+
return Object.assign(new Error(message), { status: call.status }, payload)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function readDraftReplyPayload(value: unknown): Record<string, unknown> {
|
|
595
|
+
if (!isRecord(value)) return {}
|
|
596
|
+
return isRecord(value.result) ? value.result : value
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function isDraftReplyNotConfigured(value: unknown): boolean {
|
|
600
|
+
const payload = readDraftReplyPayload(value)
|
|
601
|
+
return payload.notConfigured === true
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function readDraftReply(value: unknown): string | null {
|
|
605
|
+
return toStringOrNull(readDraftReplyPayload(value).draft)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function readErrorKey(value: unknown): string | null {
|
|
609
|
+
return isRecord(value) ? toStringOrNull(value.error) : null
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function formatAmount(value: string | null, currencyCode: string | null, fallback: string): string {
|
|
613
|
+
const amount = toNumberOrNull(value)
|
|
614
|
+
if (amount === null) return fallback
|
|
615
|
+
if (!currencyCode) return amount.toLocaleString()
|
|
616
|
+
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currencyCode }).format(amount)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function eventIcon(kind: string) {
|
|
620
|
+
if (kind === 'comment') return MessageSquare
|
|
621
|
+
if (kind === 'assignment') return UserRound
|
|
622
|
+
if (kind === 'status_changed') return RefreshCw
|
|
623
|
+
return Info
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const TIMELINE_FILTER_LABELS: Record<TimelineFilterId, { key: string; fallback: string }> = {
|
|
627
|
+
all: { key: 'warranty_claims.detail.timeline.filter.all', fallback: 'All' },
|
|
628
|
+
comments: { key: 'warranty_claims.detail.timeline.filter.comments', fallback: 'Comments' },
|
|
629
|
+
status_changes: { key: 'warranty_claims.detail.timeline.filter.statusChanges', fallback: 'Status changes' },
|
|
630
|
+
customer_visible: { key: 'warranty_claims.detail.timeline.filter.customerVisible', fallback: 'Customer-visible' },
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function RiskSignalChips({ signals, t }: { signals: ClaimRiskSignal[]; t: TranslateFn }) {
|
|
634
|
+
if (!signals.length) return null
|
|
635
|
+
return (
|
|
636
|
+
<>
|
|
637
|
+
{signals.map((signal) => (
|
|
638
|
+
<span key={signal.id} className="min-w-0 max-w-full" title={buildRiskSignalTitle(signal)}>
|
|
639
|
+
<StatusBadge
|
|
640
|
+
variant={riskSignalVariant(signal)}
|
|
641
|
+
className="h-auto max-w-full rounded-md py-1 whitespace-normal break-words text-left leading-snug"
|
|
642
|
+
>
|
|
643
|
+
{t(signal.messageKey, signal.params)}
|
|
644
|
+
</StatusBadge>
|
|
645
|
+
</span>
|
|
646
|
+
))}
|
|
647
|
+
</>
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function InlineQtyApprovedCell({
|
|
652
|
+
line,
|
|
653
|
+
disabled,
|
|
654
|
+
label,
|
|
655
|
+
onSave,
|
|
656
|
+
}: {
|
|
657
|
+
line: ClaimLine
|
|
658
|
+
disabled: boolean
|
|
659
|
+
label: string
|
|
660
|
+
onSave: InlineLineSaveHandler
|
|
661
|
+
}) {
|
|
662
|
+
const currentValue = quantityInputValue(line.qtyApproved)
|
|
663
|
+
const [draft, setDraft] = React.useState(currentValue)
|
|
664
|
+
const blurSaveSuppressedRef = React.useRef(false)
|
|
665
|
+
|
|
666
|
+
React.useEffect(() => {
|
|
667
|
+
setDraft(currentValue)
|
|
668
|
+
}, [currentValue])
|
|
669
|
+
|
|
670
|
+
const commit = React.useCallback((value: string) => {
|
|
671
|
+
const nextValue = value.trim().length ? value.trim() : null
|
|
672
|
+
if (parseQuantity(nextValue) === parseQuantity(line.qtyApproved)) return
|
|
673
|
+
void onSave(line, 'qtyApproved', nextValue)
|
|
674
|
+
}, [line, onSave])
|
|
675
|
+
|
|
676
|
+
return (
|
|
677
|
+
<Input
|
|
678
|
+
type="number"
|
|
679
|
+
inputMode="decimal"
|
|
680
|
+
value={draft}
|
|
681
|
+
disabled={disabled}
|
|
682
|
+
aria-label={label}
|
|
683
|
+
className="w-24"
|
|
684
|
+
onClick={(event) => event.stopPropagation()}
|
|
685
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
686
|
+
onFocus={() => { blurSaveSuppressedRef.current = false }}
|
|
687
|
+
onBlur={() => {
|
|
688
|
+
if (blurSaveSuppressedRef.current) {
|
|
689
|
+
blurSaveSuppressedRef.current = false
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
commit(draft)
|
|
693
|
+
}}
|
|
694
|
+
onKeyDown={(event) => {
|
|
695
|
+
if (event.key === 'Escape') {
|
|
696
|
+
event.preventDefault()
|
|
697
|
+
blurSaveSuppressedRef.current = true
|
|
698
|
+
setDraft(currentValue)
|
|
699
|
+
event.currentTarget.blur()
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
if (event.key === 'Enter') {
|
|
703
|
+
event.preventDefault()
|
|
704
|
+
blurSaveSuppressedRef.current = true
|
|
705
|
+
commit(draft)
|
|
706
|
+
event.currentTarget.blur()
|
|
707
|
+
}
|
|
708
|
+
}}
|
|
709
|
+
/>
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function InlineLineSelectCell({
|
|
714
|
+
line,
|
|
715
|
+
field,
|
|
716
|
+
value,
|
|
717
|
+
options,
|
|
718
|
+
label,
|
|
719
|
+
emptyLabel,
|
|
720
|
+
disabled,
|
|
721
|
+
onSave,
|
|
722
|
+
}: {
|
|
723
|
+
line: ClaimLine
|
|
724
|
+
field: Exclude<InlineLineUpdateField, 'qtyApproved'>
|
|
725
|
+
value: string | null
|
|
726
|
+
options: SelectOption[]
|
|
727
|
+
label: string
|
|
728
|
+
emptyLabel?: string
|
|
729
|
+
disabled: boolean
|
|
730
|
+
onSave: InlineLineSaveHandler
|
|
731
|
+
}) {
|
|
732
|
+
const selectedValue = value ?? CLEAR_SELECT_VALUE
|
|
733
|
+
|
|
734
|
+
return (
|
|
735
|
+
<div onClick={(event) => event.stopPropagation()}>
|
|
736
|
+
<Select
|
|
737
|
+
value={selectedValue}
|
|
738
|
+
disabled={disabled}
|
|
739
|
+
onValueChange={(nextValue) => {
|
|
740
|
+
const normalizedValue = nextValue === CLEAR_SELECT_VALUE ? null : nextValue
|
|
741
|
+
if (normalizedValue === value) return
|
|
742
|
+
void onSave(line, field, normalizedValue)
|
|
743
|
+
}}
|
|
744
|
+
>
|
|
745
|
+
<SelectTrigger aria-label={label} className="w-36">
|
|
746
|
+
<SelectValue />
|
|
747
|
+
</SelectTrigger>
|
|
748
|
+
<SelectContent>
|
|
749
|
+
{emptyLabel ? <SelectItem value={CLEAR_SELECT_VALUE}>{emptyLabel}</SelectItem> : null}
|
|
750
|
+
{options.map((option) => (
|
|
751
|
+
<SelectItem key={option.value} value={option.value}>
|
|
752
|
+
{option.label}
|
|
753
|
+
</SelectItem>
|
|
754
|
+
))}
|
|
755
|
+
</SelectContent>
|
|
756
|
+
</Select>
|
|
757
|
+
</div>
|
|
758
|
+
)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export default function WarrantyClaimDetailPage({ params }: { params?: { id?: string } }) {
|
|
762
|
+
const t = useT()
|
|
763
|
+
const locale = useLocale()
|
|
764
|
+
const router = useRouter()
|
|
765
|
+
const id = typeof params?.id === 'string' ? params.id : ''
|
|
766
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
767
|
+
const currentUserId = useCurrentUserId()
|
|
768
|
+
const scopeVersion = useOrganizationScopeVersion()
|
|
769
|
+
const [claim, setClaim] = React.useState<ClaimRecord | null>(null)
|
|
770
|
+
const [lines, setLines] = React.useState<ClaimLine[]>([])
|
|
771
|
+
const [events, setEvents] = React.useState<ClaimEvent[]>([])
|
|
772
|
+
const [attachmentCount, setAttachmentCount] = React.useState(0)
|
|
773
|
+
const [loading, setLoading] = React.useState(true)
|
|
774
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
775
|
+
const [notFound, setNotFound] = React.useState(false)
|
|
776
|
+
const [activeTab, setActiveTab] = React.useState<TabId>('lines')
|
|
777
|
+
const [timelineFilter, setTimelineFilter] = React.useState<TimelineFilterId>('all')
|
|
778
|
+
const [customerLink, setCustomerLink] = React.useState<{ customerId: string; href: string } | null>(null)
|
|
779
|
+
const [lineDialog, setLineDialog] = React.useState<{ mode: 'create' } | { mode: 'edit'; line: ClaimLine } | null>(null)
|
|
780
|
+
const [defaultWarrantyMonths, setDefaultWarrantyMonths] = React.useState<number | null>(null)
|
|
781
|
+
const [featureAccess, setFeatureAccess] = React.useState({
|
|
782
|
+
claimManage: false,
|
|
783
|
+
claimDelete: false,
|
|
784
|
+
receivingManage: false,
|
|
785
|
+
salesReturnsCreate: false,
|
|
786
|
+
salesOrdersManage: false,
|
|
787
|
+
salesCreditMemosManage: false,
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
React.useEffect(() => {
|
|
791
|
+
let cancelled = false
|
|
792
|
+
void apiCall<{ result?: { defaultWarrantyMonths?: number | null } }>('/api/warranty_claims/settings-general', { headers: { 'x-om-forbidden-redirect': '0' } }, { fallback: {} })
|
|
793
|
+
.then((call) => {
|
|
794
|
+
if (cancelled || !call.ok) return
|
|
795
|
+
const months = call.result?.result?.defaultWarrantyMonths
|
|
796
|
+
setDefaultWarrantyMonths(typeof months === 'number' && Number.isFinite(months) ? months : null)
|
|
797
|
+
})
|
|
798
|
+
.catch(() => undefined)
|
|
799
|
+
return () => {
|
|
800
|
+
cancelled = true
|
|
801
|
+
}
|
|
802
|
+
}, [scopeVersion])
|
|
803
|
+
|
|
804
|
+
React.useEffect(() => {
|
|
805
|
+
let cancelled = false
|
|
806
|
+
void apiCall<FeatureCheckResponse>(
|
|
807
|
+
'/api/auth/feature-check',
|
|
808
|
+
{
|
|
809
|
+
method: 'POST',
|
|
810
|
+
headers: { 'content-type': 'application/json' },
|
|
811
|
+
body: JSON.stringify({
|
|
812
|
+
features: [
|
|
813
|
+
'warranty_claims.claim.manage',
|
|
814
|
+
'warranty_claims.claim.delete',
|
|
815
|
+
'warranty_claims.receiving.manage',
|
|
816
|
+
'sales.returns.create',
|
|
817
|
+
'sales.orders.manage',
|
|
818
|
+
'sales.credit_memos.manage',
|
|
819
|
+
],
|
|
820
|
+
}),
|
|
821
|
+
},
|
|
822
|
+
{ fallback: { ok: false, granted: [] } },
|
|
823
|
+
)
|
|
824
|
+
.then((call) => {
|
|
825
|
+
if (cancelled || !call.ok) return
|
|
826
|
+
const granted = Array.isArray(call.result?.granted) ? call.result.granted : []
|
|
827
|
+
const allGranted = call.result?.ok === true
|
|
828
|
+
setFeatureAccess({
|
|
829
|
+
claimManage: allGranted || hasFeature(granted, 'warranty_claims.claim.manage'),
|
|
830
|
+
claimDelete: allGranted || hasFeature(granted, 'warranty_claims.claim.delete'),
|
|
831
|
+
receivingManage: allGranted || hasFeature(granted, 'warranty_claims.receiving.manage'),
|
|
832
|
+
salesReturnsCreate: allGranted || hasFeature(granted, 'sales.returns.create'),
|
|
833
|
+
salesOrdersManage: allGranted || hasFeature(granted, 'sales.orders.manage'),
|
|
834
|
+
salesCreditMemosManage: allGranted || hasFeature(granted, 'sales.credit_memos.manage'),
|
|
835
|
+
})
|
|
836
|
+
})
|
|
837
|
+
.catch(() => {
|
|
838
|
+
if (!cancelled) {
|
|
839
|
+
setFeatureAccess({
|
|
840
|
+
claimManage: false,
|
|
841
|
+
claimDelete: false,
|
|
842
|
+
receivingManage: false,
|
|
843
|
+
salesReturnsCreate: false,
|
|
844
|
+
salesOrdersManage: false,
|
|
845
|
+
salesCreditMemosManage: false,
|
|
846
|
+
})
|
|
847
|
+
}
|
|
848
|
+
})
|
|
849
|
+
return () => {
|
|
850
|
+
cancelled = true
|
|
851
|
+
}
|
|
852
|
+
}, [scopeVersion])
|
|
853
|
+
const [transitionDialog, setTransitionDialog] = React.useState<{ toStatus: ClaimStatus } | null>(null)
|
|
854
|
+
const [vendorDialogOpen, setVendorDialogOpen] = React.useState(false)
|
|
855
|
+
const [commentBody, setCommentBody] = React.useState('')
|
|
856
|
+
const [commentVisibility, setCommentVisibility] = React.useState<'internal' | 'customer'>('internal')
|
|
857
|
+
const [aiSuggestion, setAiSuggestion] = React.useState<ClaimTriageSuggestion | null>(null)
|
|
858
|
+
const [aiLoading, setAiLoading] = React.useState(false)
|
|
859
|
+
const [appliedTriageLineIds, setAppliedTriageLineIds] = React.useState<Set<string>>(new Set())
|
|
860
|
+
const [draftReplyLoading, setDraftReplyLoading] = React.useState(false)
|
|
861
|
+
const [draftReplyHidden, setDraftReplyHidden] = React.useState(false)
|
|
862
|
+
const [riskAssessment, setRiskAssessment] = React.useState<ClaimRiskAssessment>(EMPTY_RISK_ASSESSMENT)
|
|
863
|
+
const [slaAtRiskThresholdPct, setSlaAtRiskThresholdPct] = React.useState<number | undefined>(undefined)
|
|
864
|
+
const [assignDialogOpen, setAssignDialogOpen] = React.useState(false)
|
|
865
|
+
const [assignSearch, setAssignSearch] = React.useState('')
|
|
866
|
+
const [assignOptions, setAssignOptions] = React.useState<AssignableStaffMember[]>([])
|
|
867
|
+
const [assignLoading, setAssignLoading] = React.useState(false)
|
|
868
|
+
const [selectedAssigneeUserId, setSelectedAssigneeUserId] = React.useState<string>(UNASSIGNED_SELECT_VALUE)
|
|
869
|
+
const mutationContextId = React.useMemo(() => `warranty-claim:${id || 'pending'}`, [id])
|
|
870
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
871
|
+
formId: string
|
|
872
|
+
resourceKind: string
|
|
873
|
+
resourceId?: string
|
|
874
|
+
retryLastMutation: () => Promise<boolean>
|
|
875
|
+
}>({
|
|
876
|
+
contextId: mutationContextId,
|
|
877
|
+
blockedMessage: t('warranty_claims.common.saveBlocked'),
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
const mutationContext = React.useMemo(() => ({
|
|
881
|
+
formId: mutationContextId,
|
|
882
|
+
resourceKind: 'warranty_claims.claim',
|
|
883
|
+
resourceId: claim?.id ?? id,
|
|
884
|
+
retryLastMutation,
|
|
885
|
+
}), [claim?.id, id, mutationContextId, retryLastMutation])
|
|
886
|
+
|
|
887
|
+
const loadDictionaryOptions = React.useCallback(async (
|
|
888
|
+
dictionaryKey: string,
|
|
889
|
+
kind: DictionaryLabelKind,
|
|
890
|
+
): Promise<CrudFieldOption[]> => {
|
|
891
|
+
const dictionaries = await readApiResultOrThrow<{ items?: Array<{ id?: string; key?: string }> }>(
|
|
892
|
+
'/api/dictionaries',
|
|
893
|
+
undefined,
|
|
894
|
+
{
|
|
895
|
+
fallback: { items: [] },
|
|
896
|
+
errorMessage: t('warranty_claims.form.error.dictionaryLoad'),
|
|
897
|
+
},
|
|
898
|
+
)
|
|
899
|
+
const dictionary = (dictionaries.items ?? []).find((item) => item.key === dictionaryKey)
|
|
900
|
+
if (!dictionary?.id) return []
|
|
901
|
+
const entries = await readApiResultOrThrow<{ items?: unknown[] }>(
|
|
902
|
+
`/api/dictionaries/${encodeURIComponent(dictionary.id)}/entries`,
|
|
903
|
+
undefined,
|
|
904
|
+
{
|
|
905
|
+
fallback: { items: [] },
|
|
906
|
+
errorMessage: t('warranty_claims.form.error.dictionaryLoad'),
|
|
907
|
+
},
|
|
908
|
+
)
|
|
909
|
+
return (entries.items ?? [])
|
|
910
|
+
.map(normalizeDictionaryOption)
|
|
911
|
+
.filter((option): option is CrudFieldOption => option !== null)
|
|
912
|
+
.map((option) => ({
|
|
913
|
+
...option,
|
|
914
|
+
label: localizeDictionaryLabel(t, kind, option.value, option.label),
|
|
915
|
+
}))
|
|
916
|
+
}, [t])
|
|
917
|
+
|
|
918
|
+
const loadData = React.useCallback(async () => {
|
|
919
|
+
if (!id) return
|
|
920
|
+
setLoading(true)
|
|
921
|
+
setError(null)
|
|
922
|
+
setNotFound(false)
|
|
923
|
+
try {
|
|
924
|
+
const [claimPayload, linesPayload, eventsPayload, riskPayload, statsPayload, attachmentsPayload] = await Promise.all([
|
|
925
|
+
readApiResultOrThrow<{ items?: unknown[] }>(
|
|
926
|
+
`/api/warranty_claims?ids=${encodeURIComponent(id)}&page=1&pageSize=1`,
|
|
927
|
+
undefined,
|
|
928
|
+
{ fallback: { items: [] }, errorMessage: t('warranty_claims.detail.error.load') },
|
|
929
|
+
),
|
|
930
|
+
readApiResultOrThrow<{ items?: unknown[] }>(
|
|
931
|
+
`/api/warranty_claims/lines?claimId=${encodeURIComponent(id)}&page=1&pageSize=100`,
|
|
932
|
+
undefined,
|
|
933
|
+
{ fallback: { items: [] }, errorMessage: t('warranty_claims.detail.error.loadLines') },
|
|
934
|
+
),
|
|
935
|
+
readApiResultOrThrow<{ items?: unknown[] }>(
|
|
936
|
+
`/api/warranty_claims/events?claimId=${encodeURIComponent(id)}`,
|
|
937
|
+
undefined,
|
|
938
|
+
{ fallback: { items: [] }, errorMessage: t('warranty_claims.detail.error.loadEvents') },
|
|
939
|
+
),
|
|
940
|
+
readApiResultOrThrow<RiskApiResponse>(
|
|
941
|
+
`/api/warranty_claims/risk?claimId=${encodeURIComponent(id)}`,
|
|
942
|
+
undefined,
|
|
943
|
+
{
|
|
944
|
+
fallback: { ok: true, result: EMPTY_RISK_ASSESSMENT },
|
|
945
|
+
errorMessage: t('warranty_claims.detail.error.loadRisk'),
|
|
946
|
+
},
|
|
947
|
+
),
|
|
948
|
+
readApiResultOrThrow<{ ok?: boolean; result?: { slaAtRiskThresholdPct?: number } }>(
|
|
949
|
+
'/api/warranty_claims/stats',
|
|
950
|
+
undefined,
|
|
951
|
+
{ fallback: { ok: true, result: {} } },
|
|
952
|
+
),
|
|
953
|
+
readApiResultOrThrow<{ items?: unknown[] }>(
|
|
954
|
+
`/api/attachments?entityId=${encodeURIComponent('warranty_claims:warranty_claim')}&recordId=${encodeURIComponent(id)}`,
|
|
955
|
+
undefined,
|
|
956
|
+
{ fallback: { items: [] } },
|
|
957
|
+
),
|
|
958
|
+
])
|
|
959
|
+
const nextClaim = (claimPayload.items ?? []).map(normalizeClaim).find((item): item is ClaimRecord => item !== null) ?? null
|
|
960
|
+
if (!nextClaim) {
|
|
961
|
+
setClaim(null)
|
|
962
|
+
setLines([])
|
|
963
|
+
setEvents([])
|
|
964
|
+
setAttachmentCount(0)
|
|
965
|
+
setRiskAssessment(EMPTY_RISK_ASSESSMENT)
|
|
966
|
+
setNotFound(true)
|
|
967
|
+
return
|
|
968
|
+
}
|
|
969
|
+
setClaim(nextClaim)
|
|
970
|
+
setLines((linesPayload.items ?? []).map(normalizeLine).filter((item): item is ClaimLine => item !== null))
|
|
971
|
+
setEvents((eventsPayload.items ?? []).map(normalizeEvent).filter((item): item is ClaimEvent => item !== null))
|
|
972
|
+
setAttachmentCount(Array.isArray(attachmentsPayload.items) ? attachmentsPayload.items.length : 0)
|
|
973
|
+
setRiskAssessment(normalizeRiskResponse(riskPayload))
|
|
974
|
+
const nextThreshold = statsPayload?.result?.slaAtRiskThresholdPct
|
|
975
|
+
setSlaAtRiskThresholdPct(typeof nextThreshold === 'number' ? nextThreshold : undefined)
|
|
976
|
+
} catch (err) {
|
|
977
|
+
setError(err instanceof Error ? err.message : t('warranty_claims.detail.error.load'))
|
|
978
|
+
} finally {
|
|
979
|
+
setLoading(false)
|
|
980
|
+
}
|
|
981
|
+
}, [id, scopeVersion, t])
|
|
982
|
+
|
|
983
|
+
React.useEffect(() => {
|
|
984
|
+
void loadData()
|
|
985
|
+
}, [loadData])
|
|
986
|
+
|
|
987
|
+
useAppEvent('warranty_claims.claim.*', (event) => {
|
|
988
|
+
const eventClaimId = toStringOrNull(event.payload.claimId)
|
|
989
|
+
if (eventClaimId === id) void loadData()
|
|
990
|
+
}, [id, loadData])
|
|
991
|
+
|
|
992
|
+
const referencedUserIds = React.useMemo(() => {
|
|
993
|
+
const collected = new Set<string>()
|
|
994
|
+
if (claim?.assigneeUserId) collected.add(claim.assigneeUserId)
|
|
995
|
+
for (const event of events) {
|
|
996
|
+
if (event.actorUserId) collected.add(event.actorUserId)
|
|
997
|
+
if (event.kind === 'assignment') {
|
|
998
|
+
const assigneeUserId = toStringOrNull(event.payload?.assigneeUserId)
|
|
999
|
+
if (assigneeUserId) collected.add(assigneeUserId)
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return [...collected]
|
|
1003
|
+
}, [claim?.assigneeUserId, events])
|
|
1004
|
+
const userNames = useUserDisplayNames(referencedUserIds)
|
|
1005
|
+
|
|
1006
|
+
React.useEffect(() => {
|
|
1007
|
+
if (!assignDialogOpen) return
|
|
1008
|
+
const controller = new AbortController()
|
|
1009
|
+
setAssignLoading(true)
|
|
1010
|
+
fetchAssignableStaffMembersPage(assignSearch, { page: 1, pageSize: 24, signal: controller.signal })
|
|
1011
|
+
.then((page) => setAssignOptions(page.items))
|
|
1012
|
+
.catch(() => setAssignOptions([]))
|
|
1013
|
+
.finally(() => {
|
|
1014
|
+
if (!controller.signal.aborted) setAssignLoading(false)
|
|
1015
|
+
})
|
|
1016
|
+
return () => controller.abort()
|
|
1017
|
+
}, [assignDialogOpen, assignSearch])
|
|
1018
|
+
|
|
1019
|
+
const claimCustomerId = claim?.customerId ?? null
|
|
1020
|
+
React.useEffect(() => {
|
|
1021
|
+
if (!claimCustomerId || customerLink?.customerId === claimCustomerId) return
|
|
1022
|
+
const controller = new AbortController()
|
|
1023
|
+
const idsParam = encodeURIComponent(claimCustomerId)
|
|
1024
|
+
const hasMatch = (payload: { items?: unknown[] } | null) =>
|
|
1025
|
+
(payload?.items ?? []).some((item) => isRecord(item) && toStringOrNull(item.id) === claimCustomerId)
|
|
1026
|
+
const resolveCustomerHref = async (): Promise<string | null> => {
|
|
1027
|
+
const people = await apiCall<{ items?: unknown[] }>(
|
|
1028
|
+
`/api/customers/people?ids=${idsParam}&pageSize=1`,
|
|
1029
|
+
{ signal: controller.signal },
|
|
1030
|
+
{ fallback: { items: [] } },
|
|
1031
|
+
)
|
|
1032
|
+
if (people.ok && hasMatch(people.result)) return `/backend/customers/people/${claimCustomerId}`
|
|
1033
|
+
const companies = await apiCall<{ items?: unknown[] }>(
|
|
1034
|
+
`/api/customers/companies?ids=${idsParam}&pageSize=1`,
|
|
1035
|
+
{ signal: controller.signal },
|
|
1036
|
+
{ fallback: { items: [] } },
|
|
1037
|
+
)
|
|
1038
|
+
if (companies.ok && hasMatch(companies.result)) return `/backend/customers/companies/${claimCustomerId}`
|
|
1039
|
+
return null
|
|
1040
|
+
}
|
|
1041
|
+
resolveCustomerHref()
|
|
1042
|
+
.then((href) => {
|
|
1043
|
+
if (controller.signal.aborted || !href) return
|
|
1044
|
+
setCustomerLink({ customerId: claimCustomerId, href })
|
|
1045
|
+
})
|
|
1046
|
+
.catch(() => {})
|
|
1047
|
+
return () => controller.abort()
|
|
1048
|
+
}, [claimCustomerId, customerLink])
|
|
1049
|
+
|
|
1050
|
+
const filteredTimelineEvents = React.useMemo(
|
|
1051
|
+
() => filterTimelineEvents(events, timelineFilter),
|
|
1052
|
+
[events, timelineFilter],
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
const currentStatus = typeof claim?.status === 'string' ? claim.status : 'draft'
|
|
1056
|
+
const nextStatuses = React.useMemo(() => {
|
|
1057
|
+
if (!claim || !(currentStatus in CLAIM_STATUS_TRANSITIONS)) return []
|
|
1058
|
+
return CLAIM_STATUS_TRANSITIONS[currentStatus as ClaimStatus] ?? []
|
|
1059
|
+
}, [claim, currentStatus])
|
|
1060
|
+
const primaryNextStatus = React.useMemo(
|
|
1061
|
+
() => nextStatuses.find((status) => status !== 'cancelled' && status !== 'rejected' && status !== 'info_requested') ?? nextStatuses[0] ?? null,
|
|
1062
|
+
[nextStatuses],
|
|
1063
|
+
)
|
|
1064
|
+
const resolutionStatuses = React.useMemo(
|
|
1065
|
+
() => nextStatuses.filter((status) => status !== primaryNextStatus),
|
|
1066
|
+
[nextStatuses, primaryNextStatus],
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
const noValue = t('warranty_claims.common.noValue')
|
|
1070
|
+
const linesEditable = LINE_EDITABLE_CLAIM_STATUSES.has(currentStatus)
|
|
1071
|
+
const linesInlineEditable = linesEditable && featureAccess.claimManage
|
|
1072
|
+
// FLOW-04 gating. `claimActive` = not fully terminal (closed/cancelled): reassignment and
|
|
1073
|
+
// vendor-recovery (which requires resolved lines) stay available. `claimMutable` is stricter —
|
|
1074
|
+
// it also excludes resolved/rejected, where header fields and lines are server-locked — so the
|
|
1075
|
+
// header "Edit" and AI mutating controls are hidden there instead of leading to a dead form.
|
|
1076
|
+
const claimActive = !isTerminal(currentStatus)
|
|
1077
|
+
const claimMutable = claimActive && currentStatus !== 'resolved' && currentStatus !== 'rejected'
|
|
1078
|
+
const allowedDispositions = React.useMemo(
|
|
1079
|
+
() => resolveClaimTypeUiConfig(claim?.claimType).allowedDispositions,
|
|
1080
|
+
[claim?.claimType],
|
|
1081
|
+
)
|
|
1082
|
+
const currentLineDisposition = lineDialog?.mode === 'edit' ? lineDialog.line.disposition : null
|
|
1083
|
+
const riskSignals = riskAssessment.signals
|
|
1084
|
+
const assigneeDisplayName = claim?.assigneeUserId
|
|
1085
|
+
? claim.assigneeName ?? userNames[claim.assigneeUserId] ?? t('warranty_claims.detail.unknownUser')
|
|
1086
|
+
: t('warranty_claims.detail.unassigned')
|
|
1087
|
+
const eligibleVendorLines = React.useMemo(
|
|
1088
|
+
() => lines.filter((line) => line.lineStatus === 'resolved' && !line.vendorClaimLineId),
|
|
1089
|
+
[lines],
|
|
1090
|
+
)
|
|
1091
|
+
const assignableOptions = React.useMemo(() => {
|
|
1092
|
+
const byUserId = new Map<string, AssignableStaffMember>()
|
|
1093
|
+
for (const option of assignOptions) byUserId.set(option.userId, option)
|
|
1094
|
+
if (claim?.assigneeUserId && !byUserId.has(claim.assigneeUserId)) {
|
|
1095
|
+
byUserId.set(claim.assigneeUserId, {
|
|
1096
|
+
teamMemberId: claim.assigneeUserId,
|
|
1097
|
+
userId: claim.assigneeUserId,
|
|
1098
|
+
displayName: userNames[claim.assigneeUserId] ?? t('warranty_claims.detail.unknownUser'),
|
|
1099
|
+
email: null,
|
|
1100
|
+
teamName: null,
|
|
1101
|
+
})
|
|
1102
|
+
}
|
|
1103
|
+
return Array.from(byUserId.values())
|
|
1104
|
+
}, [assignOptions, claim?.assigneeUserId, userNames, t])
|
|
1105
|
+
|
|
1106
|
+
const openAssignDialog = React.useCallback(() => {
|
|
1107
|
+
setSelectedAssigneeUserId(claim?.assigneeUserId ?? UNASSIGNED_SELECT_VALUE)
|
|
1108
|
+
setAssignSearch('')
|
|
1109
|
+
setAssignDialogOpen(true)
|
|
1110
|
+
}, [claim?.assigneeUserId])
|
|
1111
|
+
|
|
1112
|
+
const runClaimPost = React.useCallback(async (
|
|
1113
|
+
endpoint: string,
|
|
1114
|
+
body: Record<string, unknown>,
|
|
1115
|
+
successKey: string,
|
|
1116
|
+
) => {
|
|
1117
|
+
if (!claim) return
|
|
1118
|
+
try {
|
|
1119
|
+
await runMutation({
|
|
1120
|
+
operation: async () => {
|
|
1121
|
+
const call = await withScopedApiRequestHeaders(
|
|
1122
|
+
buildOptimisticLockHeader(claim.updatedAt),
|
|
1123
|
+
() => apiCall(endpoint, {
|
|
1124
|
+
method: 'POST',
|
|
1125
|
+
headers: { 'content-type': 'application/json' },
|
|
1126
|
+
body: JSON.stringify(body),
|
|
1127
|
+
}),
|
|
1128
|
+
)
|
|
1129
|
+
if (!call.ok) throw buildConflictError(call, t('warranty_claims.detail.error.action'), t)
|
|
1130
|
+
return call
|
|
1131
|
+
},
|
|
1132
|
+
context: mutationContext,
|
|
1133
|
+
mutationPayload: body,
|
|
1134
|
+
})
|
|
1135
|
+
flash(t(successKey), 'success')
|
|
1136
|
+
await loadData()
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1139
|
+
const message = translateErrorMessage(err, t, 'warranty_claims.detail.error.action')
|
|
1140
|
+
flash(message, 'error')
|
|
1141
|
+
}
|
|
1142
|
+
}, [claim, loadData, mutationContext, runMutation, t])
|
|
1143
|
+
|
|
1144
|
+
const createSalesReturn = React.useCallback(async () => {
|
|
1145
|
+
if (!claim) return
|
|
1146
|
+
const payload = { claimId: claim.id }
|
|
1147
|
+
try {
|
|
1148
|
+
const call = await runMutation({
|
|
1149
|
+
operation: async () => {
|
|
1150
|
+
const response = await withScopedApiRequestHeaders(
|
|
1151
|
+
buildOptimisticLockHeader(claim.updatedAt),
|
|
1152
|
+
() => apiCall<{ salesReturnId?: string; skippedLineIds?: string[] }>('/api/warranty_claims/sales-return', {
|
|
1153
|
+
method: 'POST',
|
|
1154
|
+
headers: { 'content-type': 'application/json' },
|
|
1155
|
+
body: JSON.stringify(payload),
|
|
1156
|
+
}),
|
|
1157
|
+
)
|
|
1158
|
+
if (!response.ok) throw buildConflictError(response, t('warranty_claims.detail.error.action'), t)
|
|
1159
|
+
return response
|
|
1160
|
+
},
|
|
1161
|
+
context: mutationContext,
|
|
1162
|
+
mutationPayload: payload,
|
|
1163
|
+
})
|
|
1164
|
+
flash(t('warranty_claims.detail.createSalesReturnSuccess'), 'success')
|
|
1165
|
+
const skippedCount = call?.result?.skippedLineIds?.length ?? 0
|
|
1166
|
+
if (skippedCount > 0) {
|
|
1167
|
+
flash(t('warranty_claims.detail.createSalesReturnSkipped', undefined, { count: skippedCount }), 'info')
|
|
1168
|
+
}
|
|
1169
|
+
await loadData()
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1172
|
+
const message = translateErrorMessage(err, t, 'warranty_claims.detail.error.action')
|
|
1173
|
+
flash(message, 'error')
|
|
1174
|
+
}
|
|
1175
|
+
}, [claim, loadData, mutationContext, runMutation, t])
|
|
1176
|
+
|
|
1177
|
+
const createReplacementOrder = React.useCallback(async () => {
|
|
1178
|
+
if (!claim) return
|
|
1179
|
+
const payload = { claimId: claim.id }
|
|
1180
|
+
try {
|
|
1181
|
+
const call = await runMutation({
|
|
1182
|
+
operation: async () => {
|
|
1183
|
+
const response = await withScopedApiRequestHeaders(
|
|
1184
|
+
buildOptimisticLockHeader(claim.updatedAt),
|
|
1185
|
+
() => apiCall<{ replacementOrderId?: string; skippedLineIds?: string[] }>('/api/warranty_claims/replacement-order', {
|
|
1186
|
+
method: 'POST',
|
|
1187
|
+
headers: { 'content-type': 'application/json' },
|
|
1188
|
+
body: JSON.stringify(payload),
|
|
1189
|
+
}),
|
|
1190
|
+
)
|
|
1191
|
+
if (!response.ok) throw buildConflictError(response, t('warranty_claims.detail.error.action'), t)
|
|
1192
|
+
return response
|
|
1193
|
+
},
|
|
1194
|
+
context: mutationContext,
|
|
1195
|
+
mutationPayload: payload,
|
|
1196
|
+
})
|
|
1197
|
+
flash(t('warranty_claims.detail.createReplacementOrderSuccess'), 'success')
|
|
1198
|
+
const skippedCount = call?.result?.skippedLineIds?.length ?? 0
|
|
1199
|
+
if (skippedCount > 0) {
|
|
1200
|
+
flash(t('warranty_claims.detail.createReplacementOrderSkipped', undefined, { count: skippedCount }), 'info')
|
|
1201
|
+
}
|
|
1202
|
+
await loadData()
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1205
|
+
const message = translateErrorMessage(err, t, 'warranty_claims.detail.error.action')
|
|
1206
|
+
flash(message, 'error')
|
|
1207
|
+
}
|
|
1208
|
+
}, [claim, loadData, mutationContext, runMutation, t])
|
|
1209
|
+
|
|
1210
|
+
const createCreditMemo = React.useCallback(async () => {
|
|
1211
|
+
if (!claim) return
|
|
1212
|
+
const payload = { claimId: claim.id }
|
|
1213
|
+
try {
|
|
1214
|
+
const call = await runMutation({
|
|
1215
|
+
operation: async () => {
|
|
1216
|
+
const response = await withScopedApiRequestHeaders(
|
|
1217
|
+
buildOptimisticLockHeader(claim.updatedAt),
|
|
1218
|
+
() => apiCall<{ creditMemoId?: string; skippedLineIds?: string[]; grandTotalGrossAmount?: string; currencyCode?: string }>('/api/warranty_claims/credit-memo', {
|
|
1219
|
+
method: 'POST',
|
|
1220
|
+
headers: { 'content-type': 'application/json' },
|
|
1221
|
+
body: JSON.stringify(payload),
|
|
1222
|
+
}),
|
|
1223
|
+
)
|
|
1224
|
+
if (!response.ok) throw buildConflictError(response, t('warranty_claims.detail.error.action'), t)
|
|
1225
|
+
return response
|
|
1226
|
+
},
|
|
1227
|
+
context: mutationContext,
|
|
1228
|
+
mutationPayload: payload,
|
|
1229
|
+
})
|
|
1230
|
+
flash(
|
|
1231
|
+
t('warranty_claims.detail.createCreditMemoSuccess', undefined, {
|
|
1232
|
+
amount: call?.result?.grandTotalGrossAmount ?? '',
|
|
1233
|
+
currency: call?.result?.currencyCode ?? '',
|
|
1234
|
+
}),
|
|
1235
|
+
'success',
|
|
1236
|
+
)
|
|
1237
|
+
const skippedCount = call?.result?.skippedLineIds?.length ?? 0
|
|
1238
|
+
if (skippedCount > 0) {
|
|
1239
|
+
flash(t('warranty_claims.detail.createCreditMemoSkipped', undefined, { count: skippedCount }), 'info')
|
|
1240
|
+
}
|
|
1241
|
+
await loadData()
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1244
|
+
const message = translateErrorMessage(err, t, 'warranty_claims.detail.error.action')
|
|
1245
|
+
flash(message, 'error')
|
|
1246
|
+
}
|
|
1247
|
+
}, [claim, loadData, mutationContext, runMutation, t])
|
|
1248
|
+
|
|
1249
|
+
const performAssignment = React.useCallback(async (assigneeUserId: string | null) => {
|
|
1250
|
+
if (!claim) return false
|
|
1251
|
+
const payload = { id: claim.id, assigneeUserId }
|
|
1252
|
+
try {
|
|
1253
|
+
await runMutation({
|
|
1254
|
+
operation: async () => {
|
|
1255
|
+
const call = await withScopedApiRequestHeaders(
|
|
1256
|
+
buildOptimisticLockHeader(claim.updatedAt),
|
|
1257
|
+
() => apiCall('/api/warranty_claims/assign', {
|
|
1258
|
+
method: 'POST',
|
|
1259
|
+
headers: { 'content-type': 'application/json' },
|
|
1260
|
+
body: JSON.stringify(payload),
|
|
1261
|
+
}),
|
|
1262
|
+
)
|
|
1263
|
+
if (!call.ok) throw buildConflictError(call, t('warranty_claims.detail.error.action'), t)
|
|
1264
|
+
return call
|
|
1265
|
+
},
|
|
1266
|
+
context: mutationContext,
|
|
1267
|
+
mutationPayload: payload,
|
|
1268
|
+
})
|
|
1269
|
+
flash(t('warranty_claims.list.flash.assigned'), 'success')
|
|
1270
|
+
await loadData()
|
|
1271
|
+
return true
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return false
|
|
1274
|
+
flash(translateErrorMessage(err, t, 'warranty_claims.detail.error.action'), 'error')
|
|
1275
|
+
return false
|
|
1276
|
+
}
|
|
1277
|
+
}, [claim, loadData, mutationContext, runMutation, t])
|
|
1278
|
+
|
|
1279
|
+
const assignClaim = React.useCallback(async () => {
|
|
1280
|
+
const assigneeUserId = selectedAssigneeUserId === UNASSIGNED_SELECT_VALUE ? null : selectedAssigneeUserId
|
|
1281
|
+
const success = await performAssignment(assigneeUserId)
|
|
1282
|
+
if (success) setAssignDialogOpen(false)
|
|
1283
|
+
}, [performAssignment, selectedAssigneeUserId])
|
|
1284
|
+
|
|
1285
|
+
const assignToMe = React.useCallback(async () => {
|
|
1286
|
+
if (!currentUserId) return
|
|
1287
|
+
await performAssignment(currentUserId)
|
|
1288
|
+
}, [currentUserId, performAssignment])
|
|
1289
|
+
|
|
1290
|
+
const loadAiSuggestion = React.useCallback(async () => {
|
|
1291
|
+
if (!id) return
|
|
1292
|
+
setAiLoading(true)
|
|
1293
|
+
try {
|
|
1294
|
+
const suggestion = await readApiResultOrThrow<ClaimTriageSuggestion>(
|
|
1295
|
+
`/api/warranty_claims/ai/suggest?claimId=${encodeURIComponent(id)}`,
|
|
1296
|
+
undefined,
|
|
1297
|
+
{ errorMessage: t('warranty_claims.detail.error.action') },
|
|
1298
|
+
)
|
|
1299
|
+
setAiSuggestion(suggestion)
|
|
1300
|
+
setAppliedTriageLineIds(new Set())
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
const message = translateErrorMessage(err, t, 'warranty_claims.detail.error.action')
|
|
1303
|
+
flash(message, 'error')
|
|
1304
|
+
} finally {
|
|
1305
|
+
setAiLoading(false)
|
|
1306
|
+
}
|
|
1307
|
+
}, [id, t])
|
|
1308
|
+
|
|
1309
|
+
const applyAiDisposition = React.useCallback(async (lineId: string, disposition: string) => {
|
|
1310
|
+
if (!claim) return
|
|
1311
|
+
const line = lines.find((candidate) => candidate.id === lineId)
|
|
1312
|
+
if (!line) return
|
|
1313
|
+
try {
|
|
1314
|
+
await runMutation({
|
|
1315
|
+
operation: () => withScopedApiRequestHeaders(
|
|
1316
|
+
buildOptimisticLockHeader(line.updatedAt),
|
|
1317
|
+
() => updateCrud('warranty_claims/lines', { id: lineId, claimId: claim.id, disposition }, {
|
|
1318
|
+
errorMessage: t('warranty_claims.detail.lines.error.save'),
|
|
1319
|
+
}),
|
|
1320
|
+
),
|
|
1321
|
+
context: mutationContext,
|
|
1322
|
+
mutationPayload: { id: lineId, claimId: claim.id, disposition },
|
|
1323
|
+
})
|
|
1324
|
+
flash(t('warranty_claims.detail.lines.flash.saved'), 'success')
|
|
1325
|
+
setAppliedTriageLineIds((prev) => new Set(prev).add(lineId))
|
|
1326
|
+
await loadData()
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1329
|
+
const message = translateErrorMessage(err, t, 'warranty_claims.detail.error.action')
|
|
1330
|
+
flash(message, 'error')
|
|
1331
|
+
}
|
|
1332
|
+
}, [claim, lines, loadData, mutationContext, runMutation, t])
|
|
1333
|
+
|
|
1334
|
+
const handleTransition = React.useCallback(async (toStatus: ClaimStatus, values?: TransitionFormValues) => {
|
|
1335
|
+
if (!claim) return
|
|
1336
|
+
await runClaimPost(
|
|
1337
|
+
toStatus === 'submitted' && currentStatus === 'draft'
|
|
1338
|
+
? '/api/warranty_claims/submit'
|
|
1339
|
+
: '/api/warranty_claims/transition',
|
|
1340
|
+
toStatus === 'submitted' && currentStatus === 'draft'
|
|
1341
|
+
? { id: claim.id }
|
|
1342
|
+
: {
|
|
1343
|
+
id: claim.id,
|
|
1344
|
+
toStatus,
|
|
1345
|
+
rejectionReasonCode: nullableText(values?.rejectionReasonCode),
|
|
1346
|
+
resolutionSummary: nullableText(values?.resolutionSummary),
|
|
1347
|
+
},
|
|
1348
|
+
'warranty_claims.detail.flash.transitioned',
|
|
1349
|
+
)
|
|
1350
|
+
}, [claim, currentStatus, runClaimPost])
|
|
1351
|
+
|
|
1352
|
+
const generateSupplierRecovery = React.useCallback(async ({
|
|
1353
|
+
lineIds,
|
|
1354
|
+
vendorName,
|
|
1355
|
+
}: {
|
|
1356
|
+
lineIds: string[]
|
|
1357
|
+
vendorName: string
|
|
1358
|
+
}) => {
|
|
1359
|
+
if (!claim) return
|
|
1360
|
+
if (!lineIds.length || !vendorName.trim()) {
|
|
1361
|
+
flash(t('warranty_claims.errors.vendorRecoveryNeedsResolvedLines'), 'error')
|
|
1362
|
+
return
|
|
1363
|
+
}
|
|
1364
|
+
await runClaimPost(
|
|
1365
|
+
'/api/warranty_claims/vendor-recovery',
|
|
1366
|
+
{
|
|
1367
|
+
claimId: claim.id,
|
|
1368
|
+
lineIds,
|
|
1369
|
+
vendorName: vendorName.trim(),
|
|
1370
|
+
vendorRef: null,
|
|
1371
|
+
},
|
|
1372
|
+
'warranty_claims.detail.vendorRecovery.created',
|
|
1373
|
+
)
|
|
1374
|
+
}, [claim, runClaimPost, t])
|
|
1375
|
+
|
|
1376
|
+
const confirmAndTransition = React.useCallback(async (toStatus: ClaimStatus) => {
|
|
1377
|
+
if (!claim) return
|
|
1378
|
+
if (toStatus === 'rejected') {
|
|
1379
|
+
setTransitionDialog({ toStatus })
|
|
1380
|
+
return
|
|
1381
|
+
}
|
|
1382
|
+
if (toStatus === 'cancelled') {
|
|
1383
|
+
const confirmed = await confirm({
|
|
1384
|
+
title: t('warranty_claims.detail.confirm.cancelTitle'),
|
|
1385
|
+
variant: 'destructive',
|
|
1386
|
+
})
|
|
1387
|
+
if (!confirmed) return
|
|
1388
|
+
} else {
|
|
1389
|
+
const confirmed = await confirm({
|
|
1390
|
+
title: t('warranty_claims.detail.confirm.transitionTitle'),
|
|
1391
|
+
})
|
|
1392
|
+
if (!confirmed) return
|
|
1393
|
+
}
|
|
1394
|
+
await handleTransition(toStatus)
|
|
1395
|
+
}, [claim, confirm, handleTransition, t])
|
|
1396
|
+
|
|
1397
|
+
const deleteClaim = React.useCallback(async () => {
|
|
1398
|
+
if (!claim || !featureAccess.claimDelete || !DELETABLE_CLAIM_STATUSES.has(currentStatus)) return
|
|
1399
|
+
const confirmed = await confirm({
|
|
1400
|
+
title: t('warranty_claims.audit.claim.delete'),
|
|
1401
|
+
variant: 'destructive',
|
|
1402
|
+
})
|
|
1403
|
+
if (!confirmed) return
|
|
1404
|
+
try {
|
|
1405
|
+
await runMutation({
|
|
1406
|
+
operation: () => withScopedApiRequestHeaders(
|
|
1407
|
+
buildOptimisticLockHeader(claim.updatedAt),
|
|
1408
|
+
() => deleteCrud('warranty_claims', claim.id, {
|
|
1409
|
+
errorMessage: t('warranty_claims.edit.error.delete'),
|
|
1410
|
+
}),
|
|
1411
|
+
),
|
|
1412
|
+
context: mutationContext,
|
|
1413
|
+
mutationPayload: { id: claim.id },
|
|
1414
|
+
})
|
|
1415
|
+
flash(t('warranty_claims.edit.deleted'), 'success')
|
|
1416
|
+
router.push('/backend/warranty_claims')
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1419
|
+
flash(translateErrorMessage(err, t, 'warranty_claims.edit.error.delete'), 'error')
|
|
1420
|
+
}
|
|
1421
|
+
}, [claim, confirm, currentStatus, featureAccess.claimDelete, loadData, mutationContext, router, runMutation, t])
|
|
1422
|
+
|
|
1423
|
+
const headerMoreActions = React.useMemo(() => {
|
|
1424
|
+
if (!claim) return []
|
|
1425
|
+
const items = [
|
|
1426
|
+
...(claimMutable ? [{
|
|
1427
|
+
id: 'edit',
|
|
1428
|
+
label: t('warranty_claims.detail.actions.edit'),
|
|
1429
|
+
icon: Pencil,
|
|
1430
|
+
onSelect: () => router.push(`/backend/warranty_claims/${claim.id}/edit`),
|
|
1431
|
+
}] : []),
|
|
1432
|
+
{
|
|
1433
|
+
id: 'copy-link',
|
|
1434
|
+
label: t('warranty_claims.detail.copyLink'),
|
|
1435
|
+
icon: Copy,
|
|
1436
|
+
onSelect: async () => {
|
|
1437
|
+
try {
|
|
1438
|
+
await navigator.clipboard.writeText(window.location.href)
|
|
1439
|
+
flash(t('warranty_claims.detail.linkCopied'), 'success')
|
|
1440
|
+
} catch {
|
|
1441
|
+
flash(t('warranty_claims.detail.copyFailed', 'Could not copy to the clipboard.'), 'error')
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
},
|
|
1445
|
+
]
|
|
1446
|
+
if (claim.claimNumber) {
|
|
1447
|
+
items.push({
|
|
1448
|
+
id: 'copy-number',
|
|
1449
|
+
label: t('warranty_claims.detail.copyClaimNumber', 'Copy claim number'),
|
|
1450
|
+
icon: Hash,
|
|
1451
|
+
onSelect: async () => {
|
|
1452
|
+
try {
|
|
1453
|
+
await navigator.clipboard.writeText(claim.claimNumber ?? '')
|
|
1454
|
+
flash(t('warranty_claims.detail.claimNumberCopied', 'Claim number copied.'), 'success')
|
|
1455
|
+
} catch {
|
|
1456
|
+
flash(t('warranty_claims.detail.copyFailed', 'Could not copy to the clipboard.'), 'error')
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
})
|
|
1460
|
+
}
|
|
1461
|
+
if (claim.orderId) {
|
|
1462
|
+
items.push({
|
|
1463
|
+
id: 'view-order',
|
|
1464
|
+
label: claim.orderNumber ?? t('warranty_claims.detail.viewOrder'),
|
|
1465
|
+
icon: Copy,
|
|
1466
|
+
onSelect: () => router.push(`/backend/sales/documents/${claim.orderId}`),
|
|
1467
|
+
})
|
|
1468
|
+
}
|
|
1469
|
+
if (featureAccess.claimManage && claimActive) {
|
|
1470
|
+
items.push({
|
|
1471
|
+
id: 'assign',
|
|
1472
|
+
label: t('warranty_claims.detail.reassign'),
|
|
1473
|
+
icon: UserRound,
|
|
1474
|
+
onSelect: openAssignDialog,
|
|
1475
|
+
})
|
|
1476
|
+
if (currentUserId && claim.assigneeUserId !== currentUserId) {
|
|
1477
|
+
items.push({
|
|
1478
|
+
id: 'assign-to-me',
|
|
1479
|
+
label: t('warranty_claims.detail.assignToMe'),
|
|
1480
|
+
icon: UserRound,
|
|
1481
|
+
onSelect: () => { void assignToMe() },
|
|
1482
|
+
})
|
|
1483
|
+
}
|
|
1484
|
+
if (claim.claimType !== 'vendor_recovery' && eligibleVendorLines.length > 0) {
|
|
1485
|
+
items.push({
|
|
1486
|
+
id: 'vendor-recovery',
|
|
1487
|
+
label: t('warranty_claims.detail.actions.vendorRecovery'),
|
|
1488
|
+
icon: RefreshCw,
|
|
1489
|
+
onSelect: () => setVendorDialogOpen(true),
|
|
1490
|
+
})
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return items
|
|
1494
|
+
}, [assignToMe, claim, claimActive, claimMutable, currentUserId, eligibleVendorLines.length, featureAccess.claimManage, openAssignDialog, router, t])
|
|
1495
|
+
|
|
1496
|
+
const submitComment = React.useCallback(async () => {
|
|
1497
|
+
if (!claim || !commentBody.trim()) return
|
|
1498
|
+
try {
|
|
1499
|
+
await runMutation({
|
|
1500
|
+
operation: async () => {
|
|
1501
|
+
const call = await withScopedApiRequestHeaders(
|
|
1502
|
+
buildOptimisticLockHeader(claim.updatedAt),
|
|
1503
|
+
() => apiCall('/api/warranty_claims/events', {
|
|
1504
|
+
method: 'POST',
|
|
1505
|
+
headers: { 'content-type': 'application/json' },
|
|
1506
|
+
body: JSON.stringify({
|
|
1507
|
+
claimId: claim.id,
|
|
1508
|
+
body: commentBody.trim(),
|
|
1509
|
+
visibility: commentVisibility,
|
|
1510
|
+
}),
|
|
1511
|
+
}),
|
|
1512
|
+
)
|
|
1513
|
+
if (!call.ok) throw buildConflictError(call, t('warranty_claims.detail.error.action'), t)
|
|
1514
|
+
return call
|
|
1515
|
+
},
|
|
1516
|
+
context: mutationContext,
|
|
1517
|
+
mutationPayload: {
|
|
1518
|
+
claimId: claim.id,
|
|
1519
|
+
body: commentBody.trim(),
|
|
1520
|
+
visibility: commentVisibility,
|
|
1521
|
+
},
|
|
1522
|
+
})
|
|
1523
|
+
setCommentBody('')
|
|
1524
|
+
flash(t('warranty_claims.detail.flash.commentAdded'), 'success')
|
|
1525
|
+
await loadData()
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1528
|
+
const message = translateErrorMessage(err, t, 'warranty_claims.detail.error.comment')
|
|
1529
|
+
flash(message, 'error')
|
|
1530
|
+
}
|
|
1531
|
+
}, [claim, commentBody, commentVisibility, loadData, mutationContext, runMutation, t])
|
|
1532
|
+
|
|
1533
|
+
const draftReplyWithAi = React.useCallback(async () => {
|
|
1534
|
+
if (!claim) return
|
|
1535
|
+
setDraftReplyLoading(true)
|
|
1536
|
+
try {
|
|
1537
|
+
const call = await runMutation({
|
|
1538
|
+
operation: async () => {
|
|
1539
|
+
const draftCall = await apiCall('/api/warranty_claims/ai/draft-reply', {
|
|
1540
|
+
method: 'POST',
|
|
1541
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1542
|
+
body: JSON.stringify({ claimId: claim.id }),
|
|
1543
|
+
})
|
|
1544
|
+
if (isDraftReplyNotConfigured(draftCall.result)) return draftCall
|
|
1545
|
+
if (!draftCall.ok) throw new Error(readErrorKey(draftCall.result) ?? 'warranty_claims.ai.draftError')
|
|
1546
|
+
return draftCall
|
|
1547
|
+
},
|
|
1548
|
+
context: mutationContext,
|
|
1549
|
+
mutationPayload: { claimId: claim.id },
|
|
1550
|
+
})
|
|
1551
|
+
if (isDraftReplyNotConfigured(call.result)) {
|
|
1552
|
+
setDraftReplyHidden(true)
|
|
1553
|
+
return
|
|
1554
|
+
}
|
|
1555
|
+
const draft = readDraftReply(call.result)
|
|
1556
|
+
if (!draft) {
|
|
1557
|
+
flash(t('warranty_claims.ai.draftError'), 'error')
|
|
1558
|
+
return
|
|
1559
|
+
}
|
|
1560
|
+
setCommentBody(draft)
|
|
1561
|
+
setCommentVisibility('customer')
|
|
1562
|
+
} catch (err) {
|
|
1563
|
+
flash(translateErrorMessage(err, t, 'warranty_claims.ai.draftError'), 'error')
|
|
1564
|
+
} finally {
|
|
1565
|
+
setDraftReplyLoading(false)
|
|
1566
|
+
}
|
|
1567
|
+
}, [claim, mutationContext, runMutation, t])
|
|
1568
|
+
|
|
1569
|
+
const saveLineInlineField = React.useCallback<InlineLineSaveHandler>(async (line, field, value) => {
|
|
1570
|
+
if (!claim) return
|
|
1571
|
+
const payload: Record<string, unknown> = { id: line.id, claimId: claim.id, [field]: value }
|
|
1572
|
+
try {
|
|
1573
|
+
await runMutation({
|
|
1574
|
+
operation: () => withScopedApiRequestHeaders(
|
|
1575
|
+
buildOptimisticLockHeader(line.updatedAt),
|
|
1576
|
+
() => updateCrud('warranty_claims/lines', payload, {
|
|
1577
|
+
errorMessage: t('warranty_claims.detail.lines.error.save'),
|
|
1578
|
+
}),
|
|
1579
|
+
),
|
|
1580
|
+
context: mutationContext,
|
|
1581
|
+
mutationPayload: payload,
|
|
1582
|
+
})
|
|
1583
|
+
flash(t('warranty_claims.detail.lines.flash.saved'), 'success')
|
|
1584
|
+
await loadData()
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
if (surfaceRecordConflict(err, t, { onRefresh: loadData })) return
|
|
1587
|
+
flash(translateErrorMessage(err, t, 'warranty_claims.detail.lines.error.save'), 'error')
|
|
1588
|
+
}
|
|
1589
|
+
}, [claim, loadData, mutationContext, runMutation, t])
|
|
1590
|
+
|
|
1591
|
+
const lineFields = React.useMemo<CrudField[]>(() => [
|
|
1592
|
+
{
|
|
1593
|
+
id: 'productLookup',
|
|
1594
|
+
label: t('warranty_claims.form.productLookup'),
|
|
1595
|
+
type: 'custom',
|
|
1596
|
+
layout: 'full',
|
|
1597
|
+
component: ({ values, setFormValue }) => (
|
|
1598
|
+
<ClaimLineProductPicker
|
|
1599
|
+
value={{
|
|
1600
|
+
productId: nullableText(values?.productId),
|
|
1601
|
+
variantId: nullableText(values?.variantId),
|
|
1602
|
+
productName: nullableText(values?.productName),
|
|
1603
|
+
sku: nullableText(values?.sku),
|
|
1604
|
+
}}
|
|
1605
|
+
onPick={(pick: ClaimProductPick) => {
|
|
1606
|
+
setFormValue?.('productId', pick.productId)
|
|
1607
|
+
setFormValue?.('variantId', pick.variantId)
|
|
1608
|
+
setFormValue?.('sku', pick.sku)
|
|
1609
|
+
setFormValue?.('productName', pick.productName)
|
|
1610
|
+
}}
|
|
1611
|
+
onClear={() => {
|
|
1612
|
+
setFormValue?.('productId', null)
|
|
1613
|
+
setFormValue?.('variantId', null)
|
|
1614
|
+
}}
|
|
1615
|
+
hideLabel
|
|
1616
|
+
/>
|
|
1617
|
+
),
|
|
1618
|
+
},
|
|
1619
|
+
{ id: 'productName', label: t('warranty_claims.form.productName'), type: 'text', layout: 'half' },
|
|
1620
|
+
{ id: 'sku', label: t('warranty_claims.form.sku'), type: 'text', layout: 'half' },
|
|
1621
|
+
{ id: 'serialNumber', label: t('warranty_claims.form.serialNumber'), type: 'text', layout: 'half' },
|
|
1622
|
+
{ id: 'purchaseDate', label: t('warranty_claims.form.purchaseDate'), type: 'date', layout: 'half' },
|
|
1623
|
+
{ id: 'warrantyMonths', label: t('warranty_claims.form.warrantyMonths'), type: 'number', layout: 'half' },
|
|
1624
|
+
{
|
|
1625
|
+
id: 'entitlementPreview',
|
|
1626
|
+
label: t('warranty_claims.form.entitlement'),
|
|
1627
|
+
type: 'custom',
|
|
1628
|
+
layout: 'half',
|
|
1629
|
+
component: ({ values }) => {
|
|
1630
|
+
const purchaseDateText = nullableText(values?.purchaseDate)
|
|
1631
|
+
const purchaseDate = purchaseDateText ? new Date(purchaseDateText) : null
|
|
1632
|
+
const monthsValue = Number(values?.warrantyMonths)
|
|
1633
|
+
const months = Number.isFinite(monthsValue) && String(values?.warrantyMonths ?? '').trim() !== '' ? monthsValue : null
|
|
1634
|
+
return <EntitlementChip status={computeEntitlementPreview(purchaseDate, months)} t={t} />
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
id: 'faultCode',
|
|
1639
|
+
label: t('warranty_claims.form.faultCode'),
|
|
1640
|
+
type: 'select',
|
|
1641
|
+
loadOptions: () => loadDictionaryOptions(DICTIONARY_KEYS.faultCodes, 'fault'),
|
|
1642
|
+
layout: 'half',
|
|
1643
|
+
},
|
|
1644
|
+
{ id: 'faultDescription', label: t('warranty_claims.form.faultDescription'), type: 'textarea', rows: 3, layout: 'full' },
|
|
1645
|
+
{ id: 'qtyClaimed', label: t('warranty_claims.form.qtyClaimed'), type: 'number', layout: 'third' },
|
|
1646
|
+
{ id: 'qtyApproved', label: t('warranty_claims.form.qtyApproved'), type: 'number', layout: 'third' },
|
|
1647
|
+
{ id: 'qtyReceived', label: t('warranty_claims.form.qtyReceived'), type: 'number', layout: 'third' },
|
|
1648
|
+
{
|
|
1649
|
+
id: 'lineStatus',
|
|
1650
|
+
label: t('warranty_claims.form.lineStatus'),
|
|
1651
|
+
type: 'select',
|
|
1652
|
+
options: CLAIM_LINE_STATUSES.map((status) => ({ value: status, label: t(`warranty_claims.lineStatus.${status}`) })),
|
|
1653
|
+
layout: 'half',
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
id: 'disposition',
|
|
1657
|
+
label: t('warranty_claims.form.disposition'),
|
|
1658
|
+
type: 'select',
|
|
1659
|
+
options: buildDispositionOptions(allowedDispositions, currentLineDisposition, t),
|
|
1660
|
+
layout: 'half',
|
|
1661
|
+
},
|
|
1662
|
+
{ id: 'creditAmount', label: t('warranty_claims.form.creditAmount'), type: 'number', layout: 'third' },
|
|
1663
|
+
{ id: 'restockingFee', label: t('warranty_claims.form.restockingFee'), type: 'number', layout: 'third' },
|
|
1664
|
+
{ id: 'coreChargeAmount', label: t('warranty_claims.form.coreChargeAmount'), type: 'number', layout: 'third' },
|
|
1665
|
+
{ id: 'coreCreditAmount', label: t('warranty_claims.form.coreCreditAmount'), type: 'number', layout: 'third' },
|
|
1666
|
+
{ id: 'conditionOnReceipt', label: t('warranty_claims.form.conditionOnReceipt'), type: 'textarea', rows: 3, layout: 'full' },
|
|
1667
|
+
{ id: 'inspectionNotes', label: t('warranty_claims.form.inspectionNotes'), type: 'textarea', rows: 3, layout: 'full' },
|
|
1668
|
+
], [allowedDispositions, currentLineDisposition, loadDictionaryOptions, t])
|
|
1669
|
+
|
|
1670
|
+
const lineGroups = React.useMemo<CrudFormGroup[]>(() => [
|
|
1671
|
+
{
|
|
1672
|
+
id: 'line-main',
|
|
1673
|
+
title: t(resolveClaimTypeUiConfig(claim?.claimType).lineHeaderKey),
|
|
1674
|
+
fields: [
|
|
1675
|
+
'productLookup',
|
|
1676
|
+
'productName',
|
|
1677
|
+
'sku',
|
|
1678
|
+
'serialNumber',
|
|
1679
|
+
'purchaseDate',
|
|
1680
|
+
'warrantyMonths',
|
|
1681
|
+
'entitlementPreview',
|
|
1682
|
+
'faultCode',
|
|
1683
|
+
'faultDescription',
|
|
1684
|
+
'qtyClaimed',
|
|
1685
|
+
'qtyApproved',
|
|
1686
|
+
'qtyReceived',
|
|
1687
|
+
'lineStatus',
|
|
1688
|
+
'disposition',
|
|
1689
|
+
],
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
id: 'line-money',
|
|
1693
|
+
title: t('warranty_claims.detail.lines.amounts'),
|
|
1694
|
+
// Restock / core fields belong only to return-family claims; warranty & vendor-recovery
|
|
1695
|
+
// settle on the credit amount alone (LINE-05).
|
|
1696
|
+
fields: claimTypeAllowsLineFinancialAdjustments(claim?.claimType)
|
|
1697
|
+
? ['creditAmount', 'restockingFee', 'coreChargeAmount', 'coreCreditAmount']
|
|
1698
|
+
: ['creditAmount'],
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
id: 'line-inspection',
|
|
1702
|
+
title: t('warranty_claims.detail.lines.inspection'),
|
|
1703
|
+
fields: ['conditionOnReceipt', 'inspectionNotes'],
|
|
1704
|
+
},
|
|
1705
|
+
], [claim?.claimType, t])
|
|
1706
|
+
|
|
1707
|
+
const lineColumns = React.useMemo<ColumnDef<ClaimLine>[]>(() => [
|
|
1708
|
+
{
|
|
1709
|
+
accessorKey: 'lineNo',
|
|
1710
|
+
header: t('warranty_claims.detail.lines.column.number'),
|
|
1711
|
+
cell: ({ row }) => <span className="font-mono text-xs">{row.original.lineNo ?? noValue}</span>,
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
accessorKey: 'lineStatus',
|
|
1715
|
+
header: t('warranty_claims.detail.lines.column.status'),
|
|
1716
|
+
cell: ({ row }) => (
|
|
1717
|
+
<InlineLineSelectCell
|
|
1718
|
+
line={row.original}
|
|
1719
|
+
field="lineStatus"
|
|
1720
|
+
value={row.original.lineStatus}
|
|
1721
|
+
label={t('warranty_claims.form.lineStatus')}
|
|
1722
|
+
options={buildLineStatusOptions(row.original.lineStatus, t)}
|
|
1723
|
+
disabled={!linesInlineEditable}
|
|
1724
|
+
onSave={saveLineInlineField}
|
|
1725
|
+
/>
|
|
1726
|
+
),
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
accessorKey: 'productName',
|
|
1730
|
+
header: t('warranty_claims.detail.lines.column.product'),
|
|
1731
|
+
cell: ({ row }) => (
|
|
1732
|
+
<div className="max-w-80 space-y-0.5">
|
|
1733
|
+
<div className="truncate font-medium" title={row.original.productName ?? undefined}>{row.original.productName ?? noValue}</div>
|
|
1734
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
1735
|
+
{[row.original.sku, row.original.serialNumber].filter(Boolean).join(' / ') || noValue}
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1738
|
+
),
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
id: 'entitlement',
|
|
1742
|
+
header: t('warranty_claims.detail.lines.column.entitlement'),
|
|
1743
|
+
cell: ({ row }) => {
|
|
1744
|
+
const purchaseDate = row.original.purchaseDate ? new Date(row.original.purchaseDate) : null
|
|
1745
|
+
return (
|
|
1746
|
+
<EntitlementChip
|
|
1747
|
+
status={computeEntitlementPreview(purchaseDate, row.original.warrantyMonths)}
|
|
1748
|
+
t={t}
|
|
1749
|
+
/>
|
|
1750
|
+
)
|
|
1751
|
+
},
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
accessorKey: 'qtyClaimed',
|
|
1755
|
+
header: t('warranty_claims.lines.header.qtyClaimed'),
|
|
1756
|
+
cell: ({ row }) => formatQuantity(row.original.qtyClaimed, noValue),
|
|
1757
|
+
},
|
|
1758
|
+
{
|
|
1759
|
+
accessorKey: 'qtyApproved',
|
|
1760
|
+
header: t('warranty_claims.lines.header.qtyApproved'),
|
|
1761
|
+
cell: ({ row }) => (
|
|
1762
|
+
<InlineQtyApprovedCell
|
|
1763
|
+
line={row.original}
|
|
1764
|
+
disabled={!linesInlineEditable}
|
|
1765
|
+
label={t('warranty_claims.form.qtyApproved')}
|
|
1766
|
+
onSave={saveLineInlineField}
|
|
1767
|
+
/>
|
|
1768
|
+
),
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
accessorKey: 'disposition',
|
|
1772
|
+
header: t('warranty_claims.form.disposition'),
|
|
1773
|
+
cell: ({ row }) => (
|
|
1774
|
+
<InlineLineSelectCell
|
|
1775
|
+
line={row.original}
|
|
1776
|
+
field="disposition"
|
|
1777
|
+
value={row.original.disposition}
|
|
1778
|
+
label={t('warranty_claims.form.disposition')}
|
|
1779
|
+
emptyLabel={noValue}
|
|
1780
|
+
options={buildDispositionOptions(allowedDispositions, row.original.disposition, t)}
|
|
1781
|
+
disabled={!linesInlineEditable}
|
|
1782
|
+
onSave={saveLineInlineField}
|
|
1783
|
+
/>
|
|
1784
|
+
),
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
accessorKey: 'creditAmount',
|
|
1788
|
+
header: t('warranty_claims.detail.lines.column.credit'),
|
|
1789
|
+
cell: ({ row }) => formatAmount(row.original.creditAmount, claim?.currencyCode ?? null, noValue),
|
|
1790
|
+
},
|
|
1791
|
+
], [allowedDispositions, claim?.currencyCode, linesInlineEditable, noValue, saveLineInlineField, t])
|
|
1792
|
+
|
|
1793
|
+
const transitionFields = React.useMemo<CrudField[]>(() => [
|
|
1794
|
+
{
|
|
1795
|
+
id: 'rejectionReasonCode',
|
|
1796
|
+
label: t('warranty_claims.form.rejectionReasonCode'),
|
|
1797
|
+
type: 'select',
|
|
1798
|
+
required: true,
|
|
1799
|
+
loadOptions: () => loadDictionaryOptions(DICTIONARY_KEYS.rejectionReasons, 'rejection'),
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
id: 'resolutionSummary',
|
|
1803
|
+
label: t('warranty_claims.form.resolutionSummary'),
|
|
1804
|
+
type: 'textarea',
|
|
1805
|
+
rows: 3,
|
|
1806
|
+
layout: 'full',
|
|
1807
|
+
},
|
|
1808
|
+
], [loadDictionaryOptions, t])
|
|
1809
|
+
|
|
1810
|
+
const vendorFields = React.useMemo<CrudField[]>(() => [
|
|
1811
|
+
{
|
|
1812
|
+
id: 'lineIds',
|
|
1813
|
+
label: t('warranty_claims.detail.vendorRecovery.lines'),
|
|
1814
|
+
type: 'select',
|
|
1815
|
+
multiple: true,
|
|
1816
|
+
required: true,
|
|
1817
|
+
options: eligibleVendorLines.map((line) => ({
|
|
1818
|
+
value: line.id,
|
|
1819
|
+
label: [
|
|
1820
|
+
t('warranty_claims.form.lines.lineLabel', 'Line {number}', { number: line.lineNo ?? '?' }),
|
|
1821
|
+
line.productName ?? line.sku,
|
|
1822
|
+
].filter(Boolean).join(' — '),
|
|
1823
|
+
})),
|
|
1824
|
+
},
|
|
1825
|
+
{ id: 'vendorName', label: t('warranty_claims.form.vendorName'), type: 'text', required: true },
|
|
1826
|
+
{ id: 'vendorRef', label: t('warranty_claims.form.vendorRef'), type: 'text' },
|
|
1827
|
+
], [eligibleVendorLines, t])
|
|
1828
|
+
|
|
1829
|
+
if (loading) {
|
|
1830
|
+
return (
|
|
1831
|
+
<Page>
|
|
1832
|
+
<PageBody>
|
|
1833
|
+
<LoadingMessage label={t('warranty_claims.detail.loading')} />
|
|
1834
|
+
</PageBody>
|
|
1835
|
+
</Page>
|
|
1836
|
+
)
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (notFound) {
|
|
1840
|
+
return (
|
|
1841
|
+
<Page>
|
|
1842
|
+
<PageBody>
|
|
1843
|
+
<RecordNotFoundState
|
|
1844
|
+
label={t('warranty_claims.errors.notFound')}
|
|
1845
|
+
backHref="/backend/warranty_claims"
|
|
1846
|
+
backLabel={t('warranty_claims.detail.actions.backToList')}
|
|
1847
|
+
/>
|
|
1848
|
+
</PageBody>
|
|
1849
|
+
</Page>
|
|
1850
|
+
)
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
if (error || !claim) {
|
|
1854
|
+
return (
|
|
1855
|
+
<Page>
|
|
1856
|
+
<PageBody>
|
|
1857
|
+
<ErrorMessage
|
|
1858
|
+
label={error ?? t('warranty_claims.errors.notFound')}
|
|
1859
|
+
action={(
|
|
1860
|
+
<Button asChild variant="outline" size="sm">
|
|
1861
|
+
<Link href="/backend/warranty_claims">{t('warranty_claims.detail.actions.backToList')}</Link>
|
|
1862
|
+
</Button>
|
|
1863
|
+
)}
|
|
1864
|
+
/>
|
|
1865
|
+
</PageBody>
|
|
1866
|
+
</Page>
|
|
1867
|
+
)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
const tabs: Array<{ id: TabId; label: string; count?: number }> = [
|
|
1871
|
+
{ id: 'lines', label: t('warranty_claims.detail.tabs.lines'), count: lines.length },
|
|
1872
|
+
{ id: 'timeline', label: t('warranty_claims.detail.tabs.timeline'), count: events.length },
|
|
1873
|
+
{ id: 'attachments', label: t('warranty_claims.detail.tabs.attachments'), count: attachmentCount },
|
|
1874
|
+
{ id: 'ai', label: t('warranty_claims.detail.tabs.ai') },
|
|
1875
|
+
]
|
|
1876
|
+
const approvedLineCount = lines.filter((line) => line.lineStatus === 'approved').length
|
|
1877
|
+
return (
|
|
1878
|
+
<Page>
|
|
1879
|
+
<PageBody>
|
|
1880
|
+
<div className="space-y-6">
|
|
1881
|
+
<div className="overflow-hidden rounded-xl border border-border bg-card">
|
|
1882
|
+
<div className="px-8 pb-5 pt-5">
|
|
1883
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
1884
|
+
<div className="inline-flex items-center rounded-md border border-border bg-background px-2.5 py-1 text-xs text-muted-foreground">
|
|
1885
|
+
{t(`warranty_claims.claimType.${claim.claimType ?? 'warranty'}`)}
|
|
1886
|
+
<span className="ml-1 font-semibold text-foreground">
|
|
1887
|
+
{claim.claimNumber ?? t('warranty_claims.detail.unnumbered', 'Unnumbered claim')}
|
|
1888
|
+
</span>
|
|
1889
|
+
</div>
|
|
1890
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1891
|
+
{featureAccess.claimDelete ? (
|
|
1892
|
+
<IconButton
|
|
1893
|
+
type="button"
|
|
1894
|
+
variant="outline"
|
|
1895
|
+
size="default"
|
|
1896
|
+
className="border-destructive/30 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
1897
|
+
disabled={!DELETABLE_CLAIM_STATUSES.has(currentStatus)}
|
|
1898
|
+
aria-label={t('warranty_claims.audit.claim.delete')}
|
|
1899
|
+
title={DELETABLE_CLAIM_STATUSES.has(currentStatus)
|
|
1900
|
+
? t('warranty_claims.audit.claim.delete')
|
|
1901
|
+
: t('warranty_claims.errors.deleteNotAllowed')}
|
|
1902
|
+
onClick={() => { void deleteClaim() }}
|
|
1903
|
+
>
|
|
1904
|
+
<Trash2 className="size-4" aria-hidden />
|
|
1905
|
+
</IconButton>
|
|
1906
|
+
) : null}
|
|
1907
|
+
<ActionsDropdown
|
|
1908
|
+
items={headerMoreActions}
|
|
1909
|
+
triggerMode="icon"
|
|
1910
|
+
ariaLabel={t('ui.actions.actions', 'Actions')}
|
|
1911
|
+
/>
|
|
1912
|
+
<Select
|
|
1913
|
+
value=""
|
|
1914
|
+
disabled={!featureAccess.claimManage || resolutionStatuses.length === 0}
|
|
1915
|
+
onValueChange={(status) => { void confirmAndTransition(status as ClaimStatus) }}
|
|
1916
|
+
>
|
|
1917
|
+
<SelectTrigger className="h-8 w-28" aria-label={t('warranty_claims.edit.fulfillment.resolution')}>
|
|
1918
|
+
<SelectValue placeholder={t('warranty_claims.edit.fulfillment.resolution')} />
|
|
1919
|
+
</SelectTrigger>
|
|
1920
|
+
<SelectContent>
|
|
1921
|
+
{resolutionStatuses.map((status) => (
|
|
1922
|
+
<SelectItem key={status} value={status}>
|
|
1923
|
+
{t(`warranty_claims.status.${status}`)}
|
|
1924
|
+
</SelectItem>
|
|
1925
|
+
))}
|
|
1926
|
+
</SelectContent>
|
|
1927
|
+
</Select>
|
|
1928
|
+
{featureAccess.claimManage && primaryNextStatus ? (
|
|
1929
|
+
<Button
|
|
1930
|
+
type="button"
|
|
1931
|
+
size="sm"
|
|
1932
|
+
onClick={() => { void confirmAndTransition(primaryNextStatus) }}
|
|
1933
|
+
>
|
|
1934
|
+
{primaryNextStatus === 'submitted' && currentStatus === 'draft'
|
|
1935
|
+
? t('warranty_claims.detail.actions.submit')
|
|
1936
|
+
: primaryNextStatus === 'in_review' && currentStatus === 'submitted'
|
|
1937
|
+
? t('warranty_claims.list.actions.startReview')
|
|
1938
|
+
: t(`warranty_claims.status.${primaryNextStatus}`)}
|
|
1939
|
+
</Button>
|
|
1940
|
+
) : null}
|
|
1941
|
+
</div>
|
|
1942
|
+
</div>
|
|
1943
|
+
<h1 className="mt-3 max-w-3xl text-2xl font-bold tracking-tight text-foreground">
|
|
1944
|
+
{claim.notes || claim.reasonCode || claim.claimNumber || t('warranty_claims.detail.unnumbered', 'Unnumbered claim')}
|
|
1945
|
+
</h1>
|
|
1946
|
+
<div className="mt-3 grid gap-4 sm:grid-cols-2 xl:max-w-4xl xl:grid-cols-6">
|
|
1947
|
+
<div>
|
|
1948
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.customer')}</p>
|
|
1949
|
+
<div className="mt-1 text-sm font-medium text-foreground">
|
|
1950
|
+
<span>
|
|
1951
|
+
{claim.customerName ? (
|
|
1952
|
+
customerLink && customerLink.customerId === claim.customerId ? (
|
|
1953
|
+
<Link
|
|
1954
|
+
href={customerLink.href}
|
|
1955
|
+
className="text-foreground underline-offset-4 hover:underline"
|
|
1956
|
+
>
|
|
1957
|
+
{claim.customerName}
|
|
1958
|
+
</Link>
|
|
1959
|
+
) : (
|
|
1960
|
+
claim.customerName
|
|
1961
|
+
)
|
|
1962
|
+
) : (
|
|
1963
|
+
noValue
|
|
1964
|
+
)}
|
|
1965
|
+
</span>
|
|
1966
|
+
</div>
|
|
1967
|
+
</div>
|
|
1968
|
+
<div>
|
|
1969
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.priority', 'Priority')}</p>
|
|
1970
|
+
<p className="mt-1 text-sm font-medium text-foreground">
|
|
1971
|
+
{t(`warranty_claims.priority.${claim.priority ?? 'normal'}`)}
|
|
1972
|
+
</p>
|
|
1973
|
+
</div>
|
|
1974
|
+
<div>
|
|
1975
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.opened')}</p>
|
|
1976
|
+
<p className="mt-1 text-sm font-medium text-foreground" title={formatDateTime(claim.createdAt) ?? undefined}>
|
|
1977
|
+
{formatRelativeTime(claim.createdAt, { locale }) ?? noValue}
|
|
1978
|
+
</p>
|
|
1979
|
+
</div>
|
|
1980
|
+
<div>
|
|
1981
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.list.column.slaDueAt')}</p>
|
|
1982
|
+
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm">
|
|
1983
|
+
<ClaimSlaIndicator
|
|
1984
|
+
slaDueAt={claim.slaDueAt}
|
|
1985
|
+
slaPausedAt={claim.slaPausedAt}
|
|
1986
|
+
submittedAt={claim.submittedAt}
|
|
1987
|
+
status={claim.status}
|
|
1988
|
+
atRiskThresholdPct={slaAtRiskThresholdPct}
|
|
1989
|
+
/>
|
|
1990
|
+
</div>
|
|
1991
|
+
</div>
|
|
1992
|
+
<div>
|
|
1993
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.owner')}</p>
|
|
1994
|
+
<p className="mt-1 truncate text-sm font-medium text-foreground" title={assigneeDisplayName}>{assigneeDisplayName}</p>
|
|
1995
|
+
</div>
|
|
1996
|
+
<div>
|
|
1997
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.state')}</p>
|
|
1998
|
+
<div className="mt-1">
|
|
1999
|
+
<ClaimStatusBadge status={claim.status} />
|
|
2000
|
+
</div>
|
|
2001
|
+
</div>
|
|
2002
|
+
</div>
|
|
2003
|
+
{riskSignals.length > 0 ? (
|
|
2004
|
+
<div className="mt-3 flex flex-wrap items-center gap-2 xl:max-w-4xl" data-testid="warranty-claim-risk-signals">
|
|
2005
|
+
<RiskSignalChips signals={riskSignals} t={t} />
|
|
2006
|
+
</div>
|
|
2007
|
+
) : null}
|
|
2008
|
+
</div>
|
|
2009
|
+
<ClaimStageProgress status={claim.status} />
|
|
2010
|
+
<Tabs
|
|
2011
|
+
value={activeTab}
|
|
2012
|
+
onValueChange={(value) => setActiveTab(value as TabId)}
|
|
2013
|
+
variant="underline"
|
|
2014
|
+
>
|
|
2015
|
+
<TabsList className="h-auto w-full flex-wrap gap-1 px-7" aria-label={t('warranty_claims.detail.tabs.label', 'Claim sections')}>
|
|
2016
|
+
{tabs.map((tab) => (
|
|
2017
|
+
<TabsTrigger
|
|
2018
|
+
key={tab.id}
|
|
2019
|
+
value={tab.id}
|
|
2020
|
+
count={tab.count}
|
|
2021
|
+
data-tab-id={tab.id}
|
|
2022
|
+
className="px-4 pb-2 pt-3"
|
|
2023
|
+
>
|
|
2024
|
+
{tab.label}
|
|
2025
|
+
</TabsTrigger>
|
|
2026
|
+
))}
|
|
2027
|
+
</TabsList>
|
|
2028
|
+
|
|
2029
|
+
<TabsContent value="lines" className="mt-0">
|
|
2030
|
+
<div>
|
|
2031
|
+
<div className="grid items-center gap-4 border-b border-border px-8 py-5 sm:grid-cols-2 xl:grid-cols-[repeat(3,minmax(0,1fr))_auto]">
|
|
2032
|
+
<div>
|
|
2033
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.totalClaimed')}</p>
|
|
2034
|
+
<p className="mt-1 text-base font-semibold text-foreground">
|
|
2035
|
+
{formatAmount(claim.totalClaimedAmount, claim.currencyCode, noValue)}
|
|
2036
|
+
</p>
|
|
2037
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2038
|
+
{t('warranty_claims.detail.lines.count', { count: lines.length })}
|
|
2039
|
+
</p>
|
|
2040
|
+
</div>
|
|
2041
|
+
<div className="xl:border-l xl:border-border xl:pl-5">
|
|
2042
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.totalApproved')}</p>
|
|
2043
|
+
<p className="mt-1 text-base font-semibold text-foreground">
|
|
2044
|
+
{formatAmount(claim.totalApprovedAmount, claim.currencyCode, noValue)}
|
|
2045
|
+
</p>
|
|
2046
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2047
|
+
{t('warranty_claims.detail.lines.approvedCount', { count: approvedLineCount })}
|
|
2048
|
+
</p>
|
|
2049
|
+
</div>
|
|
2050
|
+
<div className="xl:border-l xl:border-border xl:pl-5">
|
|
2051
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.totalRecovered')}</p>
|
|
2052
|
+
<p className="mt-1 text-base font-semibold text-foreground">
|
|
2053
|
+
{formatAmount(claim.totalRecoveredAmount, claim.currencyCode, noValue)}
|
|
2054
|
+
</p>
|
|
2055
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2056
|
+
{claim.vendorName ?? t('warranty_claims.detail.lines.noVendorClaims')}
|
|
2057
|
+
</p>
|
|
2058
|
+
</div>
|
|
2059
|
+
{linesInlineEditable ? (
|
|
2060
|
+
<Button type="button" variant="outline" size="sm" className="justify-self-start xl:justify-self-end" onClick={() => setLineDialog({ mode: 'create' })}>
|
|
2061
|
+
{t('warranty_claims.detail.lines.add')}
|
|
2062
|
+
</Button>
|
|
2063
|
+
) : null}
|
|
2064
|
+
</div>
|
|
2065
|
+
<div className="pb-3">
|
|
2066
|
+
<DataTable<ClaimLine>
|
|
2067
|
+
embedded
|
|
2068
|
+
columns={lineColumns}
|
|
2069
|
+
data={lines}
|
|
2070
|
+
stickyActionsColumn
|
|
2071
|
+
emptyState={(
|
|
2072
|
+
<EmptyState
|
|
2073
|
+
title={t('warranty_claims.detail.lines.empty.title')}
|
|
2074
|
+
description={t('warranty_claims.detail.lines.empty.description')}
|
|
2075
|
+
variant="subtle"
|
|
2076
|
+
/>
|
|
2077
|
+
)}
|
|
2078
|
+
rowActions={linesInlineEditable ? (line) => (
|
|
2079
|
+
<RowActions
|
|
2080
|
+
items={[
|
|
2081
|
+
{
|
|
2082
|
+
id: 'edit',
|
|
2083
|
+
label: t('warranty_claims.detail.lines.edit'),
|
|
2084
|
+
onSelect: () => setLineDialog({ mode: 'edit', line }),
|
|
2085
|
+
},
|
|
2086
|
+
]}
|
|
2087
|
+
/>
|
|
2088
|
+
) : undefined}
|
|
2089
|
+
/>
|
|
2090
|
+
</div>
|
|
2091
|
+
<div className="px-8 pb-8">
|
|
2092
|
+
<ReceivingPanel
|
|
2093
|
+
claimId={claim.id}
|
|
2094
|
+
lines={lines}
|
|
2095
|
+
canManage={featureAccess.receivingManage}
|
|
2096
|
+
receivingCapable={RECEIVING_CAPABLE_CLAIM_STATUSES.has(currentStatus)}
|
|
2097
|
+
onRefresh={loadData}
|
|
2098
|
+
/>
|
|
2099
|
+
</div>
|
|
2100
|
+
</div>
|
|
2101
|
+
</TabsContent>
|
|
2102
|
+
|
|
2103
|
+
<TabsContent value="timeline" className="mt-0">
|
|
2104
|
+
<div className="mx-8 max-w-3xl space-y-5 pb-8 pt-6">
|
|
2105
|
+
{featureAccess.claimManage ? (
|
|
2106
|
+
<div>
|
|
2107
|
+
<div className="space-y-4">
|
|
2108
|
+
<Textarea
|
|
2109
|
+
value={commentBody}
|
|
2110
|
+
onChange={(event) => setCommentBody(event.target.value)}
|
|
2111
|
+
onKeyDown={(event) => {
|
|
2112
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
2113
|
+
event.preventDefault()
|
|
2114
|
+
void submitComment()
|
|
2115
|
+
}
|
|
2116
|
+
}}
|
|
2117
|
+
placeholder={t('warranty_claims.detail.timeline.commentPlaceholder')}
|
|
2118
|
+
rows={6}
|
|
2119
|
+
/>
|
|
2120
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
2121
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
2122
|
+
<Select value={commentVisibility} onValueChange={(next) => setCommentVisibility(next === 'customer' ? 'customer' : 'internal')}>
|
|
2123
|
+
<SelectTrigger className="w-40" aria-label={t('warranty_claims.detail.visibility.label')}>
|
|
2124
|
+
<SelectValue />
|
|
2125
|
+
</SelectTrigger>
|
|
2126
|
+
<SelectContent>
|
|
2127
|
+
<SelectItem value="internal">{t('warranty_claims.detail.visibility.internal')}</SelectItem>
|
|
2128
|
+
<SelectItem value="customer">{t('warranty_claims.detail.visibility.customer')}</SelectItem>
|
|
2129
|
+
</SelectContent>
|
|
2130
|
+
</Select>
|
|
2131
|
+
{!draftReplyHidden && featureAccess.claimManage && claimMutable ? (
|
|
2132
|
+
<Button
|
|
2133
|
+
type="button"
|
|
2134
|
+
variant="outline"
|
|
2135
|
+
onClick={() => { void draftReplyWithAi() }}
|
|
2136
|
+
disabled={draftReplyLoading}
|
|
2137
|
+
>
|
|
2138
|
+
<AiIcon className="size-4" aria-hidden="true" />
|
|
2139
|
+
{draftReplyLoading
|
|
2140
|
+
? t('warranty_claims.ai.draftingReply')
|
|
2141
|
+
: t('warranty_claims.ai.draftReply')}
|
|
2142
|
+
</Button>
|
|
2143
|
+
) : null}
|
|
2144
|
+
</div>
|
|
2145
|
+
<Button type="button" onClick={() => { void submitComment() }} disabled={!commentBody.trim()}>
|
|
2146
|
+
{t('warranty_claims.detail.timeline.addComment')}
|
|
2147
|
+
</Button>
|
|
2148
|
+
</div>
|
|
2149
|
+
</div>
|
|
2150
|
+
</div>
|
|
2151
|
+
) : null}
|
|
2152
|
+
<div className="space-y-3">
|
|
2153
|
+
{events.length ? (
|
|
2154
|
+
<SegmentedControl
|
|
2155
|
+
size="sm"
|
|
2156
|
+
value={timelineFilter}
|
|
2157
|
+
onValueChange={(next) => setTimelineFilter(isTimelineFilterId(next) ? next : 'all')}
|
|
2158
|
+
aria-label={t('warranty_claims.detail.timeline.filter.label', 'Filter timeline')}
|
|
2159
|
+
>
|
|
2160
|
+
{TIMELINE_FILTERS.map((filter) => (
|
|
2161
|
+
<SegmentedControlItem key={filter} value={filter}>
|
|
2162
|
+
{t(TIMELINE_FILTER_LABELS[filter].key, TIMELINE_FILTER_LABELS[filter].fallback)}
|
|
2163
|
+
</SegmentedControlItem>
|
|
2164
|
+
))}
|
|
2165
|
+
</SegmentedControl>
|
|
2166
|
+
) : null}
|
|
2167
|
+
{filteredTimelineEvents.length ? filteredTimelineEvents.map((event) => {
|
|
2168
|
+
const Icon = eventIcon(event.kind)
|
|
2169
|
+
const body = formatTimelineBody(event, t, userNames)
|
|
2170
|
+
const actor = resolveTimelineActor(event, claim, userNames, t)
|
|
2171
|
+
return (
|
|
2172
|
+
<div key={event.id} className="flex gap-3 py-2">
|
|
2173
|
+
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
2174
|
+
<Icon className="h-4 w-4" aria-hidden />
|
|
2175
|
+
</div>
|
|
2176
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
2177
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
2178
|
+
<span className="text-sm font-medium">{t(`warranty_claims.eventKind.${event.kind}`)}</span>
|
|
2179
|
+
<span className="text-sm text-muted-foreground">{actor}</span>
|
|
2180
|
+
<StatusBadge variant={event.visibility === 'customer' ? 'info' : 'neutral'}>
|
|
2181
|
+
{event.visibility === 'customer'
|
|
2182
|
+
? t('warranty_claims.detail.visibility.customer')
|
|
2183
|
+
: t('warranty_claims.detail.visibility.internal')}
|
|
2184
|
+
</StatusBadge>
|
|
2185
|
+
<span className="text-xs text-muted-foreground">{formatDateTime(event.createdAt) ?? noValue}</span>
|
|
2186
|
+
</div>
|
|
2187
|
+
{body ? <p className="text-sm">{body}</p> : null}
|
|
2188
|
+
</div>
|
|
2189
|
+
</div>
|
|
2190
|
+
)
|
|
2191
|
+
}) : events.length ? (
|
|
2192
|
+
<EmptyState
|
|
2193
|
+
title={t('warranty_claims.detail.timeline.filterEmpty.title', 'No matching events')}
|
|
2194
|
+
description={t('warranty_claims.detail.timeline.filterEmpty.description', 'No timeline events match the selected filter.')}
|
|
2195
|
+
variant="subtle"
|
|
2196
|
+
/>
|
|
2197
|
+
) : (
|
|
2198
|
+
<EmptyState
|
|
2199
|
+
title={t('warranty_claims.detail.timeline.empty.title')}
|
|
2200
|
+
description={t('warranty_claims.detail.timeline.empty.description')}
|
|
2201
|
+
variant="subtle"
|
|
2202
|
+
/>
|
|
2203
|
+
)}
|
|
2204
|
+
</div>
|
|
2205
|
+
</div>
|
|
2206
|
+
</TabsContent>
|
|
2207
|
+
|
|
2208
|
+
<TabsContent value="attachments" className="mt-0">
|
|
2209
|
+
<div className="px-8 pb-8 pt-6">
|
|
2210
|
+
<AttachmentInput
|
|
2211
|
+
entityId="warranty_claims:warranty_claim"
|
|
2212
|
+
recordId={claim.id}
|
|
2213
|
+
allowDelete={featureAccess.claimManage}
|
|
2214
|
+
allowReplace={featureAccess.claimManage}
|
|
2215
|
+
disabled={!featureAccess.claimManage}
|
|
2216
|
+
onCountChange={setAttachmentCount}
|
|
2217
|
+
renderItemMeta={(item) => (
|
|
2218
|
+
isCustomerVisibleAttachment(item.tags) ? (
|
|
2219
|
+
<StatusBadge variant="info">
|
|
2220
|
+
{t('warranty_claims.detail.attachments.customerVisible', 'Visible to customer')}
|
|
2221
|
+
</StatusBadge>
|
|
2222
|
+
) : null
|
|
2223
|
+
)}
|
|
2224
|
+
/>
|
|
2225
|
+
</div>
|
|
2226
|
+
</TabsContent>
|
|
2227
|
+
|
|
2228
|
+
<TabsContent value="ai" className="mt-0">
|
|
2229
|
+
<div className="grid gap-4 px-8 pb-8 pt-6 xl:grid-cols-2">
|
|
2230
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
2231
|
+
<div className="flex items-start justify-between gap-3">
|
|
2232
|
+
<div className="min-w-0">
|
|
2233
|
+
<h3 className="text-sm font-medium">{t('warranty_claims.triage.panelTitle')}</h3>
|
|
2234
|
+
<p className="mt-1 text-sm text-muted-foreground">{t('warranty_claims.triage.panelSubtitle')}</p>
|
|
2235
|
+
</div>
|
|
2236
|
+
<Button type="button" variant="outline" onClick={loadAiSuggestion} disabled={aiLoading}>
|
|
2237
|
+
{t('warranty_claims.triage.suggestButton')}
|
|
2238
|
+
</Button>
|
|
2239
|
+
</div>
|
|
2240
|
+
{aiSuggestion ? (
|
|
2241
|
+
<div className="mt-4 space-y-4">
|
|
2242
|
+
<div className="rounded-md border border-border p-3">
|
|
2243
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
2244
|
+
<StatusBadge variant={aiSuggestion.priority.overdue ? 'error' : 'info'}>
|
|
2245
|
+
{t(`warranty_claims.priority.${aiSuggestion.priority.currentPriority}`)}
|
|
2246
|
+
{' → '}
|
|
2247
|
+
{t(`warranty_claims.priority.${aiSuggestion.priority.suggestedPriority}`)}
|
|
2248
|
+
</StatusBadge>
|
|
2249
|
+
<RiskSignalChips signals={aiSuggestion.risk?.signals ?? []} t={t} />
|
|
2250
|
+
</div>
|
|
2251
|
+
<p className="mt-2 text-sm text-muted-foreground">{formatTriageMessage(aiSuggestion.priority.reason, t)}</p>
|
|
2252
|
+
</div>
|
|
2253
|
+
<div className="space-y-2">
|
|
2254
|
+
{aiSuggestion.lines.map((suggestedLine) => {
|
|
2255
|
+
const applied = appliedTriageLineIds.has(suggestedLine.lineId)
|
|
2256
|
+
return (
|
|
2257
|
+
<div
|
|
2258
|
+
key={suggestedLine.lineId}
|
|
2259
|
+
className="flex flex-wrap items-start justify-between gap-3 rounded-md border border-border p-3"
|
|
2260
|
+
>
|
|
2261
|
+
<div className="min-w-0">
|
|
2262
|
+
<div className="text-sm font-medium">
|
|
2263
|
+
{triageLineHeading(suggestedLine, lines, t)}
|
|
2264
|
+
</div>
|
|
2265
|
+
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
2266
|
+
<StatusBadge variant={eligibilityBadgeVariant(suggestedLine.eligibility.status)}>
|
|
2267
|
+
{t(`warranty_claims.eligibility.${suggestedLine.eligibility.status}`)}
|
|
2268
|
+
</StatusBadge>
|
|
2269
|
+
<StatusBadge variant="info">
|
|
2270
|
+
{t(`warranty_claims.disposition.${suggestedLine.suggestedDisposition}`)}
|
|
2271
|
+
</StatusBadge>
|
|
2272
|
+
</div>
|
|
2273
|
+
<p className="mt-2 text-sm text-muted-foreground">{formatTriageMessage(suggestedLine.reason, t)}</p>
|
|
2274
|
+
<p className="mt-1 text-xs text-muted-foreground">{formatTriageMessage(suggestedLine.eligibility.reason, t)}</p>
|
|
2275
|
+
</div>
|
|
2276
|
+
{applied ? (
|
|
2277
|
+
<StatusBadge variant="success">
|
|
2278
|
+
{t('warranty_claims.triage.applied', 'Applied')}
|
|
2279
|
+
{': '}
|
|
2280
|
+
{t(`warranty_claims.disposition.${suggestedLine.suggestedDisposition}`)}
|
|
2281
|
+
</StatusBadge>
|
|
2282
|
+
) : (
|
|
2283
|
+
<Button
|
|
2284
|
+
type="button"
|
|
2285
|
+
variant="outline"
|
|
2286
|
+
size="sm"
|
|
2287
|
+
onClick={() => applyAiDisposition(suggestedLine.lineId, suggestedLine.suggestedDisposition)}
|
|
2288
|
+
>
|
|
2289
|
+
{t('warranty_claims.triage.apply')}
|
|
2290
|
+
</Button>
|
|
2291
|
+
)}
|
|
2292
|
+
</div>
|
|
2293
|
+
)
|
|
2294
|
+
})}
|
|
2295
|
+
</div>
|
|
2296
|
+
</div>
|
|
2297
|
+
) : (
|
|
2298
|
+
<div className="mt-4">
|
|
2299
|
+
<EmptyState
|
|
2300
|
+
title={t('warranty_claims.triage.panelTitle')}
|
|
2301
|
+
description={t('warranty_claims.triage.empty.description')}
|
|
2302
|
+
variant="subtle"
|
|
2303
|
+
/>
|
|
2304
|
+
</div>
|
|
2305
|
+
)}
|
|
2306
|
+
</div>
|
|
2307
|
+
<div className="h-96 overflow-hidden rounded-lg border border-border bg-card">
|
|
2308
|
+
<React.Suspense fallback={<LoadingMessage label={t('warranty_claims.detail.loading')} />}>
|
|
2309
|
+
{/* Scope the conversation to this claim and remount on claim change so a
|
|
2310
|
+
claim can never inherit another claim's transcript (WQA-007 / AI-01). */}
|
|
2311
|
+
<LazyAiChat
|
|
2312
|
+
key={`${AGENT_ID}:${claim.id}`}
|
|
2313
|
+
agent={AGENT_ID}
|
|
2314
|
+
pageContext={{ entityType: 'warranty_claims.claim', recordId: claim.id }}
|
|
2315
|
+
className="h-full"
|
|
2316
|
+
placeholder={t('warranty_claims.ai.chatPlaceholder')}
|
|
2317
|
+
welcomeTitle={t('warranty_claims.ai.chatWelcomeTitle')}
|
|
2318
|
+
/>
|
|
2319
|
+
</React.Suspense>
|
|
2320
|
+
</div>
|
|
2321
|
+
{lines.length ? (
|
|
2322
|
+
<div className="space-y-3 xl:col-span-2">
|
|
2323
|
+
{lines.map((line) => (
|
|
2324
|
+
<div key={line.id} className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border p-4">
|
|
2325
|
+
<div className="min-w-0">
|
|
2326
|
+
<p className="truncate text-sm font-medium text-foreground">
|
|
2327
|
+
{line.productName ?? line.sku ?? t('warranty_claims.detail.lines.unnamed', 'Unnamed line')}
|
|
2328
|
+
</p>
|
|
2329
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
2330
|
+
{[line.sku, line.serialNumber].filter(Boolean).join(' / ') || noValue}
|
|
2331
|
+
</p>
|
|
2332
|
+
</div>
|
|
2333
|
+
<AiAssessButtons
|
|
2334
|
+
claimId={claim.id}
|
|
2335
|
+
line={line}
|
|
2336
|
+
canManage={featureAccess.claimManage && claimMutable}
|
|
2337
|
+
onRefresh={loadData}
|
|
2338
|
+
/>
|
|
2339
|
+
</div>
|
|
2340
|
+
))}
|
|
2341
|
+
</div>
|
|
2342
|
+
) : null}
|
|
2343
|
+
</div>
|
|
2344
|
+
</TabsContent>
|
|
2345
|
+
</Tabs>
|
|
2346
|
+
</div>
|
|
2347
|
+
|
|
2348
|
+
<ReturnLabelPanel claim={claim} canManage={featureAccess.claimManage} onRefresh={loadData} />
|
|
2349
|
+
|
|
2350
|
+
{(() => {
|
|
2351
|
+
const showSalesReturn = featureAccess.claimManage
|
|
2352
|
+
&& featureAccess.salesReturnsCreate
|
|
2353
|
+
&& claim.orderId
|
|
2354
|
+
&& !claim.salesReturnId
|
|
2355
|
+
&& ['approved', 'awaiting_return', 'received', 'inspecting'].includes(currentStatus ?? '')
|
|
2356
|
+
const showReplacementOrder = featureAccess.claimManage
|
|
2357
|
+
&& featureAccess.salesOrdersManage
|
|
2358
|
+
&& claim.orderId
|
|
2359
|
+
&& !claim.replacementOrderId
|
|
2360
|
+
&& ['approved', 'awaiting_return', 'received', 'inspecting', 'resolved'].includes(currentStatus ?? '')
|
|
2361
|
+
const showCreditMemo = featureAccess.claimManage
|
|
2362
|
+
&& featureAccess.salesCreditMemosManage
|
|
2363
|
+
&& claim.orderId
|
|
2364
|
+
&& !claim.creditMemoId
|
|
2365
|
+
&& ['received', 'inspecting', 'resolved'].includes(currentStatus ?? '')
|
|
2366
|
+
if (!showSalesReturn && !showReplacementOrder && !showCreditMemo) return null
|
|
2367
|
+
return (
|
|
2368
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
2369
|
+
{showSalesReturn ? (
|
|
2370
|
+
<Button type="button" variant="outline" onClick={() => { void createSalesReturn() }}>
|
|
2371
|
+
{t('warranty_claims.detail.createSalesReturn')}
|
|
2372
|
+
</Button>
|
|
2373
|
+
) : null}
|
|
2374
|
+
{showReplacementOrder ? (
|
|
2375
|
+
<Button type="button" variant="outline" onClick={() => { void createReplacementOrder() }}>
|
|
2376
|
+
{t('warranty_claims.detail.createReplacementOrder')}
|
|
2377
|
+
</Button>
|
|
2378
|
+
) : null}
|
|
2379
|
+
{showCreditMemo ? (
|
|
2380
|
+
<Button type="button" variant="outline" onClick={() => { void createCreditMemo() }}>
|
|
2381
|
+
{t('warranty_claims.detail.createCreditMemo')}
|
|
2382
|
+
</Button>
|
|
2383
|
+
) : null}
|
|
2384
|
+
</div>
|
|
2385
|
+
)
|
|
2386
|
+
})()}
|
|
2387
|
+
|
|
2388
|
+
<VendorRecoverySuggestionsPanel
|
|
2389
|
+
claim={claim}
|
|
2390
|
+
canManage={featureAccess.claimManage}
|
|
2391
|
+
onGenerateSupplierRecovery={generateSupplierRecovery}
|
|
2392
|
+
/>
|
|
2393
|
+
</div>
|
|
2394
|
+
</PageBody>
|
|
2395
|
+
|
|
2396
|
+
<Dialog open={assignDialogOpen} onOpenChange={setAssignDialogOpen}>
|
|
2397
|
+
<DialogContent
|
|
2398
|
+
className="max-w-lg"
|
|
2399
|
+
onKeyDown={(event) => {
|
|
2400
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
2401
|
+
event.preventDefault()
|
|
2402
|
+
void assignClaim()
|
|
2403
|
+
}
|
|
2404
|
+
if (event.key === 'Escape') {
|
|
2405
|
+
setAssignDialogOpen(false)
|
|
2406
|
+
}
|
|
2407
|
+
}}
|
|
2408
|
+
>
|
|
2409
|
+
<DialogHeader>
|
|
2410
|
+
<DialogTitle>{t('warranty_claims.detail.assignDialog.title')}</DialogTitle>
|
|
2411
|
+
</DialogHeader>
|
|
2412
|
+
<div className="space-y-4">
|
|
2413
|
+
<div className="space-y-2">
|
|
2414
|
+
<Label htmlFor="warranty-claim-assignee-search">
|
|
2415
|
+
{t('warranty_claims.detail.assignDialog.search')}
|
|
2416
|
+
</Label>
|
|
2417
|
+
<Input
|
|
2418
|
+
id="warranty-claim-assignee-search"
|
|
2419
|
+
value={assignSearch}
|
|
2420
|
+
onChange={(event) => setAssignSearch(event.target.value)}
|
|
2421
|
+
placeholder={t('warranty_claims.detail.assignDialog.searchPlaceholder')}
|
|
2422
|
+
/>
|
|
2423
|
+
</div>
|
|
2424
|
+
<div className="space-y-2">
|
|
2425
|
+
<Label htmlFor="warranty-claim-assignee-select">
|
|
2426
|
+
{t('warranty_claims.detail.assignDialog.staff')}
|
|
2427
|
+
</Label>
|
|
2428
|
+
<Select value={selectedAssigneeUserId} onValueChange={setSelectedAssigneeUserId}>
|
|
2429
|
+
<SelectTrigger id="warranty-claim-assignee-select">
|
|
2430
|
+
<SelectValue />
|
|
2431
|
+
</SelectTrigger>
|
|
2432
|
+
<SelectContent>
|
|
2433
|
+
<SelectItem value={UNASSIGNED_SELECT_VALUE}>{t('warranty_claims.detail.unassigned')}</SelectItem>
|
|
2434
|
+
{assignableOptions.map((staffMember) => (
|
|
2435
|
+
<SelectItem key={staffMember.userId} value={staffMember.userId}>
|
|
2436
|
+
{staffMember.email && staffMember.email !== staffMember.displayName
|
|
2437
|
+
? `${staffMember.displayName} (${staffMember.email})`
|
|
2438
|
+
: staffMember.displayName}
|
|
2439
|
+
</SelectItem>
|
|
2440
|
+
))}
|
|
2441
|
+
</SelectContent>
|
|
2442
|
+
</Select>
|
|
2443
|
+
{assignLoading ? (
|
|
2444
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.assignDialog.loading')}</p>
|
|
2445
|
+
) : null}
|
|
2446
|
+
{!assignLoading && assignableOptions.length === 0 ? (
|
|
2447
|
+
<p className="text-xs text-muted-foreground">{t('warranty_claims.detail.assignDialog.empty')}</p>
|
|
2448
|
+
) : null}
|
|
2449
|
+
</div>
|
|
2450
|
+
</div>
|
|
2451
|
+
<DialogFooter>
|
|
2452
|
+
<Button type="button" variant="outline" onClick={() => setAssignDialogOpen(false)}>
|
|
2453
|
+
{t('common.cancel', 'Cancel')}
|
|
2454
|
+
</Button>
|
|
2455
|
+
<Button type="button" onClick={() => { void assignClaim() }}>
|
|
2456
|
+
{t('warranty_claims.form.submit')}
|
|
2457
|
+
</Button>
|
|
2458
|
+
</DialogFooter>
|
|
2459
|
+
</DialogContent>
|
|
2460
|
+
</Dialog>
|
|
2461
|
+
|
|
2462
|
+
<Dialog open={lineDialog !== null} onOpenChange={(open) => { if (!open) setLineDialog(null) }}>
|
|
2463
|
+
<DialogContent className="sm:max-w-3xl">
|
|
2464
|
+
<DialogHeader>
|
|
2465
|
+
<DialogTitle>
|
|
2466
|
+
{lineDialog?.mode === 'edit'
|
|
2467
|
+
? t('warranty_claims.detail.lines.edit')
|
|
2468
|
+
: t('warranty_claims.detail.lines.add')}
|
|
2469
|
+
</DialogTitle>
|
|
2470
|
+
</DialogHeader>
|
|
2471
|
+
<CrudForm<ClaimLineFormValues>
|
|
2472
|
+
embedded
|
|
2473
|
+
title={lineDialog?.mode === 'edit' ? t('warranty_claims.detail.lines.edit') : t('warranty_claims.detail.lines.add')}
|
|
2474
|
+
fields={lineDialog?.mode === 'edit' ? lineFields : lineFields.filter((field) => field.id !== 'lineStatus')}
|
|
2475
|
+
groups={lineDialog?.mode === 'edit'
|
|
2476
|
+
? lineGroups
|
|
2477
|
+
: lineGroups.map((group) => ({ ...group, fields: (group.fields ?? []).filter((fieldId) => fieldId !== 'lineStatus') }))}
|
|
2478
|
+
initialValues={lineDialog?.mode === 'edit'
|
|
2479
|
+
? {
|
|
2480
|
+
...lineDialog.line,
|
|
2481
|
+
qtyClaimed: quantityInputValue(lineDialog.line.qtyClaimed),
|
|
2482
|
+
qtyApproved: quantityInputValue(lineDialog.line.qtyApproved),
|
|
2483
|
+
qtyReceived: quantityInputValue(lineDialog.line.qtyReceived),
|
|
2484
|
+
}
|
|
2485
|
+
: { claimId: claim.id, qtyClaimed: '1', lineStatus: 'pending', warrantyMonths: defaultWarrantyMonths ?? undefined }}
|
|
2486
|
+
submitLabel={t('warranty_claims.form.submit')}
|
|
2487
|
+
onSubmit={async (values) => {
|
|
2488
|
+
const payload: Record<string, unknown> = {
|
|
2489
|
+
claimId: claim.id,
|
|
2490
|
+
productId: nullableText(values.productId),
|
|
2491
|
+
variantId: nullableText(values.variantId),
|
|
2492
|
+
productName: nullableText(values.productName),
|
|
2493
|
+
sku: nullableText(values.sku),
|
|
2494
|
+
serialNumber: nullableText(values.serialNumber),
|
|
2495
|
+
purchaseDate: nullableText(values.purchaseDate),
|
|
2496
|
+
warrantyMonths: values.warrantyMonths === null || values.warrantyMonths === undefined || String(values.warrantyMonths).trim() === ''
|
|
2497
|
+
? null
|
|
2498
|
+
: Number(values.warrantyMonths),
|
|
2499
|
+
faultCode: nullableText(values.faultCode),
|
|
2500
|
+
faultDescription: nullableText(values.faultDescription),
|
|
2501
|
+
qtyClaimed: values.qtyClaimed ?? 1,
|
|
2502
|
+
qtyApproved: values.qtyApproved ?? null,
|
|
2503
|
+
qtyReceived: values.qtyReceived ?? null,
|
|
2504
|
+
disposition: nullableText(values.disposition),
|
|
2505
|
+
creditAmount: values.creditAmount ?? null,
|
|
2506
|
+
restockingFee: values.restockingFee ?? null,
|
|
2507
|
+
coreChargeAmount: values.coreChargeAmount ?? null,
|
|
2508
|
+
coreCreditAmount: values.coreCreditAmount ?? null,
|
|
2509
|
+
conditionOnReceipt: nullableText(values.conditionOnReceipt),
|
|
2510
|
+
inspectionNotes: nullableText(values.inspectionNotes),
|
|
2511
|
+
}
|
|
2512
|
+
if (lineDialog?.mode === 'edit') {
|
|
2513
|
+
payload.lineStatus = nullableText(values.lineStatus) ?? 'pending'
|
|
2514
|
+
}
|
|
2515
|
+
if (lineDialog?.mode === 'edit') {
|
|
2516
|
+
await updateCrud('warranty_claims/lines', { id: lineDialog.line.id, ...payload }, {
|
|
2517
|
+
errorMessage: t('warranty_claims.detail.lines.error.save'),
|
|
2518
|
+
})
|
|
2519
|
+
flash(t('warranty_claims.detail.lines.flash.saved'), 'success')
|
|
2520
|
+
} else {
|
|
2521
|
+
await createCrud('warranty_claims/lines', payload, {
|
|
2522
|
+
errorMessage: t('warranty_claims.detail.lines.error.save'),
|
|
2523
|
+
})
|
|
2524
|
+
flash(t('warranty_claims.detail.lines.flash.created'), 'success')
|
|
2525
|
+
}
|
|
2526
|
+
setLineDialog(null)
|
|
2527
|
+
await loadData()
|
|
2528
|
+
}}
|
|
2529
|
+
onDelete={lineDialog?.mode === 'edit'
|
|
2530
|
+
? async () => {
|
|
2531
|
+
await deleteCrud('warranty_claims/lines', {
|
|
2532
|
+
body: { id: lineDialog.line.id, claimId: claim.id },
|
|
2533
|
+
errorMessage: t('warranty_claims.detail.lines.error.delete'),
|
|
2534
|
+
})
|
|
2535
|
+
flash(t('warranty_claims.detail.lines.flash.deleted'), 'success')
|
|
2536
|
+
setLineDialog(null)
|
|
2537
|
+
await loadData()
|
|
2538
|
+
}
|
|
2539
|
+
: undefined}
|
|
2540
|
+
/>
|
|
2541
|
+
</DialogContent>
|
|
2542
|
+
</Dialog>
|
|
2543
|
+
|
|
2544
|
+
<Dialog open={transitionDialog !== null} onOpenChange={(open) => { if (!open) setTransitionDialog(null) }}>
|
|
2545
|
+
<DialogContent className="max-w-lg">
|
|
2546
|
+
<DialogHeader>
|
|
2547
|
+
<DialogTitle>{t('warranty_claims.detail.confirm.rejectTitle')}</DialogTitle>
|
|
2548
|
+
</DialogHeader>
|
|
2549
|
+
<CrudForm<TransitionFormValues>
|
|
2550
|
+
embedded
|
|
2551
|
+
title={t('warranty_claims.detail.confirm.rejectTitle')}
|
|
2552
|
+
fields={transitionFields}
|
|
2553
|
+
initialValues={{ rejectionReasonCode: claim.rejectionReasonCode ?? '', resolutionSummary: claim.resolutionSummary ?? '' }}
|
|
2554
|
+
submitLabel={t('warranty_claims.detail.actions.transition')}
|
|
2555
|
+
onSubmit={async (values) => {
|
|
2556
|
+
if (!transitionDialog) return
|
|
2557
|
+
await handleTransition(transitionDialog.toStatus, values)
|
|
2558
|
+
setTransitionDialog(null)
|
|
2559
|
+
}}
|
|
2560
|
+
/>
|
|
2561
|
+
</DialogContent>
|
|
2562
|
+
</Dialog>
|
|
2563
|
+
|
|
2564
|
+
<Dialog open={vendorDialogOpen} onOpenChange={setVendorDialogOpen}>
|
|
2565
|
+
<DialogContent className="max-w-lg">
|
|
2566
|
+
<DialogHeader>
|
|
2567
|
+
<DialogTitle>{t('warranty_claims.detail.actions.vendorRecovery')}</DialogTitle>
|
|
2568
|
+
</DialogHeader>
|
|
2569
|
+
<CrudForm<VendorRecoveryFormValues>
|
|
2570
|
+
embedded
|
|
2571
|
+
title={t('warranty_claims.detail.actions.vendorRecovery')}
|
|
2572
|
+
fields={vendorFields}
|
|
2573
|
+
initialValues={{ lineIds: eligibleVendorLines.map((line) => line.id), vendorName: '', vendorRef: '' }}
|
|
2574
|
+
submitLabel={t('warranty_claims.detail.actions.vendorRecovery')}
|
|
2575
|
+
onSubmit={async (values) => {
|
|
2576
|
+
const lineIds = Array.isArray(values.lineIds) ? values.lineIds.filter((lineId): lineId is string => typeof lineId === 'string') : []
|
|
2577
|
+
if (!lineIds.length) {
|
|
2578
|
+
flash(t('warranty_claims.errors.vendorRecoveryNeedsResolvedLines'), 'error')
|
|
2579
|
+
return
|
|
2580
|
+
}
|
|
2581
|
+
await runClaimPost(
|
|
2582
|
+
'/api/warranty_claims/vendor-recovery',
|
|
2583
|
+
{
|
|
2584
|
+
claimId: claim.id,
|
|
2585
|
+
lineIds,
|
|
2586
|
+
vendorName: nullableText(values.vendorName),
|
|
2587
|
+
vendorRef: nullableText(values.vendorRef),
|
|
2588
|
+
},
|
|
2589
|
+
'warranty_claims.detail.vendorRecovery.created',
|
|
2590
|
+
)
|
|
2591
|
+
setVendorDialogOpen(false)
|
|
2592
|
+
}}
|
|
2593
|
+
/>
|
|
2594
|
+
</DialogContent>
|
|
2595
|
+
</Dialog>
|
|
2596
|
+
{ConfirmDialogElement}
|
|
2597
|
+
</Page>
|
|
2598
|
+
)
|
|
2599
|
+
}
|