@open-mercato/core 0.6.8-develop.6993.1.3fc50f3409 → 0.6.8-develop.7000.1.0ae682bad1

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.
Files changed (708) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/sync_run/index.js +2 -0
  3. package/dist/generated/entities/sync_run/index.js.map +2 -2
  4. package/dist/generated/entities/warranty_claim/index.js +105 -0
  5. package/dist/generated/entities/warranty_claim/index.js.map +7 -0
  6. package/dist/generated/entities/warranty_claim_event/index.js +25 -0
  7. package/dist/generated/entities/warranty_claim_event/index.js.map +7 -0
  8. package/dist/generated/entities/warranty_claim_line/index.js +77 -0
  9. package/dist/generated/entities/warranty_claim_line/index.js.map +7 -0
  10. package/dist/generated/entities/warranty_claim_registration/index.js +43 -0
  11. package/dist/generated/entities/warranty_claim_registration/index.js.map +7 -0
  12. package/dist/generated/entities/warranty_claim_sequence/index.js +17 -0
  13. package/dist/generated/entities/warranty_claim_sequence/index.js.map +7 -0
  14. package/dist/generated/entities/warranty_claim_settings/index.js +43 -0
  15. package/dist/generated/entities/warranty_claim_settings/index.js.map +7 -0
  16. package/dist/generated/entities/warranty_claim_sla_signal/index.js +25 -0
  17. package/dist/generated/entities/warranty_claim_sla_signal/index.js.map +7 -0
  18. package/dist/generated/entities/warranty_troubleshooting_guide/index.js +25 -0
  19. package/dist/generated/entities/warranty_troubleshooting_guide/index.js.map +7 -0
  20. package/dist/generated/entities/warranty_vendor_policy/index.js +31 -0
  21. package/dist/generated/entities/warranty_vendor_policy/index.js.map +7 -0
  22. package/dist/generated/entities.ids.generated.js +12 -0
  23. package/dist/generated/entities.ids.generated.js.map +2 -2
  24. package/dist/generated/entity-fields-registry.js +201 -0
  25. package/dist/generated/entity-fields-registry.js.map +2 -2
  26. package/dist/helpers/integration/api.js +3 -2
  27. package/dist/helpers/integration/api.js.map +2 -2
  28. package/dist/helpers/integration/salesFixtures.js +13 -6
  29. package/dist/helpers/integration/salesFixtures.js.map +2 -2
  30. package/dist/modules/attachments/di.js +18 -0
  31. package/dist/modules/attachments/di.js.map +2 -2
  32. package/dist/modules/attachments/fields/attachment.js +186 -26
  33. package/dist/modules/attachments/fields/attachment.js.map +2 -2
  34. package/dist/modules/attachments/lib/scoped-upload-service.js +202 -0
  35. package/dist/modules/attachments/lib/scoped-upload-service.js.map +7 -0
  36. package/dist/modules/attachments/lib/target-access-service.js +39 -0
  37. package/dist/modules/attachments/lib/target-access-service.js.map +7 -0
  38. package/dist/modules/auth/api/sidebar/preferences/route.js +2 -2
  39. package/dist/modules/auth/api/sidebar/preferences/route.js.map +1 -1
  40. package/dist/modules/auth/lib/backendChrome.js +3 -3
  41. package/dist/modules/auth/lib/backendChrome.js.map +1 -1
  42. package/dist/modules/auth/services/sidebarPreferencesService.js +7 -2
  43. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  44. package/dist/modules/customers/api/companies/route.js +1 -0
  45. package/dist/modules/customers/api/companies/route.js.map +2 -2
  46. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +2 -2
  47. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  48. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +2 -2
  49. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  50. package/dist/modules/customers/components/detail/assignableStaff.js +6 -69
  51. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  52. package/dist/modules/customers/lib/assignableStaff.js +76 -0
  53. package/dist/modules/customers/lib/assignableStaff.js.map +7 -0
  54. package/dist/modules/data_sync/api/options.js +1 -0
  55. package/dist/modules/data_sync/api/options.js.map +2 -2
  56. package/dist/modules/data_sync/api/run.js +15 -1
  57. package/dist/modules/data_sync/api/run.js.map +2 -2
  58. package/dist/modules/data_sync/api/runs/[id]/retry.js +23 -1
  59. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  60. package/dist/modules/data_sync/api/runs/[id]/route.js +1 -0
  61. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  62. package/dist/modules/data_sync/backend/data-sync/page.js +60 -17
  63. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  64. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +34 -2
  65. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  66. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +21 -15
  67. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  68. package/dist/modules/data_sync/components/RunParameterFields.js +135 -0
  69. package/dist/modules/data_sync/components/RunParameterFields.js.map +7 -0
  70. package/dist/modules/data_sync/data/entities.js +3 -0
  71. package/dist/modules/data_sync/data/entities.js.map +2 -2
  72. package/dist/modules/data_sync/data/validators.js +4 -1
  73. package/dist/modules/data_sync/data/validators.js.map +2 -2
  74. package/dist/modules/data_sync/lib/batch-stream.js +56 -0
  75. package/dist/modules/data_sync/lib/batch-stream.js.map +7 -0
  76. package/dist/modules/data_sync/lib/run-parameters.js +108 -0
  77. package/dist/modules/data_sync/lib/run-parameters.js.map +7 -0
  78. package/dist/modules/data_sync/lib/start-run.js +1 -0
  79. package/dist/modules/data_sync/lib/start-run.js.map +2 -2
  80. package/dist/modules/data_sync/lib/sync-engine.js +165 -101
  81. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  82. package/dist/modules/data_sync/lib/sync-run-service.js +1 -0
  83. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  84. package/dist/modules/data_sync/migrations/Migration20260810120000.js +13 -0
  85. package/dist/modules/data_sync/migrations/Migration20260810120000.js.map +7 -0
  86. package/dist/modules/data_sync/workers/sync-scheduled.js +23 -2
  87. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  88. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +4 -0
  89. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
  90. package/dist/modules/inbox_ops/api/extract/route.js +4 -0
  91. package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
  92. package/dist/modules/inbox_ops/api/webhook/inbound.js +4 -0
  93. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  94. package/dist/modules/query_index/di.js +125 -48
  95. package/dist/modules/query_index/di.js.map +2 -2
  96. package/dist/modules/sales/commands/documents.js +34 -13
  97. package/dist/modules/sales/commands/documents.js.map +2 -2
  98. package/dist/modules/warranty_claims/acl.js +75 -0
  99. package/dist/modules/warranty_claims/acl.js.map +7 -0
  100. package/dist/modules/warranty_claims/ai-agents.js +161 -0
  101. package/dist/modules/warranty_claims/ai-agents.js.map +7 -0
  102. package/dist/modules/warranty_claims/ai-tools.js +591 -0
  103. package/dist/modules/warranty_claims/ai-tools.js.map +7 -0
  104. package/dist/modules/warranty_claims/api/ai/assess/route.js +304 -0
  105. package/dist/modules/warranty_claims/api/ai/assess/route.js.map +7 -0
  106. package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +127 -0
  107. package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +7 -0
  108. package/dist/modules/warranty_claims/api/ai/suggest/route.js +177 -0
  109. package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +7 -0
  110. package/dist/modules/warranty_claims/api/assign/route.js +124 -0
  111. package/dist/modules/warranty_claims/api/assign/route.js.map +7 -0
  112. package/dist/modules/warranty_claims/api/assignees/route.js +97 -0
  113. package/dist/modules/warranty_claims/api/assignees/route.js.map +7 -0
  114. package/dist/modules/warranty_claims/api/credit-memo/route.js +152 -0
  115. package/dist/modules/warranty_claims/api/credit-memo/route.js.map +7 -0
  116. package/dist/modules/warranty_claims/api/entitlement/route.js +145 -0
  117. package/dist/modules/warranty_claims/api/entitlement/route.js.map +7 -0
  118. package/dist/modules/warranty_claims/api/events/route.js +199 -0
  119. package/dist/modules/warranty_claims/api/events/route.js.map +7 -0
  120. package/dist/modules/warranty_claims/api/external/claims/route.js +446 -0
  121. package/dist/modules/warranty_claims/api/external/claims/route.js.map +7 -0
  122. package/dist/modules/warranty_claims/api/lines/route.js +258 -0
  123. package/dist/modules/warranty_claims/api/lines/route.js.map +7 -0
  124. package/dist/modules/warranty_claims/api/openapi.js +27 -0
  125. package/dist/modules/warranty_claims/api/openapi.js.map +7 -0
  126. package/dist/modules/warranty_claims/api/portal/attachments/route.js +490 -0
  127. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +7 -0
  128. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js +178 -0
  129. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js.map +7 -0
  130. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js +101 -0
  131. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js.map +7 -0
  132. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +78 -0
  133. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +7 -0
  134. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +81 -0
  135. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +7 -0
  136. package/dist/modules/warranty_claims/api/portal/claims/route.js +462 -0
  137. package/dist/modules/warranty_claims/api/portal/claims/route.js.map +7 -0
  138. package/dist/modules/warranty_claims/api/portal/events/route.js +228 -0
  139. package/dist/modules/warranty_claims/api/portal/events/route.js.map +7 -0
  140. package/dist/modules/warranty_claims/api/portal/options/route.js +91 -0
  141. package/dist/modules/warranty_claims/api/portal/options/route.js.map +7 -0
  142. package/dist/modules/warranty_claims/api/portal/orders/lines/route.js +172 -0
  143. package/dist/modules/warranty_claims/api/portal/orders/lines/route.js.map +7 -0
  144. package/dist/modules/warranty_claims/api/portal/orders/route.js +151 -0
  145. package/dist/modules/warranty_claims/api/portal/orders/route.js.map +7 -0
  146. package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js +121 -0
  147. package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js.map +7 -0
  148. package/dist/modules/warranty_claims/api/receiving/route.js +176 -0
  149. package/dist/modules/warranty_claims/api/receiving/route.js.map +7 -0
  150. package/dist/modules/warranty_claims/api/registrations/route.js +382 -0
  151. package/dist/modules/warranty_claims/api/registrations/route.js.map +7 -0
  152. package/dist/modules/warranty_claims/api/replacement-order/route.js +150 -0
  153. package/dist/modules/warranty_claims/api/replacement-order/route.js.map +7 -0
  154. package/dist/modules/warranty_claims/api/return-label/route.js +235 -0
  155. package/dist/modules/warranty_claims/api/return-label/route.js.map +7 -0
  156. package/dist/modules/warranty_claims/api/risk/route.js +101 -0
  157. package/dist/modules/warranty_claims/api/risk/route.js.map +7 -0
  158. package/dist/modules/warranty_claims/api/route.js +482 -0
  159. package/dist/modules/warranty_claims/api/route.js.map +7 -0
  160. package/dist/modules/warranty_claims/api/sales-return/route.js +151 -0
  161. package/dist/modules/warranty_claims/api/sales-return/route.js.map +7 -0
  162. package/dist/modules/warranty_claims/api/settings-general/route.js +195 -0
  163. package/dist/modules/warranty_claims/api/settings-general/route.js.map +7 -0
  164. package/dist/modules/warranty_claims/api/stats/route.js +199 -0
  165. package/dist/modules/warranty_claims/api/stats/route.js.map +7 -0
  166. package/dist/modules/warranty_claims/api/submit/route.js +120 -0
  167. package/dist/modules/warranty_claims/api/submit/route.js.map +7 -0
  168. package/dist/modules/warranty_claims/api/transition/route.js +126 -0
  169. package/dist/modules/warranty_claims/api/transition/route.js.map +7 -0
  170. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js +261 -0
  171. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js.map +7 -0
  172. package/dist/modules/warranty_claims/api/vendor-policies/route.js +311 -0
  173. package/dist/modules/warranty_claims/api/vendor-policies/route.js.map +7 -0
  174. package/dist/modules/warranty_claims/api/vendor-recovery/route.js +130 -0
  175. package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +7 -0
  176. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +153 -0
  177. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +7 -0
  178. package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js +268 -0
  179. package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js.map +7 -0
  180. package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js +54 -0
  181. package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js.map +7 -0
  182. package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js +87 -0
  183. package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js.map +7 -0
  184. package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js +120 -0
  185. package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js.map +7 -0
  186. package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js +135 -0
  187. package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js.map +7 -0
  188. package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js +218 -0
  189. package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js.map +7 -0
  190. package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js +231 -0
  191. package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js.map +7 -0
  192. package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js +82 -0
  193. package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js.map +7 -0
  194. package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js +136 -0
  195. package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js.map +7 -0
  196. package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js +41 -0
  197. package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js.map +7 -0
  198. package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js +45 -0
  199. package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js.map +7 -0
  200. package/dist/modules/warranty_claims/backend/components/claimSla.js +92 -0
  201. package/dist/modules/warranty_claims/backend/components/claimSla.js.map +7 -0
  202. package/dist/modules/warranty_claims/backend/components/orderLookup.js +81 -0
  203. package/dist/modules/warranty_claims/backend/components/orderLookup.js.map +7 -0
  204. package/dist/modules/warranty_claims/backend/components/productLookup.js +317 -0
  205. package/dist/modules/warranty_claims/backend/components/productLookup.js.map +7 -0
  206. package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js +61 -0
  207. package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js.map +7 -0
  208. package/dist/modules/warranty_claims/backend/page.js +1088 -0
  209. package/dist/modules/warranty_claims/backend/page.js.map +7 -0
  210. package/dist/modules/warranty_claims/backend/page.meta.js +16 -0
  211. package/dist/modules/warranty_claims/backend/page.meta.js.map +7 -0
  212. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js +427 -0
  213. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js.map +7 -0
  214. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js +16 -0
  215. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js.map +7 -0
  216. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +2101 -0
  217. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +7 -0
  218. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js +16 -0
  219. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js.map +7 -0
  220. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js +1249 -0
  221. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js.map +7 -0
  222. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js +16 -0
  223. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js.map +7 -0
  224. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js +110 -0
  225. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js.map +7 -0
  226. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js +17 -0
  227. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js.map +7 -0
  228. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js +49 -0
  229. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js.map +7 -0
  230. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js +17 -0
  231. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js.map +7 -0
  232. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +485 -0
  233. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +7 -0
  234. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js +19 -0
  235. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js.map +7 -0
  236. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js +253 -0
  237. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js.map +7 -0
  238. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +1565 -0
  239. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +7 -0
  240. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js +19 -0
  241. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js.map +7 -0
  242. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js +472 -0
  243. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js.map +7 -0
  244. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js +115 -0
  245. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js.map +7 -0
  246. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js +19 -0
  247. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js.map +7 -0
  248. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js +54 -0
  249. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js.map +7 -0
  250. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js +19 -0
  251. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js.map +7 -0
  252. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +414 -0
  253. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +7 -0
  254. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js +21 -0
  255. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js.map +7 -0
  256. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js +171 -0
  257. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js.map +7 -0
  258. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js +111 -0
  259. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js.map +7 -0
  260. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js +19 -0
  261. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js.map +7 -0
  262. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js +48 -0
  263. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js.map +7 -0
  264. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js +19 -0
  265. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js.map +7 -0
  266. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +429 -0
  267. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +7 -0
  268. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js +21 -0
  269. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js.map +7 -0
  270. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js +182 -0
  271. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js.map +7 -0
  272. package/dist/modules/warranty_claims/ce.js +48 -0
  273. package/dist/modules/warranty_claims/ce.js.map +7 -0
  274. package/dist/modules/warranty_claims/commands/claim-lines.js +911 -0
  275. package/dist/modules/warranty_claims/commands/claim-lines.js.map +7 -0
  276. package/dist/modules/warranty_claims/commands/claims.js +2577 -0
  277. package/dist/modules/warranty_claims/commands/claims.js.map +7 -0
  278. package/dist/modules/warranty_claims/commands/index.js +4 -0
  279. package/dist/modules/warranty_claims/commands/index.js.map +7 -0
  280. package/dist/modules/warranty_claims/commands/settings.js +169 -0
  281. package/dist/modules/warranty_claims/commands/settings.js.map +7 -0
  282. package/dist/modules/warranty_claims/commands/shared.js +162 -0
  283. package/dist/modules/warranty_claims/commands/shared.js.map +7 -0
  284. package/dist/modules/warranty_claims/data/constants.js +35 -0
  285. package/dist/modules/warranty_claims/data/constants.js.map +7 -0
  286. package/dist/modules/warranty_claims/data/enrichers.js +97 -0
  287. package/dist/modules/warranty_claims/data/enrichers.js.map +7 -0
  288. package/dist/modules/warranty_claims/data/entities.js +731 -0
  289. package/dist/modules/warranty_claims/data/entities.js.map +7 -0
  290. package/dist/modules/warranty_claims/data/validators.js +488 -0
  291. package/dist/modules/warranty_claims/data/validators.js.map +7 -0
  292. package/dist/modules/warranty_claims/di.js +29 -0
  293. package/dist/modules/warranty_claims/di.js.map +7 -0
  294. package/dist/modules/warranty_claims/encryption.js +28 -0
  295. package/dist/modules/warranty_claims/encryption.js.map +7 -0
  296. package/dist/modules/warranty_claims/events.js +81 -0
  297. package/dist/modules/warranty_claims/events.js.map +7 -0
  298. package/dist/modules/warranty_claims/extension-points.js +19 -0
  299. package/dist/modules/warranty_claims/extension-points.js.map +7 -0
  300. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +825 -0
  301. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +7 -0
  302. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js +11 -0
  303. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js.map +7 -0
  304. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +1139 -0
  305. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +7 -0
  306. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js +11 -0
  307. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js.map +7 -0
  308. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +253 -0
  309. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +7 -0
  310. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js +18 -0
  311. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js.map +7 -0
  312. package/dist/modules/warranty_claims/index.js +15 -0
  313. package/dist/modules/warranty_claims/index.js.map +7 -0
  314. package/dist/modules/warranty_claims/lib/aiAssist.js +496 -0
  315. package/dist/modules/warranty_claims/lib/aiAssist.js.map +7 -0
  316. package/dist/modules/warranty_claims/lib/apiIdsFilter.js +11 -0
  317. package/dist/modules/warranty_claims/lib/apiIdsFilter.js.map +7 -0
  318. package/dist/modules/warranty_claims/lib/assigneeNames.js +153 -0
  319. package/dist/modules/warranty_claims/lib/assigneeNames.js.map +7 -0
  320. package/dist/modules/warranty_claims/lib/attachmentVisibility.js +9 -0
  321. package/dist/modules/warranty_claims/lib/attachmentVisibility.js.map +7 -0
  322. package/dist/modules/warranty_claims/lib/bulkFeedback.js +8 -0
  323. package/dist/modules/warranty_claims/lib/bulkFeedback.js.map +7 -0
  324. package/dist/modules/warranty_claims/lib/businessHours.js +250 -0
  325. package/dist/modules/warranty_claims/lib/businessHours.js.map +7 -0
  326. package/dist/modules/warranty_claims/lib/businessHoursForm.js +146 -0
  327. package/dist/modules/warranty_claims/lib/businessHoursForm.js.map +7 -0
  328. package/dist/modules/warranty_claims/lib/claimTypeConfig.js +39 -0
  329. package/dist/modules/warranty_claims/lib/claimTypeConfig.js.map +7 -0
  330. package/dist/modules/warranty_claims/lib/decryptionSafety.js +17 -0
  331. package/dist/modules/warranty_claims/lib/decryptionSafety.js.map +7 -0
  332. package/dist/modules/warranty_claims/lib/deskFilters.js +88 -0
  333. package/dist/modules/warranty_claims/lib/deskFilters.js.map +7 -0
  334. package/dist/modules/warranty_claims/lib/dictionaries.js +157 -0
  335. package/dist/modules/warranty_claims/lib/dictionaries.js.map +7 -0
  336. package/dist/modules/warranty_claims/lib/dictionaryLabels.js +12 -0
  337. package/dist/modules/warranty_claims/lib/dictionaryLabels.js.map +7 -0
  338. package/dist/modules/warranty_claims/lib/emailIntake.js +87 -0
  339. package/dist/modules/warranty_claims/lib/emailIntake.js.map +7 -0
  340. package/dist/modules/warranty_claims/lib/escalation.js +58 -0
  341. package/dist/modules/warranty_claims/lib/escalation.js.map +7 -0
  342. package/dist/modules/warranty_claims/lib/externalIntake.js +233 -0
  343. package/dist/modules/warranty_claims/lib/externalIntake.js.map +7 -0
  344. package/dist/modules/warranty_claims/lib/grading.js +21 -0
  345. package/dist/modules/warranty_claims/lib/grading.js.map +7 -0
  346. package/dist/modules/warranty_claims/lib/listSegments.js +18 -0
  347. package/dist/modules/warranty_claims/lib/listSegments.js.map +7 -0
  348. package/dist/modules/warranty_claims/lib/orderOwnership.js +25 -0
  349. package/dist/modules/warranty_claims/lib/orderOwnership.js.map +7 -0
  350. package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js +11 -0
  351. package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js.map +7 -0
  352. package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js +108 -0
  353. package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js.map +7 -0
  354. package/dist/modules/warranty_claims/lib/portalClaimAccess.js +26 -0
  355. package/dist/modules/warranty_claims/lib/portalClaimAccess.js.map +7 -0
  356. package/dist/modules/warranty_claims/lib/quantity.js +19 -0
  357. package/dist/modules/warranty_claims/lib/quantity.js.map +7 -0
  358. package/dist/modules/warranty_claims/lib/risk.js +204 -0
  359. package/dist/modules/warranty_claims/lib/risk.js.map +7 -0
  360. package/dist/modules/warranty_claims/lib/settings.js +55 -0
  361. package/dist/modules/warranty_claims/lib/settings.js.map +7 -0
  362. package/dist/modules/warranty_claims/lib/stateMachine.js +88 -0
  363. package/dist/modules/warranty_claims/lib/stateMachine.js.map +7 -0
  364. package/dist/modules/warranty_claims/lib/timelineFilters.js +16 -0
  365. package/dist/modules/warranty_claims/lib/timelineFilters.js.map +7 -0
  366. package/dist/modules/warranty_claims/lib/triage.js +241 -0
  367. package/dist/modules/warranty_claims/lib/triage.js.map +7 -0
  368. package/dist/modules/warranty_claims/lib/troubleshooting.js +111 -0
  369. package/dist/modules/warranty_claims/lib/troubleshooting.js.map +7 -0
  370. package/dist/modules/warranty_claims/lib/tryResolve.js +11 -0
  371. package/dist/modules/warranty_claims/lib/tryResolve.js.map +7 -0
  372. package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js +101 -0
  373. package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js.map +7 -0
  374. package/dist/modules/warranty_claims/lib/warrantyPreview.js +18 -0
  375. package/dist/modules/warranty_claims/lib/warrantyPreview.js.map +7 -0
  376. package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js +32 -0
  377. package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js.map +7 -0
  378. package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js +16 -0
  379. package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js.map +7 -0
  380. package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js +17 -0
  381. package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js.map +7 -0
  382. package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js +29 -0
  383. package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js.map +7 -0
  384. package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js +25 -0
  385. package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js.map +7 -0
  386. package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js +13 -0
  387. package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js.map +7 -0
  388. package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js +15 -0
  389. package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js.map +7 -0
  390. package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js +13 -0
  391. package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js.map +7 -0
  392. package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js +13 -0
  393. package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js.map +7 -0
  394. package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js +12 -0
  395. package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js.map +7 -0
  396. package/dist/modules/warranty_claims/notifications.client.js +67 -0
  397. package/dist/modules/warranty_claims/notifications.client.js.map +7 -0
  398. package/dist/modules/warranty_claims/notifications.js +103 -0
  399. package/dist/modules/warranty_claims/notifications.js.map +7 -0
  400. package/dist/modules/warranty_claims/search.js +174 -0
  401. package/dist/modules/warranty_claims/search.js.map +7 -0
  402. package/dist/modules/warranty_claims/services/adjudicationEvaluator.js +202 -0
  403. package/dist/modules/warranty_claims/services/adjudicationEvaluator.js.map +7 -0
  404. package/dist/modules/warranty_claims/services/claimNumberGenerator.js +58 -0
  405. package/dist/modules/warranty_claims/services/claimNumberGenerator.js.map +7 -0
  406. package/dist/modules/warranty_claims/services/entitlementResolver.js +69 -0
  407. package/dist/modules/warranty_claims/services/entitlementResolver.js.map +7 -0
  408. package/dist/modules/warranty_claims/services/returnLabelProvider.js +14 -0
  409. package/dist/modules/warranty_claims/services/returnLabelProvider.js.map +7 -0
  410. package/dist/modules/warranty_claims/setup.js +121 -0
  411. package/dist/modules/warranty_claims/setup.js.map +7 -0
  412. package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js +119 -0
  413. package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js.map +7 -0
  414. package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js +47 -0
  415. package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js.map +7 -0
  416. package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js +60 -0
  417. package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js.map +7 -0
  418. package/dist/modules/warranty_claims/subscribers/claim-status-notification.js +83 -0
  419. package/dist/modules/warranty_claims/subscribers/claim-status-notification.js.map +7 -0
  420. package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js +46 -0
  421. package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js.map +7 -0
  422. package/dist/modules/warranty_claims/subscribers/email-to-claim.js +135 -0
  423. package/dist/modules/warranty_claims/subscribers/email-to-claim.js.map +7 -0
  424. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +164 -0
  425. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +7 -0
  426. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js +24 -0
  427. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js.map +7 -0
  428. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js +26 -0
  429. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js.map +7 -0
  430. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js +15 -0
  431. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js.map +7 -0
  432. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js +197 -0
  433. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js.map +7 -0
  434. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js +28 -0
  435. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js.map +7 -0
  436. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js +418 -0
  437. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js.map +7 -0
  438. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js +18 -0
  439. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js.map +7 -0
  440. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js +17 -0
  441. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js.map +7 -0
  442. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js +146 -0
  443. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js.map +7 -0
  444. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js +17 -0
  445. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js.map +7 -0
  446. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js +147 -0
  447. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js.map +7 -0
  448. package/dist/modules/warranty_claims/widgets/injection-table.js +38 -0
  449. package/dist/modules/warranty_claims/widgets/injection-table.js.map +7 -0
  450. package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js +223 -0
  451. package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js.map +7 -0
  452. package/dist/modules/warranty_claims/widgets/notifications/index.js +5 -0
  453. package/dist/modules/warranty_claims/widgets/notifications/index.js.map +7 -0
  454. package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js +334 -0
  455. package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js.map +7 -0
  456. package/generated/entities/sync_run/index.ts +1 -0
  457. package/generated/entities/warranty_claim/index.ts +51 -0
  458. package/generated/entities/warranty_claim_event/index.ts +11 -0
  459. package/generated/entities/warranty_claim_line/index.ts +37 -0
  460. package/generated/entities/warranty_claim_registration/index.ts +20 -0
  461. package/generated/entities/warranty_claim_sequence/index.ts +7 -0
  462. package/generated/entities/warranty_claim_settings/index.ts +20 -0
  463. package/generated/entities/warranty_claim_sla_signal/index.ts +11 -0
  464. package/generated/entities/warranty_troubleshooting_guide/index.ts +11 -0
  465. package/generated/entities/warranty_vendor_policy/index.ts +14 -0
  466. package/generated/entities.ids.generated.ts +12 -0
  467. package/generated/entity-fields-registry.ts +201 -0
  468. package/package.json +7 -7
  469. package/src/helpers/integration/api.ts +10 -3
  470. package/src/helpers/integration/salesFixtures.ts +17 -7
  471. package/src/modules/attachments/di.ts +30 -1
  472. package/src/modules/attachments/fields/attachment.tsx +215 -23
  473. package/src/modules/attachments/i18n/de.json +5 -0
  474. package/src/modules/attachments/i18n/en.json +5 -0
  475. package/src/modules/attachments/i18n/es.json +5 -0
  476. package/src/modules/attachments/i18n/ko.json +5 -0
  477. package/src/modules/attachments/i18n/pl.json +5 -0
  478. package/src/modules/attachments/lib/scoped-upload-service.ts +257 -0
  479. package/src/modules/attachments/lib/target-access-service.ts +57 -0
  480. package/src/modules/auth/api/sidebar/preferences/route.ts +2 -2
  481. package/src/modules/auth/lib/backendChrome.tsx +2 -2
  482. package/src/modules/auth/services/sidebarPreferencesService.ts +22 -3
  483. package/src/modules/customers/api/companies/route.ts +1 -0
  484. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +4 -2
  485. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +4 -2
  486. package/src/modules/customers/components/detail/assignableStaff.ts +14 -177
  487. package/src/modules/customers/lib/assignableStaff.ts +177 -0
  488. package/src/modules/data_sync/AGENTS.md +86 -8
  489. package/src/modules/data_sync/api/options.ts +1 -0
  490. package/src/modules/data_sync/api/run.ts +17 -0
  491. package/src/modules/data_sync/api/runs/[id]/retry.ts +32 -1
  492. package/src/modules/data_sync/api/runs/[id]/route.ts +1 -0
  493. package/src/modules/data_sync/backend/data-sync/page.tsx +73 -17
  494. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +62 -2
  495. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +28 -15
  496. package/src/modules/data_sync/components/RunParameterFields.tsx +229 -0
  497. package/src/modules/data_sync/data/entities.ts +4 -1
  498. package/src/modules/data_sync/data/validators.ts +3 -0
  499. package/src/modules/data_sync/i18n/de.json +10 -0
  500. package/src/modules/data_sync/i18n/en.json +10 -0
  501. package/src/modules/data_sync/i18n/es.json +10 -0
  502. package/src/modules/data_sync/i18n/ko.json +10 -0
  503. package/src/modules/data_sync/i18n/pl.json +10 -0
  504. package/src/modules/data_sync/lib/adapter.ts +88 -0
  505. package/src/modules/data_sync/lib/batch-stream.ts +117 -0
  506. package/src/modules/data_sync/lib/run-parameters.ts +196 -0
  507. package/src/modules/data_sync/lib/start-run.ts +2 -0
  508. package/src/modules/data_sync/lib/sync-engine.ts +193 -114
  509. package/src/modules/data_sync/lib/sync-run-service.ts +2 -0
  510. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +17 -1
  511. package/src/modules/data_sync/migrations/Migration20260810120000.ts +13 -0
  512. package/src/modules/data_sync/workers/sync-scheduled.ts +32 -1
  513. package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +4 -0
  514. package/src/modules/inbox_ops/api/extract/route.ts +4 -0
  515. package/src/modules/inbox_ops/api/webhook/inbound.ts +4 -0
  516. package/src/modules/query_index/di.ts +153 -56
  517. package/src/modules/sales/commands/documents.ts +56 -12
  518. package/src/modules/sales/i18n/de.json +1 -0
  519. package/src/modules/sales/i18n/en.json +1 -0
  520. package/src/modules/sales/i18n/es.json +1 -0
  521. package/src/modules/sales/i18n/ko.json +1 -0
  522. package/src/modules/sales/i18n/pl.json +1 -0
  523. package/src/modules/warranty_claims/AGENTS.md +49 -0
  524. package/src/modules/warranty_claims/acl.ts +71 -0
  525. package/src/modules/warranty_claims/ai-agents.ts +189 -0
  526. package/src/modules/warranty_claims/ai-tools.ts +711 -0
  527. package/src/modules/warranty_claims/api/ai/assess/route.ts +371 -0
  528. package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +149 -0
  529. package/src/modules/warranty_claims/api/ai/suggest/route.ts +190 -0
  530. package/src/modules/warranty_claims/api/assign/route.ts +142 -0
  531. package/src/modules/warranty_claims/api/assignees/route.ts +102 -0
  532. package/src/modules/warranty_claims/api/credit-memo/route.ts +191 -0
  533. package/src/modules/warranty_claims/api/entitlement/route.ts +224 -0
  534. package/src/modules/warranty_claims/api/events/route.ts +226 -0
  535. package/src/modules/warranty_claims/api/external/claims/route.ts +528 -0
  536. package/src/modules/warranty_claims/api/lines/route.ts +275 -0
  537. package/src/modules/warranty_claims/api/openapi.ts +28 -0
  538. package/src/modules/warranty_claims/api/portal/attachments/route.ts +547 -0
  539. package/src/modules/warranty_claims/api/portal/claims/[id]/route.ts +195 -0
  540. package/src/modules/warranty_claims/api/portal/claims/[id]/shared.ts +125 -0
  541. package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +82 -0
  542. package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +87 -0
  543. package/src/modules/warranty_claims/api/portal/claims/route.ts +579 -0
  544. package/src/modules/warranty_claims/api/portal/events/route.ts +260 -0
  545. package/src/modules/warranty_claims/api/portal/options/route.ts +113 -0
  546. package/src/modules/warranty_claims/api/portal/orders/lines/route.ts +258 -0
  547. package/src/modules/warranty_claims/api/portal/orders/route.ts +206 -0
  548. package/src/modules/warranty_claims/api/portal/troubleshooting/route.ts +151 -0
  549. package/src/modules/warranty_claims/api/receiving/route.ts +215 -0
  550. package/src/modules/warranty_claims/api/registrations/route.ts +417 -0
  551. package/src/modules/warranty_claims/api/replacement-order/route.ts +188 -0
  552. package/src/modules/warranty_claims/api/return-label/route.ts +291 -0
  553. package/src/modules/warranty_claims/api/risk/route.ts +112 -0
  554. package/src/modules/warranty_claims/api/route.ts +543 -0
  555. package/src/modules/warranty_claims/api/sales-return/route.ts +183 -0
  556. package/src/modules/warranty_claims/api/settings-general/route.ts +230 -0
  557. package/src/modules/warranty_claims/api/stats/route.ts +306 -0
  558. package/src/modules/warranty_claims/api/submit/route.ts +146 -0
  559. package/src/modules/warranty_claims/api/transition/route.ts +144 -0
  560. package/src/modules/warranty_claims/api/troubleshooting-guides/route.ts +295 -0
  561. package/src/modules/warranty_claims/api/vendor-policies/route.ts +351 -0
  562. package/src/modules/warranty_claims/api/vendor-recovery/route.ts +148 -0
  563. package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +167 -0
  564. package/src/modules/warranty_claims/backend/components/AiAssessButtons.tsx +389 -0
  565. package/src/modules/warranty_claims/backend/components/ClaimStageProgress.tsx +65 -0
  566. package/src/modules/warranty_claims/backend/components/ClaimStatusBadge.tsx +137 -0
  567. package/src/modules/warranty_claims/backend/components/ClaimsKpiStrip.tsx +169 -0
  568. package/src/modules/warranty_claims/backend/components/EntitlementLookupBadge.tsx +203 -0
  569. package/src/modules/warranty_claims/backend/components/ReceivingPanel.tsx +300 -0
  570. package/src/modules/warranty_claims/backend/components/ReturnLabelPanel.tsx +292 -0
  571. package/src/modules/warranty_claims/backend/components/TroubleshootingWalker.tsx +125 -0
  572. package/src/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.tsx +217 -0
  573. package/src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx +66 -0
  574. package/src/modules/warranty_claims/backend/components/claimReasonOptions.ts +57 -0
  575. package/src/modules/warranty_claims/backend/components/claimSla.tsx +127 -0
  576. package/src/modules/warranty_claims/backend/components/orderLookup.ts +109 -0
  577. package/src/modules/warranty_claims/backend/components/productLookup.tsx +433 -0
  578. package/src/modules/warranty_claims/backend/components/useUserDisplayNames.ts +66 -0
  579. package/src/modules/warranty_claims/backend/page.meta.ts +12 -0
  580. package/src/modules/warranty_claims/backend/page.tsx +1302 -0
  581. package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.ts +12 -0
  582. package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.tsx +531 -0
  583. package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.ts +12 -0
  584. package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +2599 -0
  585. package/src/modules/warranty_claims/backend/warranty_claims/create/page.meta.ts +12 -0
  586. package/src/modules/warranty_claims/backend/warranty_claims/create/page.tsx +1533 -0
  587. package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.ts +13 -0
  588. package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.tsx +136 -0
  589. package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.ts +13 -0
  590. package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.tsx +52 -0
  591. package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.ts +15 -0
  592. package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +555 -0
  593. package/src/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.tsx +295 -0
  594. package/src/modules/warranty_claims/backend/warranty_claims/settings/page.meta.ts +15 -0
  595. package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +1903 -0
  596. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.tsx +550 -0
  597. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.ts +15 -0
  598. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.tsx +141 -0
  599. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.ts +15 -0
  600. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.tsx +57 -0
  601. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.ts +17 -0
  602. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +477 -0
  603. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.tsx +214 -0
  604. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.ts +15 -0
  605. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.tsx +137 -0
  606. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.ts +15 -0
  607. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.tsx +51 -0
  608. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.ts +17 -0
  609. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +506 -0
  610. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.tsx +220 -0
  611. package/src/modules/warranty_claims/ce.ts +44 -0
  612. package/src/modules/warranty_claims/commands/claim-lines.ts +1082 -0
  613. package/src/modules/warranty_claims/commands/claims.ts +3364 -0
  614. package/src/modules/warranty_claims/commands/index.ts +3 -0
  615. package/src/modules/warranty_claims/commands/settings.ts +198 -0
  616. package/src/modules/warranty_claims/commands/shared.ts +212 -0
  617. package/src/modules/warranty_claims/data/constants.ts +37 -0
  618. package/src/modules/warranty_claims/data/enrichers.ts +165 -0
  619. package/src/modules/warranty_claims/data/entities.ts +663 -0
  620. package/src/modules/warranty_claims/data/validators.ts +634 -0
  621. package/src/modules/warranty_claims/di.ts +34 -0
  622. package/src/modules/warranty_claims/encryption.ts +26 -0
  623. package/src/modules/warranty_claims/events.ts +81 -0
  624. package/src/modules/warranty_claims/extension-points.ts +16 -0
  625. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.ts +9 -0
  626. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +1037 -0
  627. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.ts +9 -0
  628. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +1409 -0
  629. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.ts +16 -0
  630. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +304 -0
  631. package/src/modules/warranty_claims/i18n/de.json +1270 -0
  632. package/src/modules/warranty_claims/i18n/en.json +1270 -0
  633. package/src/modules/warranty_claims/i18n/es.json +1270 -0
  634. package/src/modules/warranty_claims/i18n/ko.json +1270 -0
  635. package/src/modules/warranty_claims/i18n/pl.json +1270 -0
  636. package/src/modules/warranty_claims/index.ts +13 -0
  637. package/src/modules/warranty_claims/lib/aiAssist.ts +677 -0
  638. package/src/modules/warranty_claims/lib/apiIdsFilter.ts +12 -0
  639. package/src/modules/warranty_claims/lib/assigneeNames.ts +184 -0
  640. package/src/modules/warranty_claims/lib/attachmentVisibility.ts +5 -0
  641. package/src/modules/warranty_claims/lib/bulkFeedback.ts +6 -0
  642. package/src/modules/warranty_claims/lib/businessHours.ts +333 -0
  643. package/src/modules/warranty_claims/lib/businessHoursForm.ts +206 -0
  644. package/src/modules/warranty_claims/lib/claimTypeConfig.ts +49 -0
  645. package/src/modules/warranty_claims/lib/decryptionSafety.ts +19 -0
  646. package/src/modules/warranty_claims/lib/deskFilters.ts +131 -0
  647. package/src/modules/warranty_claims/lib/dictionaries.ts +212 -0
  648. package/src/modules/warranty_claims/lib/dictionaryLabels.ts +16 -0
  649. package/src/modules/warranty_claims/lib/emailIntake.ts +123 -0
  650. package/src/modules/warranty_claims/lib/escalation.ts +83 -0
  651. package/src/modules/warranty_claims/lib/externalIntake.ts +362 -0
  652. package/src/modules/warranty_claims/lib/grading.ts +23 -0
  653. package/src/modules/warranty_claims/lib/listSegments.ts +19 -0
  654. package/src/modules/warranty_claims/lib/orderOwnership.ts +58 -0
  655. package/src/modules/warranty_claims/lib/portalAttachmentUpload.ts +11 -0
  656. package/src/modules/warranty_claims/lib/portalAttachmentValidation.ts +115 -0
  657. package/src/modules/warranty_claims/lib/portalClaimAccess.ts +37 -0
  658. package/src/modules/warranty_claims/lib/quantity.ts +15 -0
  659. package/src/modules/warranty_claims/lib/risk.ts +371 -0
  660. package/src/modules/warranty_claims/lib/settings.ts +76 -0
  661. package/src/modules/warranty_claims/lib/stateMachine.ts +130 -0
  662. package/src/modules/warranty_claims/lib/timelineFilters.ts +22 -0
  663. package/src/modules/warranty_claims/lib/triage.ts +366 -0
  664. package/src/modules/warranty_claims/lib/troubleshooting.ts +157 -0
  665. package/src/modules/warranty_claims/lib/tryResolve.ts +7 -0
  666. package/src/modules/warranty_claims/lib/vendorPolicyRecovery.ts +182 -0
  667. package/src/modules/warranty_claims/lib/warrantyPreview.ts +20 -0
  668. package/src/modules/warranty_claims/migrations/.snapshot-open-mercato.json +3525 -0
  669. package/src/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.ts +38 -0
  670. package/src/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.ts +17 -0
  671. package/src/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.ts +17 -0
  672. package/src/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.ts +37 -0
  673. package/src/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.ts +25 -0
  674. package/src/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.ts +13 -0
  675. package/src/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.ts +15 -0
  676. package/src/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.ts +13 -0
  677. package/src/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.ts +13 -0
  678. package/src/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.ts +11 -0
  679. package/src/modules/warranty_claims/notifications.client.ts +78 -0
  680. package/src/modules/warranty_claims/notifications.ts +101 -0
  681. package/src/modules/warranty_claims/search.ts +199 -0
  682. package/src/modules/warranty_claims/services/adjudicationEvaluator.ts +344 -0
  683. package/src/modules/warranty_claims/services/claimNumberGenerator.ts +69 -0
  684. package/src/modules/warranty_claims/services/entitlementResolver.ts +102 -0
  685. package/src/modules/warranty_claims/services/returnLabelProvider.ts +30 -0
  686. package/src/modules/warranty_claims/setup.ts +154 -0
  687. package/src/modules/warranty_claims/subscribers/auto-vendor-recovery.ts +140 -0
  688. package/src/modules/warranty_claims/subscribers/claim-assigned-notification.ts +54 -0
  689. package/src/modules/warranty_claims/subscribers/claim-customer-reply-notification.ts +68 -0
  690. package/src/modules/warranty_claims/subscribers/claim-status-notification.ts +100 -0
  691. package/src/modules/warranty_claims/subscribers/claim-submitted-notification.ts +53 -0
  692. package/src/modules/warranty_claims/subscribers/email-to-claim.ts +175 -0
  693. package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +226 -0
  694. package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.ts +28 -0
  695. package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.ts +31 -0
  696. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/config.ts +15 -0
  697. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.tsx +237 -0
  698. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.ts +27 -0
  699. package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.tsx +527 -0
  700. package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.ts +37 -0
  701. package/src/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.ts +21 -0
  702. package/src/modules/warranty_claims/widgets/injection/customer-claims-tab.tsx +210 -0
  703. package/src/modules/warranty_claims/widgets/injection/order-claims-tab/widget.ts +21 -0
  704. package/src/modules/warranty_claims/widgets/injection/order-claims-tab.tsx +210 -0
  705. package/src/modules/warranty_claims/widgets/injection-table.ts +36 -0
  706. package/src/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.tsx +253 -0
  707. package/src/modules/warranty_claims/widgets/notifications/index.ts +1 -0
  708. 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
+ }