@open-mercato/core 0.6.8-develop.6998.1.d2bc46c56c → 0.6.8-develop.7001.1.03608ab252

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (651) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/warranty_claim/index.js +105 -0
  3. package/dist/generated/entities/warranty_claim/index.js.map +7 -0
  4. package/dist/generated/entities/warranty_claim_event/index.js +25 -0
  5. package/dist/generated/entities/warranty_claim_event/index.js.map +7 -0
  6. package/dist/generated/entities/warranty_claim_line/index.js +77 -0
  7. package/dist/generated/entities/warranty_claim_line/index.js.map +7 -0
  8. package/dist/generated/entities/warranty_claim_registration/index.js +43 -0
  9. package/dist/generated/entities/warranty_claim_registration/index.js.map +7 -0
  10. package/dist/generated/entities/warranty_claim_sequence/index.js +17 -0
  11. package/dist/generated/entities/warranty_claim_sequence/index.js.map +7 -0
  12. package/dist/generated/entities/warranty_claim_settings/index.js +43 -0
  13. package/dist/generated/entities/warranty_claim_settings/index.js.map +7 -0
  14. package/dist/generated/entities/warranty_claim_sla_signal/index.js +25 -0
  15. package/dist/generated/entities/warranty_claim_sla_signal/index.js.map +7 -0
  16. package/dist/generated/entities/warranty_troubleshooting_guide/index.js +25 -0
  17. package/dist/generated/entities/warranty_troubleshooting_guide/index.js.map +7 -0
  18. package/dist/generated/entities/warranty_vendor_policy/index.js +31 -0
  19. package/dist/generated/entities/warranty_vendor_policy/index.js.map +7 -0
  20. package/dist/generated/entities.ids.generated.js +12 -0
  21. package/dist/generated/entities.ids.generated.js.map +2 -2
  22. package/dist/generated/entity-fields-registry.js +200 -0
  23. package/dist/generated/entity-fields-registry.js.map +2 -2
  24. package/dist/helpers/integration/api.js +3 -2
  25. package/dist/helpers/integration/api.js.map +2 -2
  26. package/dist/helpers/integration/salesFixtures.js +13 -6
  27. package/dist/helpers/integration/salesFixtures.js.map +2 -2
  28. package/dist/modules/attachments/di.js +18 -0
  29. package/dist/modules/attachments/di.js.map +2 -2
  30. package/dist/modules/attachments/fields/attachment.js +186 -26
  31. package/dist/modules/attachments/fields/attachment.js.map +2 -2
  32. package/dist/modules/attachments/lib/scoped-upload-service.js +202 -0
  33. package/dist/modules/attachments/lib/scoped-upload-service.js.map +7 -0
  34. package/dist/modules/attachments/lib/target-access-service.js +39 -0
  35. package/dist/modules/attachments/lib/target-access-service.js.map +7 -0
  36. package/dist/modules/auth/api/logout.js +37 -10
  37. package/dist/modules/auth/api/logout.js.map +2 -2
  38. package/dist/modules/auth/api/reset/confirm.js +7 -0
  39. package/dist/modules/auth/api/reset/confirm.js.map +2 -2
  40. package/dist/modules/auth/api/reset.js +7 -0
  41. package/dist/modules/auth/api/reset.js.map +2 -2
  42. package/dist/modules/auth/commands/users.js +13 -0
  43. package/dist/modules/auth/commands/users.js.map +2 -2
  44. package/dist/modules/auth/events.js +15 -0
  45. package/dist/modules/auth/events.js.map +2 -2
  46. package/dist/modules/customers/api/companies/route.js +1 -0
  47. package/dist/modules/customers/api/companies/route.js.map +2 -2
  48. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +2 -2
  49. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  50. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +2 -2
  51. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  52. package/dist/modules/customers/components/detail/assignableStaff.js +6 -69
  53. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  54. package/dist/modules/customers/lib/assignableStaff.js +76 -0
  55. package/dist/modules/customers/lib/assignableStaff.js.map +7 -0
  56. package/dist/modules/data_sync/lib/batch-stream.js +56 -0
  57. package/dist/modules/data_sync/lib/batch-stream.js.map +7 -0
  58. package/dist/modules/data_sync/lib/sync-engine.js +165 -103
  59. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  60. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +4 -0
  61. package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
  62. package/dist/modules/inbox_ops/api/extract/route.js +4 -0
  63. package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
  64. package/dist/modules/inbox_ops/api/webhook/inbound.js +4 -0
  65. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  66. package/dist/modules/sales/commands/documents.js +8 -8
  67. package/dist/modules/sales/commands/documents.js.map +2 -2
  68. package/dist/modules/warranty_claims/acl.js +75 -0
  69. package/dist/modules/warranty_claims/acl.js.map +7 -0
  70. package/dist/modules/warranty_claims/ai-agents.js +161 -0
  71. package/dist/modules/warranty_claims/ai-agents.js.map +7 -0
  72. package/dist/modules/warranty_claims/ai-tools.js +591 -0
  73. package/dist/modules/warranty_claims/ai-tools.js.map +7 -0
  74. package/dist/modules/warranty_claims/api/ai/assess/route.js +304 -0
  75. package/dist/modules/warranty_claims/api/ai/assess/route.js.map +7 -0
  76. package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +127 -0
  77. package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +7 -0
  78. package/dist/modules/warranty_claims/api/ai/suggest/route.js +177 -0
  79. package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +7 -0
  80. package/dist/modules/warranty_claims/api/assign/route.js +124 -0
  81. package/dist/modules/warranty_claims/api/assign/route.js.map +7 -0
  82. package/dist/modules/warranty_claims/api/assignees/route.js +97 -0
  83. package/dist/modules/warranty_claims/api/assignees/route.js.map +7 -0
  84. package/dist/modules/warranty_claims/api/credit-memo/route.js +152 -0
  85. package/dist/modules/warranty_claims/api/credit-memo/route.js.map +7 -0
  86. package/dist/modules/warranty_claims/api/entitlement/route.js +145 -0
  87. package/dist/modules/warranty_claims/api/entitlement/route.js.map +7 -0
  88. package/dist/modules/warranty_claims/api/events/route.js +199 -0
  89. package/dist/modules/warranty_claims/api/events/route.js.map +7 -0
  90. package/dist/modules/warranty_claims/api/external/claims/route.js +446 -0
  91. package/dist/modules/warranty_claims/api/external/claims/route.js.map +7 -0
  92. package/dist/modules/warranty_claims/api/lines/route.js +258 -0
  93. package/dist/modules/warranty_claims/api/lines/route.js.map +7 -0
  94. package/dist/modules/warranty_claims/api/openapi.js +27 -0
  95. package/dist/modules/warranty_claims/api/openapi.js.map +7 -0
  96. package/dist/modules/warranty_claims/api/portal/attachments/route.js +490 -0
  97. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +7 -0
  98. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js +178 -0
  99. package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js.map +7 -0
  100. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js +101 -0
  101. package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js.map +7 -0
  102. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +78 -0
  103. package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +7 -0
  104. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +81 -0
  105. package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +7 -0
  106. package/dist/modules/warranty_claims/api/portal/claims/route.js +462 -0
  107. package/dist/modules/warranty_claims/api/portal/claims/route.js.map +7 -0
  108. package/dist/modules/warranty_claims/api/portal/events/route.js +228 -0
  109. package/dist/modules/warranty_claims/api/portal/events/route.js.map +7 -0
  110. package/dist/modules/warranty_claims/api/portal/options/route.js +91 -0
  111. package/dist/modules/warranty_claims/api/portal/options/route.js.map +7 -0
  112. package/dist/modules/warranty_claims/api/portal/orders/lines/route.js +172 -0
  113. package/dist/modules/warranty_claims/api/portal/orders/lines/route.js.map +7 -0
  114. package/dist/modules/warranty_claims/api/portal/orders/route.js +151 -0
  115. package/dist/modules/warranty_claims/api/portal/orders/route.js.map +7 -0
  116. package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js +121 -0
  117. package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js.map +7 -0
  118. package/dist/modules/warranty_claims/api/receiving/route.js +176 -0
  119. package/dist/modules/warranty_claims/api/receiving/route.js.map +7 -0
  120. package/dist/modules/warranty_claims/api/registrations/route.js +382 -0
  121. package/dist/modules/warranty_claims/api/registrations/route.js.map +7 -0
  122. package/dist/modules/warranty_claims/api/replacement-order/route.js +150 -0
  123. package/dist/modules/warranty_claims/api/replacement-order/route.js.map +7 -0
  124. package/dist/modules/warranty_claims/api/return-label/route.js +235 -0
  125. package/dist/modules/warranty_claims/api/return-label/route.js.map +7 -0
  126. package/dist/modules/warranty_claims/api/risk/route.js +101 -0
  127. package/dist/modules/warranty_claims/api/risk/route.js.map +7 -0
  128. package/dist/modules/warranty_claims/api/route.js +482 -0
  129. package/dist/modules/warranty_claims/api/route.js.map +7 -0
  130. package/dist/modules/warranty_claims/api/sales-return/route.js +151 -0
  131. package/dist/modules/warranty_claims/api/sales-return/route.js.map +7 -0
  132. package/dist/modules/warranty_claims/api/settings-general/route.js +195 -0
  133. package/dist/modules/warranty_claims/api/settings-general/route.js.map +7 -0
  134. package/dist/modules/warranty_claims/api/stats/route.js +199 -0
  135. package/dist/modules/warranty_claims/api/stats/route.js.map +7 -0
  136. package/dist/modules/warranty_claims/api/submit/route.js +120 -0
  137. package/dist/modules/warranty_claims/api/submit/route.js.map +7 -0
  138. package/dist/modules/warranty_claims/api/transition/route.js +126 -0
  139. package/dist/modules/warranty_claims/api/transition/route.js.map +7 -0
  140. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js +261 -0
  141. package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js.map +7 -0
  142. package/dist/modules/warranty_claims/api/vendor-policies/route.js +311 -0
  143. package/dist/modules/warranty_claims/api/vendor-policies/route.js.map +7 -0
  144. package/dist/modules/warranty_claims/api/vendor-recovery/route.js +130 -0
  145. package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +7 -0
  146. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +153 -0
  147. package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +7 -0
  148. package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js +268 -0
  149. package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js.map +7 -0
  150. package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js +54 -0
  151. package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js.map +7 -0
  152. package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js +87 -0
  153. package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js.map +7 -0
  154. package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js +120 -0
  155. package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js.map +7 -0
  156. package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js +135 -0
  157. package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js.map +7 -0
  158. package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js +218 -0
  159. package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js.map +7 -0
  160. package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js +231 -0
  161. package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js.map +7 -0
  162. package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js +82 -0
  163. package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js.map +7 -0
  164. package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js +136 -0
  165. package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js.map +7 -0
  166. package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js +41 -0
  167. package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js.map +7 -0
  168. package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js +45 -0
  169. package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js.map +7 -0
  170. package/dist/modules/warranty_claims/backend/components/claimSla.js +92 -0
  171. package/dist/modules/warranty_claims/backend/components/claimSla.js.map +7 -0
  172. package/dist/modules/warranty_claims/backend/components/orderLookup.js +81 -0
  173. package/dist/modules/warranty_claims/backend/components/orderLookup.js.map +7 -0
  174. package/dist/modules/warranty_claims/backend/components/productLookup.js +317 -0
  175. package/dist/modules/warranty_claims/backend/components/productLookup.js.map +7 -0
  176. package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js +61 -0
  177. package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js.map +7 -0
  178. package/dist/modules/warranty_claims/backend/page.js +1088 -0
  179. package/dist/modules/warranty_claims/backend/page.js.map +7 -0
  180. package/dist/modules/warranty_claims/backend/page.meta.js +16 -0
  181. package/dist/modules/warranty_claims/backend/page.meta.js.map +7 -0
  182. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js +427 -0
  183. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js.map +7 -0
  184. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js +16 -0
  185. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js.map +7 -0
  186. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +2101 -0
  187. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +7 -0
  188. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js +16 -0
  189. package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js.map +7 -0
  190. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js +1249 -0
  191. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js.map +7 -0
  192. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js +16 -0
  193. package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js.map +7 -0
  194. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js +110 -0
  195. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js.map +7 -0
  196. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js +17 -0
  197. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js.map +7 -0
  198. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js +49 -0
  199. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js.map +7 -0
  200. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js +17 -0
  201. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js.map +7 -0
  202. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +485 -0
  203. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +7 -0
  204. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js +19 -0
  205. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js.map +7 -0
  206. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js +253 -0
  207. package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js.map +7 -0
  208. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +1565 -0
  209. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +7 -0
  210. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js +19 -0
  211. package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js.map +7 -0
  212. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js +472 -0
  213. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js.map +7 -0
  214. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js +115 -0
  215. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js.map +7 -0
  216. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js +19 -0
  217. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js.map +7 -0
  218. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js +54 -0
  219. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js.map +7 -0
  220. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js +19 -0
  221. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js.map +7 -0
  222. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +414 -0
  223. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +7 -0
  224. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js +21 -0
  225. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js.map +7 -0
  226. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js +171 -0
  227. package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js.map +7 -0
  228. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js +111 -0
  229. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js.map +7 -0
  230. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js +19 -0
  231. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js.map +7 -0
  232. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js +48 -0
  233. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js.map +7 -0
  234. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js +19 -0
  235. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js.map +7 -0
  236. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +429 -0
  237. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +7 -0
  238. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js +21 -0
  239. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js.map +7 -0
  240. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js +182 -0
  241. package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js.map +7 -0
  242. package/dist/modules/warranty_claims/ce.js +48 -0
  243. package/dist/modules/warranty_claims/ce.js.map +7 -0
  244. package/dist/modules/warranty_claims/commands/claim-lines.js +911 -0
  245. package/dist/modules/warranty_claims/commands/claim-lines.js.map +7 -0
  246. package/dist/modules/warranty_claims/commands/claims.js +2577 -0
  247. package/dist/modules/warranty_claims/commands/claims.js.map +7 -0
  248. package/dist/modules/warranty_claims/commands/index.js +4 -0
  249. package/dist/modules/warranty_claims/commands/index.js.map +7 -0
  250. package/dist/modules/warranty_claims/commands/settings.js +169 -0
  251. package/dist/modules/warranty_claims/commands/settings.js.map +7 -0
  252. package/dist/modules/warranty_claims/commands/shared.js +162 -0
  253. package/dist/modules/warranty_claims/commands/shared.js.map +7 -0
  254. package/dist/modules/warranty_claims/data/constants.js +35 -0
  255. package/dist/modules/warranty_claims/data/constants.js.map +7 -0
  256. package/dist/modules/warranty_claims/data/enrichers.js +97 -0
  257. package/dist/modules/warranty_claims/data/enrichers.js.map +7 -0
  258. package/dist/modules/warranty_claims/data/entities.js +731 -0
  259. package/dist/modules/warranty_claims/data/entities.js.map +7 -0
  260. package/dist/modules/warranty_claims/data/validators.js +488 -0
  261. package/dist/modules/warranty_claims/data/validators.js.map +7 -0
  262. package/dist/modules/warranty_claims/di.js +29 -0
  263. package/dist/modules/warranty_claims/di.js.map +7 -0
  264. package/dist/modules/warranty_claims/encryption.js +28 -0
  265. package/dist/modules/warranty_claims/encryption.js.map +7 -0
  266. package/dist/modules/warranty_claims/events.js +81 -0
  267. package/dist/modules/warranty_claims/events.js.map +7 -0
  268. package/dist/modules/warranty_claims/extension-points.js +19 -0
  269. package/dist/modules/warranty_claims/extension-points.js.map +7 -0
  270. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +825 -0
  271. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +7 -0
  272. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js +11 -0
  273. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js.map +7 -0
  274. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +1139 -0
  275. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +7 -0
  276. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js +11 -0
  277. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js.map +7 -0
  278. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +253 -0
  279. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +7 -0
  280. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js +18 -0
  281. package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js.map +7 -0
  282. package/dist/modules/warranty_claims/index.js +15 -0
  283. package/dist/modules/warranty_claims/index.js.map +7 -0
  284. package/dist/modules/warranty_claims/lib/aiAssist.js +496 -0
  285. package/dist/modules/warranty_claims/lib/aiAssist.js.map +7 -0
  286. package/dist/modules/warranty_claims/lib/apiIdsFilter.js +11 -0
  287. package/dist/modules/warranty_claims/lib/apiIdsFilter.js.map +7 -0
  288. package/dist/modules/warranty_claims/lib/assigneeNames.js +153 -0
  289. package/dist/modules/warranty_claims/lib/assigneeNames.js.map +7 -0
  290. package/dist/modules/warranty_claims/lib/attachmentVisibility.js +9 -0
  291. package/dist/modules/warranty_claims/lib/attachmentVisibility.js.map +7 -0
  292. package/dist/modules/warranty_claims/lib/bulkFeedback.js +8 -0
  293. package/dist/modules/warranty_claims/lib/bulkFeedback.js.map +7 -0
  294. package/dist/modules/warranty_claims/lib/businessHours.js +250 -0
  295. package/dist/modules/warranty_claims/lib/businessHours.js.map +7 -0
  296. package/dist/modules/warranty_claims/lib/businessHoursForm.js +146 -0
  297. package/dist/modules/warranty_claims/lib/businessHoursForm.js.map +7 -0
  298. package/dist/modules/warranty_claims/lib/claimTypeConfig.js +39 -0
  299. package/dist/modules/warranty_claims/lib/claimTypeConfig.js.map +7 -0
  300. package/dist/modules/warranty_claims/lib/decryptionSafety.js +17 -0
  301. package/dist/modules/warranty_claims/lib/decryptionSafety.js.map +7 -0
  302. package/dist/modules/warranty_claims/lib/deskFilters.js +88 -0
  303. package/dist/modules/warranty_claims/lib/deskFilters.js.map +7 -0
  304. package/dist/modules/warranty_claims/lib/dictionaries.js +157 -0
  305. package/dist/modules/warranty_claims/lib/dictionaries.js.map +7 -0
  306. package/dist/modules/warranty_claims/lib/dictionaryLabels.js +12 -0
  307. package/dist/modules/warranty_claims/lib/dictionaryLabels.js.map +7 -0
  308. package/dist/modules/warranty_claims/lib/emailIntake.js +87 -0
  309. package/dist/modules/warranty_claims/lib/emailIntake.js.map +7 -0
  310. package/dist/modules/warranty_claims/lib/escalation.js +58 -0
  311. package/dist/modules/warranty_claims/lib/escalation.js.map +7 -0
  312. package/dist/modules/warranty_claims/lib/externalIntake.js +233 -0
  313. package/dist/modules/warranty_claims/lib/externalIntake.js.map +7 -0
  314. package/dist/modules/warranty_claims/lib/grading.js +21 -0
  315. package/dist/modules/warranty_claims/lib/grading.js.map +7 -0
  316. package/dist/modules/warranty_claims/lib/listSegments.js +18 -0
  317. package/dist/modules/warranty_claims/lib/listSegments.js.map +7 -0
  318. package/dist/modules/warranty_claims/lib/orderOwnership.js +25 -0
  319. package/dist/modules/warranty_claims/lib/orderOwnership.js.map +7 -0
  320. package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js +11 -0
  321. package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js.map +7 -0
  322. package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js +108 -0
  323. package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js.map +7 -0
  324. package/dist/modules/warranty_claims/lib/portalClaimAccess.js +26 -0
  325. package/dist/modules/warranty_claims/lib/portalClaimAccess.js.map +7 -0
  326. package/dist/modules/warranty_claims/lib/quantity.js +19 -0
  327. package/dist/modules/warranty_claims/lib/quantity.js.map +7 -0
  328. package/dist/modules/warranty_claims/lib/risk.js +204 -0
  329. package/dist/modules/warranty_claims/lib/risk.js.map +7 -0
  330. package/dist/modules/warranty_claims/lib/settings.js +55 -0
  331. package/dist/modules/warranty_claims/lib/settings.js.map +7 -0
  332. package/dist/modules/warranty_claims/lib/stateMachine.js +88 -0
  333. package/dist/modules/warranty_claims/lib/stateMachine.js.map +7 -0
  334. package/dist/modules/warranty_claims/lib/timelineFilters.js +16 -0
  335. package/dist/modules/warranty_claims/lib/timelineFilters.js.map +7 -0
  336. package/dist/modules/warranty_claims/lib/triage.js +241 -0
  337. package/dist/modules/warranty_claims/lib/triage.js.map +7 -0
  338. package/dist/modules/warranty_claims/lib/troubleshooting.js +111 -0
  339. package/dist/modules/warranty_claims/lib/troubleshooting.js.map +7 -0
  340. package/dist/modules/warranty_claims/lib/tryResolve.js +11 -0
  341. package/dist/modules/warranty_claims/lib/tryResolve.js.map +7 -0
  342. package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js +101 -0
  343. package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js.map +7 -0
  344. package/dist/modules/warranty_claims/lib/warrantyPreview.js +18 -0
  345. package/dist/modules/warranty_claims/lib/warrantyPreview.js.map +7 -0
  346. package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js +32 -0
  347. package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js.map +7 -0
  348. package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js +16 -0
  349. package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js.map +7 -0
  350. package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js +17 -0
  351. package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js.map +7 -0
  352. package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js +29 -0
  353. package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js.map +7 -0
  354. package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js +25 -0
  355. package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js.map +7 -0
  356. package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js +13 -0
  357. package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js.map +7 -0
  358. package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js +15 -0
  359. package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js.map +7 -0
  360. package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js +13 -0
  361. package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js.map +7 -0
  362. package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js +13 -0
  363. package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js.map +7 -0
  364. package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js +12 -0
  365. package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js.map +7 -0
  366. package/dist/modules/warranty_claims/notifications.client.js +67 -0
  367. package/dist/modules/warranty_claims/notifications.client.js.map +7 -0
  368. package/dist/modules/warranty_claims/notifications.js +103 -0
  369. package/dist/modules/warranty_claims/notifications.js.map +7 -0
  370. package/dist/modules/warranty_claims/search.js +174 -0
  371. package/dist/modules/warranty_claims/search.js.map +7 -0
  372. package/dist/modules/warranty_claims/services/adjudicationEvaluator.js +202 -0
  373. package/dist/modules/warranty_claims/services/adjudicationEvaluator.js.map +7 -0
  374. package/dist/modules/warranty_claims/services/claimNumberGenerator.js +58 -0
  375. package/dist/modules/warranty_claims/services/claimNumberGenerator.js.map +7 -0
  376. package/dist/modules/warranty_claims/services/entitlementResolver.js +69 -0
  377. package/dist/modules/warranty_claims/services/entitlementResolver.js.map +7 -0
  378. package/dist/modules/warranty_claims/services/returnLabelProvider.js +14 -0
  379. package/dist/modules/warranty_claims/services/returnLabelProvider.js.map +7 -0
  380. package/dist/modules/warranty_claims/setup.js +121 -0
  381. package/dist/modules/warranty_claims/setup.js.map +7 -0
  382. package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js +119 -0
  383. package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js.map +7 -0
  384. package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js +47 -0
  385. package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js.map +7 -0
  386. package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js +60 -0
  387. package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js.map +7 -0
  388. package/dist/modules/warranty_claims/subscribers/claim-status-notification.js +83 -0
  389. package/dist/modules/warranty_claims/subscribers/claim-status-notification.js.map +7 -0
  390. package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js +46 -0
  391. package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js.map +7 -0
  392. package/dist/modules/warranty_claims/subscribers/email-to-claim.js +135 -0
  393. package/dist/modules/warranty_claims/subscribers/email-to-claim.js.map +7 -0
  394. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +164 -0
  395. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +7 -0
  396. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js +24 -0
  397. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js.map +7 -0
  398. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js +26 -0
  399. package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js.map +7 -0
  400. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js +15 -0
  401. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js.map +7 -0
  402. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js +197 -0
  403. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js.map +7 -0
  404. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js +28 -0
  405. package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js.map +7 -0
  406. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js +418 -0
  407. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js.map +7 -0
  408. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js +18 -0
  409. package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js.map +7 -0
  410. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js +17 -0
  411. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js.map +7 -0
  412. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js +146 -0
  413. package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js.map +7 -0
  414. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js +17 -0
  415. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js.map +7 -0
  416. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js +147 -0
  417. package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js.map +7 -0
  418. package/dist/modules/warranty_claims/widgets/injection-table.js +38 -0
  419. package/dist/modules/warranty_claims/widgets/injection-table.js.map +7 -0
  420. package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js +223 -0
  421. package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js.map +7 -0
  422. package/dist/modules/warranty_claims/widgets/notifications/index.js +5 -0
  423. package/dist/modules/warranty_claims/widgets/notifications/index.js.map +7 -0
  424. package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js +334 -0
  425. package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js.map +7 -0
  426. package/generated/entities/warranty_claim/index.ts +51 -0
  427. package/generated/entities/warranty_claim_event/index.ts +11 -0
  428. package/generated/entities/warranty_claim_line/index.ts +37 -0
  429. package/generated/entities/warranty_claim_registration/index.ts +20 -0
  430. package/generated/entities/warranty_claim_sequence/index.ts +7 -0
  431. package/generated/entities/warranty_claim_settings/index.ts +20 -0
  432. package/generated/entities/warranty_claim_sla_signal/index.ts +11 -0
  433. package/generated/entities/warranty_troubleshooting_guide/index.ts +11 -0
  434. package/generated/entities/warranty_vendor_policy/index.ts +14 -0
  435. package/generated/entities.ids.generated.ts +12 -0
  436. package/generated/entity-fields-registry.ts +200 -0
  437. package/package.json +7 -7
  438. package/src/helpers/integration/api.ts +10 -3
  439. package/src/helpers/integration/salesFixtures.ts +17 -7
  440. package/src/modules/attachments/di.ts +30 -1
  441. package/src/modules/attachments/fields/attachment.tsx +215 -23
  442. package/src/modules/attachments/i18n/de.json +5 -0
  443. package/src/modules/attachments/i18n/en.json +5 -0
  444. package/src/modules/attachments/i18n/es.json +5 -0
  445. package/src/modules/attachments/i18n/ko.json +5 -0
  446. package/src/modules/attachments/i18n/pl.json +5 -0
  447. package/src/modules/attachments/lib/scoped-upload-service.ts +257 -0
  448. package/src/modules/attachments/lib/target-access-service.ts +57 -0
  449. package/src/modules/auth/api/logout.ts +46 -10
  450. package/src/modules/auth/api/reset/confirm.ts +7 -0
  451. package/src/modules/auth/api/reset.ts +7 -0
  452. package/src/modules/auth/commands/users.ts +22 -0
  453. package/src/modules/auth/events.ts +15 -0
  454. package/src/modules/customers/api/companies/route.ts +1 -0
  455. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +4 -2
  456. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +4 -2
  457. package/src/modules/customers/components/detail/assignableStaff.ts +14 -177
  458. package/src/modules/customers/lib/assignableStaff.ts +177 -0
  459. package/src/modules/data_sync/AGENTS.md +3 -0
  460. package/src/modules/data_sync/lib/batch-stream.ts +117 -0
  461. package/src/modules/data_sync/lib/sync-engine.ts +190 -115
  462. package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +4 -0
  463. package/src/modules/inbox_ops/api/extract/route.ts +4 -0
  464. package/src/modules/inbox_ops/api/webhook/inbound.ts +4 -0
  465. package/src/modules/sales/commands/documents.ts +16 -8
  466. package/src/modules/warranty_claims/AGENTS.md +49 -0
  467. package/src/modules/warranty_claims/acl.ts +71 -0
  468. package/src/modules/warranty_claims/ai-agents.ts +189 -0
  469. package/src/modules/warranty_claims/ai-tools.ts +711 -0
  470. package/src/modules/warranty_claims/api/ai/assess/route.ts +371 -0
  471. package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +149 -0
  472. package/src/modules/warranty_claims/api/ai/suggest/route.ts +190 -0
  473. package/src/modules/warranty_claims/api/assign/route.ts +142 -0
  474. package/src/modules/warranty_claims/api/assignees/route.ts +102 -0
  475. package/src/modules/warranty_claims/api/credit-memo/route.ts +191 -0
  476. package/src/modules/warranty_claims/api/entitlement/route.ts +224 -0
  477. package/src/modules/warranty_claims/api/events/route.ts +226 -0
  478. package/src/modules/warranty_claims/api/external/claims/route.ts +528 -0
  479. package/src/modules/warranty_claims/api/lines/route.ts +275 -0
  480. package/src/modules/warranty_claims/api/openapi.ts +28 -0
  481. package/src/modules/warranty_claims/api/portal/attachments/route.ts +547 -0
  482. package/src/modules/warranty_claims/api/portal/claims/[id]/route.ts +195 -0
  483. package/src/modules/warranty_claims/api/portal/claims/[id]/shared.ts +125 -0
  484. package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +82 -0
  485. package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +87 -0
  486. package/src/modules/warranty_claims/api/portal/claims/route.ts +579 -0
  487. package/src/modules/warranty_claims/api/portal/events/route.ts +260 -0
  488. package/src/modules/warranty_claims/api/portal/options/route.ts +113 -0
  489. package/src/modules/warranty_claims/api/portal/orders/lines/route.ts +258 -0
  490. package/src/modules/warranty_claims/api/portal/orders/route.ts +206 -0
  491. package/src/modules/warranty_claims/api/portal/troubleshooting/route.ts +151 -0
  492. package/src/modules/warranty_claims/api/receiving/route.ts +215 -0
  493. package/src/modules/warranty_claims/api/registrations/route.ts +417 -0
  494. package/src/modules/warranty_claims/api/replacement-order/route.ts +188 -0
  495. package/src/modules/warranty_claims/api/return-label/route.ts +291 -0
  496. package/src/modules/warranty_claims/api/risk/route.ts +112 -0
  497. package/src/modules/warranty_claims/api/route.ts +543 -0
  498. package/src/modules/warranty_claims/api/sales-return/route.ts +183 -0
  499. package/src/modules/warranty_claims/api/settings-general/route.ts +230 -0
  500. package/src/modules/warranty_claims/api/stats/route.ts +306 -0
  501. package/src/modules/warranty_claims/api/submit/route.ts +146 -0
  502. package/src/modules/warranty_claims/api/transition/route.ts +144 -0
  503. package/src/modules/warranty_claims/api/troubleshooting-guides/route.ts +295 -0
  504. package/src/modules/warranty_claims/api/vendor-policies/route.ts +351 -0
  505. package/src/modules/warranty_claims/api/vendor-recovery/route.ts +148 -0
  506. package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +167 -0
  507. package/src/modules/warranty_claims/backend/components/AiAssessButtons.tsx +389 -0
  508. package/src/modules/warranty_claims/backend/components/ClaimStageProgress.tsx +65 -0
  509. package/src/modules/warranty_claims/backend/components/ClaimStatusBadge.tsx +137 -0
  510. package/src/modules/warranty_claims/backend/components/ClaimsKpiStrip.tsx +169 -0
  511. package/src/modules/warranty_claims/backend/components/EntitlementLookupBadge.tsx +203 -0
  512. package/src/modules/warranty_claims/backend/components/ReceivingPanel.tsx +300 -0
  513. package/src/modules/warranty_claims/backend/components/ReturnLabelPanel.tsx +292 -0
  514. package/src/modules/warranty_claims/backend/components/TroubleshootingWalker.tsx +125 -0
  515. package/src/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.tsx +217 -0
  516. package/src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx +66 -0
  517. package/src/modules/warranty_claims/backend/components/claimReasonOptions.ts +57 -0
  518. package/src/modules/warranty_claims/backend/components/claimSla.tsx +127 -0
  519. package/src/modules/warranty_claims/backend/components/orderLookup.ts +109 -0
  520. package/src/modules/warranty_claims/backend/components/productLookup.tsx +433 -0
  521. package/src/modules/warranty_claims/backend/components/useUserDisplayNames.ts +66 -0
  522. package/src/modules/warranty_claims/backend/page.meta.ts +12 -0
  523. package/src/modules/warranty_claims/backend/page.tsx +1302 -0
  524. package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.ts +12 -0
  525. package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.tsx +531 -0
  526. package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.ts +12 -0
  527. package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +2599 -0
  528. package/src/modules/warranty_claims/backend/warranty_claims/create/page.meta.ts +12 -0
  529. package/src/modules/warranty_claims/backend/warranty_claims/create/page.tsx +1533 -0
  530. package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.ts +13 -0
  531. package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.tsx +136 -0
  532. package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.ts +13 -0
  533. package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.tsx +52 -0
  534. package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.ts +15 -0
  535. package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +555 -0
  536. package/src/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.tsx +295 -0
  537. package/src/modules/warranty_claims/backend/warranty_claims/settings/page.meta.ts +15 -0
  538. package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +1903 -0
  539. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.tsx +550 -0
  540. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.ts +15 -0
  541. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.tsx +141 -0
  542. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.ts +15 -0
  543. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.tsx +57 -0
  544. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.ts +17 -0
  545. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +477 -0
  546. package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.tsx +214 -0
  547. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.ts +15 -0
  548. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.tsx +137 -0
  549. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.ts +15 -0
  550. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.tsx +51 -0
  551. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.ts +17 -0
  552. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +506 -0
  553. package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.tsx +220 -0
  554. package/src/modules/warranty_claims/ce.ts +44 -0
  555. package/src/modules/warranty_claims/commands/claim-lines.ts +1082 -0
  556. package/src/modules/warranty_claims/commands/claims.ts +3364 -0
  557. package/src/modules/warranty_claims/commands/index.ts +3 -0
  558. package/src/modules/warranty_claims/commands/settings.ts +198 -0
  559. package/src/modules/warranty_claims/commands/shared.ts +212 -0
  560. package/src/modules/warranty_claims/data/constants.ts +37 -0
  561. package/src/modules/warranty_claims/data/enrichers.ts +165 -0
  562. package/src/modules/warranty_claims/data/entities.ts +663 -0
  563. package/src/modules/warranty_claims/data/validators.ts +634 -0
  564. package/src/modules/warranty_claims/di.ts +34 -0
  565. package/src/modules/warranty_claims/encryption.ts +26 -0
  566. package/src/modules/warranty_claims/events.ts +81 -0
  567. package/src/modules/warranty_claims/extension-points.ts +16 -0
  568. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.ts +9 -0
  569. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +1037 -0
  570. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.ts +9 -0
  571. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +1409 -0
  572. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.ts +16 -0
  573. package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +304 -0
  574. package/src/modules/warranty_claims/i18n/de.json +1270 -0
  575. package/src/modules/warranty_claims/i18n/en.json +1270 -0
  576. package/src/modules/warranty_claims/i18n/es.json +1270 -0
  577. package/src/modules/warranty_claims/i18n/ko.json +1270 -0
  578. package/src/modules/warranty_claims/i18n/pl.json +1270 -0
  579. package/src/modules/warranty_claims/index.ts +13 -0
  580. package/src/modules/warranty_claims/lib/aiAssist.ts +677 -0
  581. package/src/modules/warranty_claims/lib/apiIdsFilter.ts +12 -0
  582. package/src/modules/warranty_claims/lib/assigneeNames.ts +184 -0
  583. package/src/modules/warranty_claims/lib/attachmentVisibility.ts +5 -0
  584. package/src/modules/warranty_claims/lib/bulkFeedback.ts +6 -0
  585. package/src/modules/warranty_claims/lib/businessHours.ts +333 -0
  586. package/src/modules/warranty_claims/lib/businessHoursForm.ts +206 -0
  587. package/src/modules/warranty_claims/lib/claimTypeConfig.ts +49 -0
  588. package/src/modules/warranty_claims/lib/decryptionSafety.ts +19 -0
  589. package/src/modules/warranty_claims/lib/deskFilters.ts +131 -0
  590. package/src/modules/warranty_claims/lib/dictionaries.ts +212 -0
  591. package/src/modules/warranty_claims/lib/dictionaryLabels.ts +16 -0
  592. package/src/modules/warranty_claims/lib/emailIntake.ts +123 -0
  593. package/src/modules/warranty_claims/lib/escalation.ts +83 -0
  594. package/src/modules/warranty_claims/lib/externalIntake.ts +362 -0
  595. package/src/modules/warranty_claims/lib/grading.ts +23 -0
  596. package/src/modules/warranty_claims/lib/listSegments.ts +19 -0
  597. package/src/modules/warranty_claims/lib/orderOwnership.ts +58 -0
  598. package/src/modules/warranty_claims/lib/portalAttachmentUpload.ts +11 -0
  599. package/src/modules/warranty_claims/lib/portalAttachmentValidation.ts +115 -0
  600. package/src/modules/warranty_claims/lib/portalClaimAccess.ts +37 -0
  601. package/src/modules/warranty_claims/lib/quantity.ts +15 -0
  602. package/src/modules/warranty_claims/lib/risk.ts +371 -0
  603. package/src/modules/warranty_claims/lib/settings.ts +76 -0
  604. package/src/modules/warranty_claims/lib/stateMachine.ts +130 -0
  605. package/src/modules/warranty_claims/lib/timelineFilters.ts +22 -0
  606. package/src/modules/warranty_claims/lib/triage.ts +366 -0
  607. package/src/modules/warranty_claims/lib/troubleshooting.ts +157 -0
  608. package/src/modules/warranty_claims/lib/tryResolve.ts +7 -0
  609. package/src/modules/warranty_claims/lib/vendorPolicyRecovery.ts +182 -0
  610. package/src/modules/warranty_claims/lib/warrantyPreview.ts +20 -0
  611. package/src/modules/warranty_claims/migrations/.snapshot-open-mercato.json +3525 -0
  612. package/src/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.ts +38 -0
  613. package/src/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.ts +17 -0
  614. package/src/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.ts +17 -0
  615. package/src/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.ts +37 -0
  616. package/src/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.ts +25 -0
  617. package/src/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.ts +13 -0
  618. package/src/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.ts +15 -0
  619. package/src/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.ts +13 -0
  620. package/src/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.ts +13 -0
  621. package/src/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.ts +11 -0
  622. package/src/modules/warranty_claims/notifications.client.ts +78 -0
  623. package/src/modules/warranty_claims/notifications.ts +101 -0
  624. package/src/modules/warranty_claims/search.ts +199 -0
  625. package/src/modules/warranty_claims/services/adjudicationEvaluator.ts +344 -0
  626. package/src/modules/warranty_claims/services/claimNumberGenerator.ts +69 -0
  627. package/src/modules/warranty_claims/services/entitlementResolver.ts +102 -0
  628. package/src/modules/warranty_claims/services/returnLabelProvider.ts +30 -0
  629. package/src/modules/warranty_claims/setup.ts +154 -0
  630. package/src/modules/warranty_claims/subscribers/auto-vendor-recovery.ts +140 -0
  631. package/src/modules/warranty_claims/subscribers/claim-assigned-notification.ts +54 -0
  632. package/src/modules/warranty_claims/subscribers/claim-customer-reply-notification.ts +68 -0
  633. package/src/modules/warranty_claims/subscribers/claim-status-notification.ts +100 -0
  634. package/src/modules/warranty_claims/subscribers/claim-submitted-notification.ts +53 -0
  635. package/src/modules/warranty_claims/subscribers/email-to-claim.ts +175 -0
  636. package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +226 -0
  637. package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.ts +28 -0
  638. package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.ts +31 -0
  639. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/config.ts +15 -0
  640. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.tsx +237 -0
  641. package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.ts +27 -0
  642. package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.tsx +527 -0
  643. package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.ts +37 -0
  644. package/src/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.ts +21 -0
  645. package/src/modules/warranty_claims/widgets/injection/customer-claims-tab.tsx +210 -0
  646. package/src/modules/warranty_claims/widgets/injection/order-claims-tab/widget.ts +21 -0
  647. package/src/modules/warranty_claims/widgets/injection/order-claims-tab.tsx +210 -0
  648. package/src/modules/warranty_claims/widgets/injection-table.ts +36 -0
  649. package/src/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.tsx +253 -0
  650. package/src/modules/warranty_claims/widgets/notifications/index.ts +1 -0
  651. package/src/modules/warranty_claims/workers/sla-escalation-sweep.ts +450 -0
@@ -0,0 +1,1533 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useRouter, useSearchParams } from 'next/navigation'
5
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
6
+ import { CheckCircle2, Plus, ShieldCheck } from 'lucide-react'
7
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
+ import { CrudForm, type CrudField, type CrudFieldOption, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
9
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
10
+ import { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'
11
+ import { createCrud } from '@open-mercato/ui/backend/utils/crud'
12
+ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
13
+ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
14
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
+ import { Button } from '@open-mercato/ui/primitives/button'
16
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
17
+ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
18
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
19
+ import { Input } from '@open-mercato/ui/primitives/input'
20
+ import { Label } from '@open-mercato/ui/primitives/label'
21
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '@open-mercato/ui/primitives/select'
29
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
30
+ import { CollapsibleSection } from '@open-mercato/ui/backend/SectionHeader'
31
+ import { type TranslateFn, useT } from '@open-mercato/shared/lib/i18n/context'
32
+ import {
33
+ ClaimLineProductPicker,
34
+ EntitlementChip,
35
+ computeEntitlementPreview,
36
+ type ClaimProductPick,
37
+ } from '../../components/productLookup'
38
+ import {
39
+ loadOrderOptions as loadOrderOptionsShared,
40
+ resolveOrderLabel,
41
+ } from '../../components/orderLookup'
42
+ import { EntitlementLookupBadge } from '../../components/EntitlementLookupBadge'
43
+ import { TroubleshootingWalker, type TroubleshootingWalkerGuide } from '../../components/TroubleshootingWalker'
44
+ import { resolveClaimTypeUiConfig } from '../../../lib/claimTypeConfig'
45
+ import { formatQuantity, parseQuantity } from '../../../lib/quantity'
46
+ import { localizeDictionaryLabel, type DictionaryLabelKind } from '../../../lib/dictionaryLabels'
47
+ import {
48
+ parseGuideSteps,
49
+ selectBestGuide,
50
+ type TroubleshootingNode,
51
+ } from '../../../lib/troubleshooting'
52
+
53
+ type ClaimCreateLineValues = {
54
+ productId?: string | null
55
+ variantId?: string | null
56
+ orderLineId?: string | null
57
+ productName?: string | null
58
+ sku?: string | null
59
+ serialNumber?: string | null
60
+ purchaseDate?: string | null
61
+ warrantyMonths?: number | string | null
62
+ faultCode?: string | null
63
+ faultDescription?: string | null
64
+ qtyClaimed?: number | string | null
65
+ }
66
+
67
+ type ClaimCreateFormValues = {
68
+ claimType: string
69
+ customerId?: string | null
70
+ orderId?: string | null
71
+ priority: string
72
+ reasonCode?: string | null
73
+ resolutionSummary?: string | null
74
+ notes?: string | null
75
+ lines?: ClaimCreateLineValues[]
76
+ }
77
+
78
+ type DictionaryListItem = {
79
+ id?: string
80
+ key?: string
81
+ }
82
+
83
+ type DictionaryEntriesResponse = {
84
+ items?: unknown[]
85
+ }
86
+
87
+ type GeneralSettingsResponse = {
88
+ result?: {
89
+ defaultWarrantyMonths?: unknown
90
+ default_warranty_months?: unknown
91
+ }
92
+ }
93
+
94
+ type TroubleshootingGuideListResponse = {
95
+ items?: unknown[]
96
+ }
97
+
98
+ type TroubleshootingGuideCandidate = {
99
+ id: string
100
+ title: string
101
+ claimType: string | null
102
+ reasonCode: string | null
103
+ isActive: boolean
104
+ }
105
+
106
+ type TroubleshootingGuideDetail = TroubleshootingGuideCandidate & {
107
+ steps: TroubleshootingNode | null
108
+ }
109
+
110
+ type SalesOrderLine = {
111
+ id: string
112
+ productId: string | null
113
+ variantId: string | null
114
+ sku: string | null
115
+ name: string | null
116
+ quantity: number | string | null
117
+ }
118
+
119
+ // Rows carry their position in the form's `lines` array so table actions can edit
120
+ // or drop the right entry after search and pagination have reordered what is shown.
121
+ type ClaimLineRow = ClaimCreateLineValues & {
122
+ id: string
123
+ index: number
124
+ lineNo: number
125
+ }
126
+
127
+ type LineDialogState =
128
+ | { mode: 'create' }
129
+ | { mode: 'edit'; index: number }
130
+
131
+ type LineEditorTranslations = {
132
+ addLine: string
133
+ removeLine: string
134
+ productName: string
135
+ sku: string
136
+ serialNumber: string
137
+ faultCode: string
138
+ faultDescription: string
139
+ qtyClaimed: string
140
+ faultCodePlaceholder: string
141
+ }
142
+
143
+ // The sales list API caps a page at 100, so a large order needs several round trips.
144
+ // The hard cap keeps a pathological order from locking the dialog up forever; when it
145
+ // bites, the dialog says so instead of silently offering a partial list.
146
+ const ORDER_LINE_PAGE_SIZE = 100
147
+ const ORDER_LINE_MAX_FETCH = 2000
148
+ // Mirrors `lines: z.array(...).max(200)` in data/validators.ts.
149
+ const MAX_CLAIM_LINES = 200
150
+ const LINE_PAGE_SIZE = 20
151
+
152
+ // Vendor-recovery claims are created only via the dedicated vendor-recovery action (they need
153
+ // an existing claim with resolved lines), so they are intentionally excluded from standard
154
+ // intake — selecting one here previously produced an HTTP 400 dead path (INTAKE-02).
155
+ const CLAIM_TYPES = ['warranty', 'return', 'core_return'] as const
156
+ const CLAIM_PRIORITIES = ['low', 'normal', 'high', 'urgent'] as const
157
+ const DICTIONARY_KEYS = {
158
+ faultCodes: 'warranty_claims.warranty_claim_fault_code',
159
+ claimReasons: 'warranty_claims.warranty_claim_reason',
160
+ } as const
161
+
162
+ function normalizeClaimType(value: string | null): (typeof CLAIM_TYPES)[number] {
163
+ return CLAIM_TYPES.find((claimType) => claimType === value) ?? 'warranty'
164
+ }
165
+
166
+ function isRecord(value: unknown): value is Record<string, unknown> {
167
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
168
+ }
169
+
170
+ function toStringOrNull(value: unknown): string | null {
171
+ return typeof value === 'string' && value.trim().length ? value.trim() : null
172
+ }
173
+
174
+ function normalizeOption(item: unknown, t: TranslateFn): CrudFieldOption | null {
175
+ if (!isRecord(item)) return null
176
+ const id = toStringOrNull(item.id)
177
+ if (!id) return null
178
+ const label =
179
+ toStringOrNull(item.label) ??
180
+ toStringOrNull(item.displayName) ??
181
+ toStringOrNull(item.display_name) ??
182
+ toStringOrNull(item.name) ??
183
+ t('warranty_claims.form.customerUnnamed', 'Unnamed customer')
184
+ const email = toStringOrNull(item.primaryEmail) ?? toStringOrNull(item.primary_email)
185
+ return { value: id, label: email ? `${label} (${email})` : label }
186
+ }
187
+
188
+ function normalizeDictionaryOption(item: unknown): CrudFieldOption | null {
189
+ if (!isRecord(item)) return null
190
+ const value = toStringOrNull(item.value)
191
+ if (!value) return null
192
+ const label = toStringOrNull(item.label) ?? value
193
+ return { value, label }
194
+ }
195
+
196
+ function readBoolean(value: unknown): boolean {
197
+ return value === true || value === 'true' || value === 1 || value === '1'
198
+ }
199
+
200
+ function normalizeTroubleshootingGuideCandidate(item: unknown): TroubleshootingGuideCandidate | null {
201
+ if (!isRecord(item)) return null
202
+ const id = toStringOrNull(item.id)
203
+ const title = toStringOrNull(item.title)
204
+ if (!id || !title) return null
205
+ return {
206
+ id,
207
+ title,
208
+ claimType: toStringOrNull(item.claimType) ?? toStringOrNull(item.claim_type),
209
+ reasonCode: toStringOrNull(item.reasonCode) ?? toStringOrNull(item.reason_code),
210
+ isActive: readBoolean(item.isActive ?? item.is_active),
211
+ }
212
+ }
213
+
214
+ function normalizeTroubleshootingGuideDetail(item: unknown): TroubleshootingGuideDetail | null {
215
+ const candidate = normalizeTroubleshootingGuideCandidate(item)
216
+ if (!candidate || !isRecord(item)) return null
217
+ return {
218
+ ...candidate,
219
+ steps: parseGuideSteps(item.steps),
220
+ }
221
+ }
222
+
223
+ async function loadStaffTroubleshootingGuide(
224
+ claimType: string | null,
225
+ reasonCode: string | null,
226
+ ): Promise<TroubleshootingWalkerGuide | null> {
227
+ if (!claimType) return null
228
+ const listParams = new URLSearchParams({
229
+ isActive: 'true',
230
+ page: '1',
231
+ pageSize: '100',
232
+ sortField: 'updatedAt',
233
+ sortDir: 'desc',
234
+ })
235
+ const listCall = await apiCall<TroubleshootingGuideListResponse>(
236
+ `/api/warranty_claims/troubleshooting-guides?${listParams.toString()}`,
237
+ undefined,
238
+ { fallback: { items: [] } },
239
+ )
240
+ if (!listCall.ok) return null
241
+
242
+ const candidates = (listCall.result?.items ?? [])
243
+ .map(normalizeTroubleshootingGuideCandidate)
244
+ .filter((guide): guide is TroubleshootingGuideCandidate => guide !== null)
245
+ const selected = selectBestGuide(candidates, claimType, reasonCode)
246
+ if (!selected) return null
247
+
248
+ const detailParams = new URLSearchParams({
249
+ ids: selected.id,
250
+ page: '1',
251
+ pageSize: '1',
252
+ })
253
+ const detailCall = await apiCall<TroubleshootingGuideListResponse>(
254
+ `/api/warranty_claims/troubleshooting-guides?${detailParams.toString()}`,
255
+ undefined,
256
+ { fallback: { items: [] } },
257
+ )
258
+ if (!detailCall.ok) return null
259
+
260
+ const detail = normalizeTroubleshootingGuideDetail(detailCall.result?.items?.[0])
261
+ if (!detail?.steps) return null
262
+ return { title: detail.title, steps: detail.steps }
263
+ }
264
+
265
+ function readCatalogSnapshotName(value: unknown): string | null {
266
+ if (!isRecord(value)) return null
267
+ const product = isRecord(value.product) ? value.product : null
268
+ const variant = isRecord(value.variant) ? value.variant : null
269
+ return toStringOrNull(variant?.title) ?? toStringOrNull(product?.title) ?? null
270
+ }
271
+
272
+ function normalizeSalesOrderLine(item: unknown): SalesOrderLine | null {
273
+ if (!isRecord(item)) return null
274
+ const id = toStringOrNull(item.id)
275
+ if (!id) return null
276
+ const kind = toStringOrNull(item.kind)
277
+ if (kind !== 'product') return null
278
+ return {
279
+ id,
280
+ productId: toStringOrNull(item.product_id) ?? toStringOrNull(item.productId),
281
+ variantId: toStringOrNull(item.product_variant_id) ?? toStringOrNull(item.productVariantId),
282
+ sku: toStringOrNull(item.sku),
283
+ name: toStringOrNull(item.name) ?? readCatalogSnapshotName(item.catalog_snapshot ?? item.catalogSnapshot),
284
+ quantity: typeof item.quantity === 'number' || typeof item.quantity === 'string' ? item.quantity : null,
285
+ }
286
+ }
287
+
288
+ function nullableText(value: unknown): string | null {
289
+ const next = toStringOrNull(value)
290
+ return next ?? null
291
+ }
292
+
293
+ function stringifyFieldValue(value: unknown): string {
294
+ if (typeof value === 'number') return String(value)
295
+ if (typeof value === 'string') return value
296
+ return ''
297
+ }
298
+
299
+ function dateInputValue(value: unknown): string {
300
+ if (typeof value !== 'string') return ''
301
+ const trimmed = value.trim()
302
+ return trimmed.length >= 10 ? trimmed.slice(0, 10) : trimmed
303
+ }
304
+
305
+ function dateFromInputValue(value: unknown): Date | null {
306
+ const dateValue = dateInputValue(value)
307
+ if (!dateValue) return null
308
+ const date = new Date(`${dateValue}T00:00:00.000Z`)
309
+ return Number.isNaN(date.getTime()) ? null : date
310
+ }
311
+
312
+ function parsePositiveNumber(value: unknown): number | null {
313
+ if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? value : null
314
+ if (typeof value !== 'string') return null
315
+ const trimmed = value.trim()
316
+ if (!trimmed) return null
317
+ const parsed = Number(trimmed)
318
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null
319
+ }
320
+
321
+ function parseWarrantyMonths(value: unknown): number | null {
322
+ if (typeof value === 'number') return Number.isInteger(value) && value >= 0 ? value : null
323
+ if (typeof value !== 'string') return null
324
+ const trimmed = value.trim()
325
+ if (!trimmed) return null
326
+ const parsed = Number(trimmed)
327
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : null
328
+ }
329
+
330
+ function isWarrantyMonthsEmpty(value: unknown): boolean {
331
+ if (value === null || value === undefined) return true
332
+ return typeof value === 'string' && !value.trim()
333
+ }
334
+
335
+ function normalizeDateOnly(value: unknown): string | null {
336
+ const normalized = dateInputValue(value)
337
+ return normalized || null
338
+ }
339
+
340
+ function assignStringIfSet(payload: Record<string, unknown>, key: string, value: unknown): void {
341
+ const normalized = nullableText(value)
342
+ if (normalized) payload[key] = normalized
343
+ }
344
+
345
+ function readDefaultWarrantyMonths(value: unknown): number | null {
346
+ if (!isRecord(value)) return null
347
+ const settings = isRecord(value.result) ? value.result : value
348
+ const months = parseWarrantyMonths(settings.defaultWarrantyMonths ?? settings.default_warranty_months)
349
+ return months
350
+ }
351
+
352
+ function matchesLineSearch(line: ClaimCreateLineValues, term: string): boolean {
353
+ if (!term) return true
354
+ const haystack = [line.productName, line.sku, line.serialNumber]
355
+ .map((value) => nullableText(value) ?? '')
356
+ .join(' ')
357
+ .toLowerCase()
358
+ return haystack.includes(term)
359
+ }
360
+
361
+ type OrderLinesFetch =
362
+ | { status: 'ok'; lines: SalesOrderLine[]; truncated: boolean }
363
+ | { status: 'forbidden' }
364
+ | { status: 'error' }
365
+
366
+ // Pages through the whole order so a 250-line order is fully offered. A page that
367
+ // comes back short means the order is exhausted; anything else stops at the hard cap
368
+ // and reports back that the list is partial.
369
+ async function loadAllOrderProductLines(orderId: string): Promise<OrderLinesFetch> {
370
+ const encodedOrderId = encodeURIComponent(orderId)
371
+ const collected: SalesOrderLine[] = []
372
+ for (let page = 1; ; page += 1) {
373
+ const call = await apiCall<{ items?: unknown[] }>(
374
+ `/api/sales/order-lines?orderId=${encodedOrderId}&page=${page}&pageSize=${ORDER_LINE_PAGE_SIZE}`,
375
+ )
376
+ if (call.status === 403) return { status: 'forbidden' }
377
+ if (!call.ok) {
378
+ return page === 1 ? { status: 'error' } : { status: 'ok', lines: collected, truncated: true }
379
+ }
380
+ const items = Array.isArray(call.result?.items) ? call.result.items : []
381
+ for (const item of items) {
382
+ const line = normalizeSalesOrderLine(item)
383
+ if (line) collected.push(line)
384
+ }
385
+ if (items.length < ORDER_LINE_PAGE_SIZE) return { status: 'ok', lines: collected, truncated: false }
386
+ if (page * ORDER_LINE_PAGE_SIZE >= ORDER_LINE_MAX_FETCH) {
387
+ return { status: 'ok', lines: collected, truncated: true }
388
+ }
389
+ }
390
+ }
391
+
392
+ function createDefaultLine(): ClaimCreateLineValues {
393
+ return {
394
+ productId: null,
395
+ variantId: null,
396
+ orderLineId: null,
397
+ productName: '',
398
+ sku: '',
399
+ serialNumber: '',
400
+ purchaseDate: '',
401
+ warrantyMonths: '',
402
+ faultCode: null,
403
+ faultDescription: '',
404
+ qtyClaimed: 1,
405
+ }
406
+ }
407
+
408
+ function normalizeLineValue(value: unknown): ClaimCreateLineValues {
409
+ if (!isRecord(value)) return createDefaultLine()
410
+ return {
411
+ productId: nullableText(value.productId),
412
+ variantId: nullableText(value.variantId),
413
+ orderLineId: nullableText(value.orderLineId),
414
+ productName: stringifyFieldValue(value.productName),
415
+ sku: stringifyFieldValue(value.sku),
416
+ serialNumber: stringifyFieldValue(value.serialNumber),
417
+ purchaseDate: dateInputValue(value.purchaseDate),
418
+ warrantyMonths: typeof value.warrantyMonths === 'number' || typeof value.warrantyMonths === 'string' ? value.warrantyMonths : '',
419
+ faultCode: nullableText(value.faultCode),
420
+ faultDescription: stringifyFieldValue(value.faultDescription),
421
+ qtyClaimed: typeof value.qtyClaimed === 'number' || typeof value.qtyClaimed === 'string' ? value.qtyClaimed : '',
422
+ }
423
+ }
424
+
425
+ function readLineValues(value: unknown): ClaimCreateLineValues[] {
426
+ if (!Array.isArray(value)) return [createDefaultLine()]
427
+ const rows = value.map(normalizeLineValue)
428
+ return rows.length ? rows : [createDefaultLine()]
429
+ }
430
+
431
+ function readSubmittedLineValues(value: unknown): ClaimCreateLineValues[] {
432
+ if (!Array.isArray(value)) return []
433
+ return value.map(normalizeLineValue)
434
+ }
435
+
436
+ function lineHasContentBeyondQuantity(line: ClaimCreateLineValues): boolean {
437
+ return Boolean(
438
+ nullableText(line.productId) ||
439
+ nullableText(line.variantId) ||
440
+ nullableText(line.orderLineId) ||
441
+ nullableText(line.productName) ||
442
+ nullableText(line.sku) ||
443
+ nullableText(line.serialNumber) ||
444
+ normalizeDateOnly(line.purchaseDate) ||
445
+ parseWarrantyMonths(line.warrantyMonths) !== null ||
446
+ nullableText(line.faultCode) ||
447
+ nullableText(line.faultDescription),
448
+ )
449
+ }
450
+
451
+ function lineHasContent(line: ClaimCreateLineValues): boolean {
452
+ return Boolean(
453
+ nullableText(line.productId) ||
454
+ nullableText(line.variantId) ||
455
+ nullableText(line.orderLineId) ||
456
+ nullableText(line.productName) ||
457
+ nullableText(line.sku) ||
458
+ nullableText(line.serialNumber) ||
459
+ normalizeDateOnly(line.purchaseDate) ||
460
+ parseWarrantyMonths(line.warrantyMonths) !== null ||
461
+ nullableText(line.faultCode) ||
462
+ nullableText(line.faultDescription) ||
463
+ parsePositiveNumber(line.qtyClaimed) !== null,
464
+ )
465
+ }
466
+
467
+ function normalizeLinePayload(line: ClaimCreateLineValues): Record<string, unknown> {
468
+ const payload: Record<string, unknown> = {
469
+ qtyClaimed: parsePositiveNumber(line.qtyClaimed) ?? 1,
470
+ }
471
+ assignStringIfSet(payload, 'productId', line.productId)
472
+ assignStringIfSet(payload, 'variantId', line.variantId)
473
+ assignStringIfSet(payload, 'orderLineId', line.orderLineId)
474
+ assignStringIfSet(payload, 'productName', line.productName)
475
+ assignStringIfSet(payload, 'sku', line.sku)
476
+ assignStringIfSet(payload, 'serialNumber', line.serialNumber)
477
+ assignStringIfSet(payload, 'faultCode', line.faultCode)
478
+ assignStringIfSet(payload, 'faultDescription', line.faultDescription)
479
+ const purchaseDate = normalizeDateOnly(line.purchaseDate)
480
+ if (purchaseDate) payload.purchaseDate = purchaseDate
481
+ const warrantyMonths = parseWarrantyMonths(line.warrantyMonths)
482
+ if (warrantyMonths !== null) payload.warrantyMonths = warrantyMonths
483
+ return payload
484
+ }
485
+
486
+ function CustomerSelectionObserver({
487
+ value,
488
+ onChange,
489
+ }: {
490
+ value: unknown
491
+ onChange: (customerId: string | null) => void
492
+ }) {
493
+ const customerId = nullableText(value)
494
+ React.useEffect(() => {
495
+ onChange(customerId)
496
+ }, [customerId, onChange])
497
+ return null
498
+ }
499
+
500
+ function ClaimTypeSelectionObserver({
501
+ value,
502
+ onChange,
503
+ }: {
504
+ value: unknown
505
+ onChange: React.Dispatch<React.SetStateAction<string | null>>
506
+ }) {
507
+ const claimType = nullableText(value) ?? 'warranty'
508
+ React.useEffect(() => {
509
+ onChange((current) => (current === claimType ? current : claimType))
510
+ }, [claimType, onChange])
511
+ return null
512
+ }
513
+
514
+ function ReasonCodeSelectionObserver({
515
+ value,
516
+ onChange,
517
+ }: {
518
+ value: unknown
519
+ onChange: React.Dispatch<React.SetStateAction<string | null>>
520
+ }) {
521
+ const reasonCode = nullableText(value)
522
+ React.useEffect(() => {
523
+ onChange((current) => (current === reasonCode ? current : reasonCode))
524
+ }, [reasonCode, onChange])
525
+ return null
526
+ }
527
+
528
+ function GuidedTroubleshootingSection({
529
+ claimType,
530
+ reasonCode,
531
+ onResolve,
532
+ t,
533
+ }: {
534
+ claimType: string | null
535
+ reasonCode: string | null
536
+ onResolve: (result: { resolution?: string; reasonCode?: string }) => void
537
+ t: TranslateFn
538
+ }) {
539
+ const [guide, setGuide] = React.useState<TroubleshootingWalkerGuide | null>(null)
540
+ const [loading, setLoading] = React.useState(false)
541
+
542
+ React.useEffect(() => {
543
+ if (!claimType) {
544
+ setGuide(null)
545
+ setLoading(false)
546
+ return
547
+ }
548
+ let cancelled = false
549
+ const timer = window.setTimeout(() => {
550
+ setLoading(true)
551
+ void loadStaffTroubleshootingGuide(claimType, reasonCode)
552
+ .then((nextGuide) => {
553
+ if (!cancelled) setGuide(nextGuide)
554
+ })
555
+ .catch(() => {
556
+ if (!cancelled) setGuide(null)
557
+ })
558
+ .finally(() => {
559
+ if (!cancelled) setLoading(false)
560
+ })
561
+ }, 300)
562
+ return () => {
563
+ cancelled = true
564
+ window.clearTimeout(timer)
565
+ }
566
+ }, [claimType, reasonCode])
567
+
568
+ if (!guide && !loading) return null
569
+
570
+ return (
571
+ <CollapsibleSection
572
+ title={t('warranty_claims.form.troubleshooting.title', 'Guided troubleshooting')}
573
+ defaultCollapsed
574
+ contentClassName="rounded-lg border border-border bg-muted/30 p-4"
575
+ >
576
+ {loading && !guide ? (
577
+ <span className="inline-flex items-center gap-2 text-sm text-muted-foreground">
578
+ <Spinner size="sm" />
579
+ {t('warranty_claims.form.troubleshooting.loading', 'Loading guide...')}
580
+ </span>
581
+ ) : (
582
+ <TroubleshootingWalker guide={guide} onResolve={onResolve} />
583
+ )}
584
+ </CollapsibleSection>
585
+ )
586
+ }
587
+
588
+ export function LineItemsEditor({
589
+ value,
590
+ setLines,
591
+ orderId,
592
+ defaultWarrantyMonths,
593
+ error,
594
+ faultCodeOptions,
595
+ translations,
596
+ t,
597
+ }: {
598
+ value: unknown
599
+ setLines: (lines: ClaimCreateLineValues[]) => void
600
+ orderId: string | null
601
+ defaultWarrantyMonths: number | null
602
+ error?: string
603
+ faultCodeOptions: CrudFieldOption[]
604
+ translations: LineEditorTranslations
605
+ t: TranslateFn
606
+ }) {
607
+ const lines = readLineValues(value)
608
+ const noValue = t('warranty_claims.common.noValue')
609
+ const [orderDialogOpen, setOrderDialogOpen] = React.useState(false)
610
+ const [orderLines, setOrderLines] = React.useState<SalesOrderLine[]>([])
611
+ const [selectedOrderLineIds, setSelectedOrderLineIds] = React.useState<Set<string>>(new Set())
612
+ const [orderPurchaseDate, setOrderPurchaseDate] = React.useState<string | null>(null)
613
+ const [orderLinesLoading, setOrderLinesLoading] = React.useState(false)
614
+ const [orderLinesTruncated, setOrderLinesTruncated] = React.useState(false)
615
+ const [orderLineSearch, setOrderLineSearch] = React.useState('')
616
+ const [hideOrderImport, setHideOrderImport] = React.useState(false)
617
+ const [lineSearch, setLineSearch] = React.useState('')
618
+ const [linePage, setLinePage] = React.useState(1)
619
+ const [lineDialog, setLineDialog] = React.useState<LineDialogState | null>(null)
620
+ const [lineDraft, setLineDraft] = React.useState<ClaimCreateLineValues>(createDefaultLine)
621
+ const editorRef = React.useRef<HTMLDivElement | null>(null)
622
+
623
+ React.useEffect(() => {
624
+ if (defaultWarrantyMonths === null) return
625
+ let changed = false
626
+ const next = lines.map((line) => {
627
+ if (!normalizeDateOnly(line.purchaseDate) || !isWarrantyMonthsEmpty(line.warrantyMonths)) return line
628
+ changed = true
629
+ return { ...line, warrantyMonths: defaultWarrantyMonths }
630
+ })
631
+ if (changed) setLines(next)
632
+ }, [defaultWarrantyMonths, lines, setLines])
633
+
634
+ // Search and paging run over the whole array but only a page's worth of rows ever
635
+ // reaches the table, so an order with hundreds of lines never renders at once.
636
+ const searchTerm = lineSearch.trim().toLowerCase()
637
+ const rows = lines.map((line, index): ClaimLineRow => ({
638
+ ...line,
639
+ id: `claim-line-${index}`,
640
+ index,
641
+ lineNo: index + 1,
642
+ }))
643
+ const filteredRows = searchTerm ? rows.filter((row) => matchesLineSearch(row, searchTerm)) : rows
644
+ const totalPages = Math.max(1, Math.ceil(filteredRows.length / LINE_PAGE_SIZE))
645
+ const currentPage = Math.min(Math.max(1, linePage), totalPages)
646
+ const pageRows = filteredRows.slice((currentPage - 1) * LINE_PAGE_SIZE, currentPage * LINE_PAGE_SIZE)
647
+
648
+ const exceedsLineCap = React.useCallback((currentCount: number, addingCount: number): boolean => {
649
+ if (currentCount + addingCount <= MAX_CLAIM_LINES) return false
650
+ flash(
651
+ t('warranty_claims.form.lines.error.maxLines', 'A claim can hold at most {max} lines. Remove some lines or select fewer.', {
652
+ max: String(MAX_CLAIM_LINES),
653
+ }),
654
+ 'error',
655
+ )
656
+ return true
657
+ }, [t])
658
+
659
+ // Freshly added lines must be visible even when a filter is active, so drop the
660
+ // search term and jump to the page that now holds them.
661
+ const revealLastPage = React.useCallback((total: number) => {
662
+ setLineSearch('')
663
+ setLinePage(Math.max(1, Math.ceil(total / LINE_PAGE_SIZE)))
664
+ requestAnimationFrame(() => {
665
+ editorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
666
+ })
667
+ }, [])
668
+
669
+ const handleSearchChange = React.useCallback((next: string) => {
670
+ setLineSearch(next)
671
+ setLinePage(1)
672
+ }, [])
673
+
674
+ const updateDraft = React.useCallback((patch: Partial<ClaimCreateLineValues>) => {
675
+ setLineDraft((current) => ({ ...current, ...patch }))
676
+ }, [])
677
+
678
+ const updateDraftPurchaseDate = React.useCallback((purchaseDate: string) => {
679
+ setLineDraft((current) => {
680
+ const next: ClaimCreateLineValues = { ...current, purchaseDate }
681
+ if (purchaseDate && defaultWarrantyMonths !== null && isWarrantyMonthsEmpty(current.warrantyMonths)) {
682
+ next.warrantyMonths = defaultWarrantyMonths
683
+ }
684
+ return next
685
+ })
686
+ }, [defaultWarrantyMonths])
687
+
688
+ const openCreateDialog = React.useCallback(() => {
689
+ if (exceedsLineCap(lines.length, 1)) return
690
+ setLineDraft(createDefaultLine())
691
+ setLineDialog({ mode: 'create' })
692
+ }, [exceedsLineCap, lines.length])
693
+
694
+ const openEditDialog = React.useCallback((row: ClaimLineRow) => {
695
+ setLineDraft(lines[row.index] ?? createDefaultLine())
696
+ setLineDialog({ mode: 'edit', index: row.index })
697
+ }, [lines])
698
+
699
+ const submitLineDialog = React.useCallback(() => {
700
+ if (!lineDialog) return
701
+ if (lineDialog.mode === 'edit') {
702
+ setLines(lines.map((line, index) => (index === lineDialog.index ? lineDraft : line)))
703
+ setLineDialog(null)
704
+ return
705
+ }
706
+ const nextLines = [...lines, lineDraft]
707
+ setLines(nextLines)
708
+ setLineDialog(null)
709
+ revealLastPage(nextLines.length)
710
+ }, [lineDialog, lineDraft, lines, revealLastPage, setLines])
711
+
712
+ const removeLine = React.useCallback((index: number) => {
713
+ if (lines.length <= 1) return
714
+ setLines(lines.filter((_, lineIndex) => lineIndex !== index))
715
+ setLinePage((current) => Math.min(current, Math.max(1, Math.ceil((filteredRows.length - 1) / LINE_PAGE_SIZE))))
716
+ }, [filteredRows.length, lines, setLines])
717
+
718
+ const faultCodeLabels = React.useMemo(() => {
719
+ const labels = new Map<string, string>()
720
+ for (const option of faultCodeOptions) labels.set(option.value, option.label)
721
+ return labels
722
+ }, [faultCodeOptions])
723
+
724
+ const columns = React.useMemo<ColumnDef<ClaimLineRow>[]>(() => [
725
+ {
726
+ accessorKey: 'lineNo',
727
+ header: t('warranty_claims.form.lineNo'),
728
+ meta: { maxWidth: '80px' },
729
+ cell: ({ row }) => <span className="font-mono text-xs">{row.original.lineNo}</span>,
730
+ },
731
+ {
732
+ accessorKey: 'productName',
733
+ header: translations.productName,
734
+ meta: { maxWidth: '240px' },
735
+ cell: ({ row }) => (
736
+ <span className="font-medium">
737
+ {nullableText(row.original.productName) ?? nullableText(row.original.sku) ?? noValue}
738
+ </span>
739
+ ),
740
+ },
741
+ {
742
+ accessorKey: 'serialNumber',
743
+ header: translations.serialNumber,
744
+ meta: { maxWidth: '180px' },
745
+ cell: ({ row }) => nullableText(row.original.serialNumber) ?? noValue,
746
+ },
747
+ {
748
+ accessorKey: 'qtyClaimed',
749
+ header: t('warranty_claims.lines.header.qtyClaimed'),
750
+ meta: { maxWidth: '120px' },
751
+ cell: ({ row }) => formatQuantity(parseQuantity(stringifyFieldValue(row.original.qtyClaimed)), noValue),
752
+ },
753
+ {
754
+ accessorKey: 'faultCode',
755
+ header: translations.faultCode,
756
+ meta: { maxWidth: '200px' },
757
+ cell: ({ row }) => {
758
+ const faultCode = nullableText(row.original.faultCode)
759
+ if (!faultCode) return noValue
760
+ return faultCodeLabels.get(faultCode) ?? faultCode
761
+ },
762
+ },
763
+ ], [faultCodeLabels, noValue, t, translations.faultCode, translations.productName, translations.serialNumber])
764
+
765
+ const openOrderImportDialog = React.useCallback(async () => {
766
+ if (!orderId) return
767
+ setOrderDialogOpen(true)
768
+ setOrderLinesLoading(true)
769
+ setOrderLines([])
770
+ setSelectedOrderLineIds(new Set())
771
+ setOrderPurchaseDate(null)
772
+ setOrderLinesTruncated(false)
773
+ setOrderLineSearch('')
774
+ const encodedOrderId = encodeURIComponent(orderId)
775
+ try {
776
+ const [linesFetch, orderCall] = await Promise.all([
777
+ loadAllOrderProductLines(orderId),
778
+ apiCall<{ items?: unknown[] }>(`/api/sales/orders?id=${encodedOrderId}&pageSize=1`),
779
+ ])
780
+ if (linesFetch.status === 'forbidden' || orderCall.status === 403) {
781
+ setHideOrderImport(true)
782
+ setOrderDialogOpen(false)
783
+ return
784
+ }
785
+ if (linesFetch.status === 'error' || !orderCall.ok) {
786
+ flash(t('warranty_claims.form.addFromOrder.error'), 'error')
787
+ setOrderDialogOpen(false)
788
+ return
789
+ }
790
+ const order = Array.isArray(orderCall.result?.items) ? orderCall.result.items[0] : null
791
+ const placedAt = isRecord(order)
792
+ ? toStringOrNull(order.placed_at) ?? toStringOrNull(order.placedAt)
793
+ : null
794
+ setOrderLines(linesFetch.lines)
795
+ setOrderLinesTruncated(linesFetch.truncated)
796
+ setSelectedOrderLineIds(new Set(linesFetch.lines.map((line) => line.id)))
797
+ setOrderPurchaseDate(placedAt ? placedAt.slice(0, 10) : null)
798
+ } finally {
799
+ setOrderLinesLoading(false)
800
+ }
801
+ }, [orderId, t])
802
+
803
+ const insertSelectedOrderLines = React.useCallback(() => {
804
+ const selectedRows = orderLines.filter((line) => selectedOrderLineIds.has(line.id))
805
+ if (!selectedRows.length) {
806
+ setOrderDialogOpen(false)
807
+ return
808
+ }
809
+ const retainedLines = lines.filter(lineHasContentBeyondQuantity)
810
+ if (exceedsLineCap(retainedLines.length, selectedRows.length)) return
811
+ const nextRows = selectedRows.map((line): ClaimCreateLineValues => ({
812
+ ...createDefaultLine(),
813
+ productId: line.productId,
814
+ variantId: line.variantId,
815
+ sku: line.sku ?? '',
816
+ productName: line.name ?? '',
817
+ qtyClaimed: parseQuantity(line.quantity) ?? 1,
818
+ orderLineId: line.id,
819
+ purchaseDate: orderPurchaseDate ?? '',
820
+ warrantyMonths: orderPurchaseDate && defaultWarrantyMonths !== null ? defaultWarrantyMonths : '',
821
+ }))
822
+ const nextLines = [...retainedLines, ...nextRows]
823
+ setLines(nextLines)
824
+ setOrderDialogOpen(false)
825
+ flash(t('warranty_claims.form.addFromOrder.added', '{count} line(s) added from order', { count: String(nextRows.length) }), 'success')
826
+ revealLastPage(nextLines.length)
827
+ }, [defaultWarrantyMonths, exceedsLineCap, lines, orderLines, orderPurchaseDate, revealLastPage, selectedOrderLineIds, setLines, t])
828
+
829
+ const visibleOrderLines = React.useMemo(() => {
830
+ const term = orderLineSearch.trim().toLowerCase()
831
+ if (!term) return orderLines
832
+ return orderLines.filter((line) => `${line.name ?? ''} ${line.sku ?? ''}`.toLowerCase().includes(term))
833
+ }, [orderLineSearch, orderLines])
834
+
835
+ // Select-all applies to what the filter is currently showing, so a filtered
836
+ // selection never silently pulls in rows the user cannot see.
837
+ const toggleAllVisibleOrderLines = React.useCallback((selected: boolean) => {
838
+ setSelectedOrderLineIds((current) => {
839
+ const next = new Set(current)
840
+ for (const line of visibleOrderLines) {
841
+ if (selected) next.add(line.id)
842
+ else next.delete(line.id)
843
+ }
844
+ return next
845
+ })
846
+ }, [visibleOrderLines])
847
+
848
+ const toggleOrderLine = React.useCallback((lineId: string, selected: boolean) => {
849
+ setSelectedOrderLineIds((current) => {
850
+ const next = new Set(current)
851
+ if (selected) next.add(lineId)
852
+ else next.delete(lineId)
853
+ return next
854
+ })
855
+ }, [])
856
+
857
+ const draftWarrantyStatus = computeEntitlementPreview(
858
+ dateFromInputValue(lineDraft.purchaseDate),
859
+ parseWarrantyMonths(lineDraft.warrantyMonths),
860
+ )
861
+ const draftDialogId = 'warranty-claim-line-draft'
862
+
863
+ return (
864
+ <div className="space-y-4" ref={editorRef}>
865
+ <DataTable<ClaimLineRow>
866
+ embedded
867
+ columns={columns}
868
+ data={pageRows}
869
+ stickyActionsColumn
870
+ searchValue={lineSearch}
871
+ onSearchChange={handleSearchChange}
872
+ searchPlaceholder={t('warranty_claims.form.lines.search', 'Search product, SKU or serial number')}
873
+ pagination={{
874
+ page: currentPage,
875
+ pageSize: LINE_PAGE_SIZE,
876
+ total: filteredRows.length,
877
+ totalPages,
878
+ onPageChange: setLinePage,
879
+ }}
880
+ actions={(
881
+ <div className="flex flex-wrap items-center gap-2">
882
+ {orderId && !hideOrderImport ? (
883
+ <Button type="button" variant="outline" onClick={() => { void openOrderImportDialog() }}>
884
+ <Plus className="size-4" aria-hidden="true" />
885
+ {t('warranty_claims.form.addFromOrder')}
886
+ </Button>
887
+ ) : null}
888
+ <Button type="button" onClick={openCreateDialog}>
889
+ <Plus className="size-4" aria-hidden="true" />
890
+ {translations.addLine}
891
+ </Button>
892
+ </div>
893
+ )}
894
+ emptyState={(
895
+ <EmptyState
896
+ title={searchTerm
897
+ ? t('warranty_claims.form.lines.empty.noMatches', 'No claim lines match your search')
898
+ : t('warranty_claims.form.lines.empty.title', 'No claim lines yet')}
899
+ description={searchTerm
900
+ ? undefined
901
+ : t('warranty_claims.form.lines.empty.description', 'Add a claim line or import them from the linked order.')}
902
+ variant="subtle"
903
+ />
904
+ )}
905
+ rowActions={(row) => {
906
+ const items: RowActionItem[] = [
907
+ {
908
+ id: 'edit',
909
+ label: t('warranty_claims.form.lines.edit', 'Edit line'),
910
+ onSelect: () => openEditDialog(row),
911
+ },
912
+ ]
913
+ if (lines.length > 1) {
914
+ items.push({
915
+ id: 'delete',
916
+ label: translations.removeLine,
917
+ destructive: true,
918
+ onSelect: () => removeLine(row.index),
919
+ })
920
+ }
921
+ return <RowActions items={items} />
922
+ }}
923
+ />
924
+ {error ? <p className="text-sm text-status-error-text">{error}</p> : null}
925
+ <Dialog open={lineDialog !== null} onOpenChange={(open) => { if (!open) setLineDialog(null) }}>
926
+ <DialogContent
927
+ className="sm:max-w-3xl"
928
+ onKeyDown={(event) => {
929
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
930
+ event.preventDefault()
931
+ submitLineDialog()
932
+ }
933
+ if (event.key === 'Escape') {
934
+ setLineDialog(null)
935
+ }
936
+ }}
937
+ >
938
+ <DialogHeader>
939
+ <DialogTitle>
940
+ {lineDialog?.mode === 'edit'
941
+ ? t('warranty_claims.form.lines.edit', 'Edit line')
942
+ : translations.addLine}
943
+ </DialogTitle>
944
+ </DialogHeader>
945
+ <div className="grid gap-4 md:grid-cols-2">
946
+ <div className="md:col-span-2">
947
+ <ClaimLineProductPicker
948
+ value={{
949
+ productId: lineDraft.productId,
950
+ productName: lineDraft.productName,
951
+ sku: lineDraft.sku,
952
+ variantId: lineDraft.variantId,
953
+ }}
954
+ onPick={(pick: ClaimProductPick) => updateDraft({
955
+ productId: pick.productId,
956
+ variantId: pick.variantId,
957
+ sku: pick.sku,
958
+ productName: pick.productName,
959
+ })}
960
+ onClear={() => updateDraft({ productId: null, variantId: null })}
961
+ />
962
+ </div>
963
+ <div className="space-y-2">
964
+ <Label htmlFor={`${draftDialogId}-productName`}>{translations.productName}</Label>
965
+ <Input
966
+ id={`${draftDialogId}-productName`}
967
+ value={stringifyFieldValue(lineDraft.productName)}
968
+ onChange={(event) => updateDraft({ productName: event.target.value })}
969
+ />
970
+ </div>
971
+ <div className="space-y-2">
972
+ <Label htmlFor={`${draftDialogId}-sku`}>{translations.sku}</Label>
973
+ <Input
974
+ id={`${draftDialogId}-sku`}
975
+ value={stringifyFieldValue(lineDraft.sku)}
976
+ onChange={(event) => updateDraft({ sku: event.target.value })}
977
+ />
978
+ </div>
979
+ <div className="space-y-2">
980
+ <Label htmlFor={`${draftDialogId}-serialNumber`}>{translations.serialNumber}</Label>
981
+ <Input
982
+ id={`${draftDialogId}-serialNumber`}
983
+ value={stringifyFieldValue(lineDraft.serialNumber)}
984
+ onChange={(event) => updateDraft({ serialNumber: event.target.value })}
985
+ />
986
+ </div>
987
+ <div className="space-y-2">
988
+ <Label htmlFor={`${draftDialogId}-qtyClaimed`}>{translations.qtyClaimed}</Label>
989
+ <Input
990
+ id={`${draftDialogId}-qtyClaimed`}
991
+ type="number"
992
+ min={1}
993
+ step="1"
994
+ value={stringifyFieldValue(lineDraft.qtyClaimed)}
995
+ onChange={(event) => updateDraft({ qtyClaimed: event.target.value })}
996
+ />
997
+ </div>
998
+ <div className="space-y-2">
999
+ <Label htmlFor={`${draftDialogId}-purchaseDate`}>{t('warranty_claims.form.purchaseDate')}</Label>
1000
+ <Input
1001
+ id={`${draftDialogId}-purchaseDate`}
1002
+ type="date"
1003
+ value={dateInputValue(lineDraft.purchaseDate)}
1004
+ onChange={(event) => updateDraftPurchaseDate(event.target.value)}
1005
+ />
1006
+ </div>
1007
+ <div className="space-y-2">
1008
+ <Label htmlFor={`${draftDialogId}-warrantyMonths`}>{t('warranty_claims.form.warrantyMonths')}</Label>
1009
+ <div className="flex flex-wrap items-center gap-2">
1010
+ <Input
1011
+ id={`${draftDialogId}-warrantyMonths`}
1012
+ type="number"
1013
+ min={0}
1014
+ step="1"
1015
+ value={stringifyFieldValue(lineDraft.warrantyMonths)}
1016
+ onChange={(event) => updateDraft({ warrantyMonths: event.target.value })}
1017
+ className="min-w-28 flex-1"
1018
+ />
1019
+ <EntitlementChip status={draftWarrantyStatus} t={t} />
1020
+ <EntitlementLookupBadge
1021
+ claim={{ orderId }}
1022
+ lines={[{
1023
+ productId: nullableText(lineDraft.productId),
1024
+ sku: nullableText(lineDraft.sku),
1025
+ serialNumber: nullableText(lineDraft.serialNumber),
1026
+ purchaseDate: normalizeDateOnly(lineDraft.purchaseDate),
1027
+ }]}
1028
+ />
1029
+ </div>
1030
+ </div>
1031
+ <div className="space-y-2 md:col-span-2">
1032
+ <Label htmlFor={`${draftDialogId}-faultCode`}>{translations.faultCode}</Label>
1033
+ <Select
1034
+ value={nullableText(lineDraft.faultCode) ?? ''}
1035
+ onValueChange={(next) => updateDraft({ faultCode: next })}
1036
+ >
1037
+ <SelectTrigger id={`${draftDialogId}-faultCode`}>
1038
+ <SelectValue placeholder={translations.faultCodePlaceholder} />
1039
+ </SelectTrigger>
1040
+ <SelectContent>
1041
+ {faultCodeOptions.map((option) => (
1042
+ <SelectItem key={option.value} value={option.value}>
1043
+ {option.label}
1044
+ </SelectItem>
1045
+ ))}
1046
+ </SelectContent>
1047
+ </Select>
1048
+ </div>
1049
+ <div className="space-y-2 md:col-span-2">
1050
+ <Label htmlFor={`${draftDialogId}-faultDescription`}>{translations.faultDescription}</Label>
1051
+ <Textarea
1052
+ id={`${draftDialogId}-faultDescription`}
1053
+ rows={4}
1054
+ value={stringifyFieldValue(lineDraft.faultDescription)}
1055
+ onChange={(event) => updateDraft({ faultDescription: event.target.value })}
1056
+ />
1057
+ </div>
1058
+ </div>
1059
+ <DialogFooter>
1060
+ <Button type="button" variant="outline" onClick={() => setLineDialog(null)}>
1061
+ {t('common.cancel', 'Cancel')}
1062
+ </Button>
1063
+ <Button type="button" onClick={submitLineDialog}>
1064
+ {t('common.save', 'Save')}
1065
+ </Button>
1066
+ </DialogFooter>
1067
+ </DialogContent>
1068
+ </Dialog>
1069
+ <Dialog open={orderDialogOpen} onOpenChange={setOrderDialogOpen}>
1070
+ <DialogContent
1071
+ className="sm:max-w-2xl"
1072
+ onKeyDown={(event) => {
1073
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
1074
+ event.preventDefault()
1075
+ if (selectedOrderLineIds.size) insertSelectedOrderLines()
1076
+ }
1077
+ if (event.key === 'Escape') {
1078
+ setOrderDialogOpen(false)
1079
+ }
1080
+ }}
1081
+ >
1082
+ <DialogHeader>
1083
+ <DialogTitle>{t('warranty_claims.form.addFromOrder')}</DialogTitle>
1084
+ </DialogHeader>
1085
+ <div className="space-y-3">
1086
+ {orderLinesLoading ? (
1087
+ <p className="text-sm text-muted-foreground">{t('warranty_claims.form.addFromOrder.loading')}</p>
1088
+ ) : null}
1089
+ {!orderLinesLoading && orderLines.length === 0 ? (
1090
+ <p className="text-sm text-muted-foreground">{t('warranty_claims.form.addFromOrder.empty')}</p>
1091
+ ) : null}
1092
+ {!orderLinesLoading && orderLines.length ? (
1093
+ <div className="space-y-2">
1094
+ <Input
1095
+ type="search"
1096
+ value={orderLineSearch}
1097
+ onChange={(event) => setOrderLineSearch(event.target.value)}
1098
+ placeholder={t('warranty_claims.form.addFromOrder.search', 'Filter by product or SKU')}
1099
+ aria-label={t('warranty_claims.form.addFromOrder.search', 'Filter by product or SKU')}
1100
+ />
1101
+ <div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
1102
+ <span>
1103
+ {t('warranty_claims.form.addFromOrder.count', '{shown} of {total} lines', {
1104
+ shown: String(visibleOrderLines.length),
1105
+ total: String(orderLines.length),
1106
+ })}
1107
+ {orderLinesTruncated
1108
+ ? ` — ${t('warranty_claims.form.addFromOrder.truncated', 'first {limit} only', { limit: String(ORDER_LINE_MAX_FETCH) })}`
1109
+ : ''}
1110
+ {/* Selection survives filter changes, so it must be visible: otherwise
1111
+ rows picked under an earlier filter silently push the claim over
1112
+ the line cap and the insert appears to do nothing. */}
1113
+ {selectedOrderLineIds.size
1114
+ ? ` — ${t('warranty_claims.form.addFromOrder.selected', '{count} selected', { count: String(selectedOrderLineIds.size) })}`
1115
+ : ''}
1116
+ </span>
1117
+ <span className="flex gap-3">
1118
+ <Button
1119
+ type="button"
1120
+ variant="link"
1121
+ size="sm"
1122
+ onClick={() => toggleAllVisibleOrderLines(true)}
1123
+ >
1124
+ {t('warranty_claims.form.addFromOrder.selectAll', 'Select all')}
1125
+ </Button>
1126
+ <Button
1127
+ type="button"
1128
+ variant="link"
1129
+ size="sm"
1130
+ onClick={() => setSelectedOrderLineIds(new Set())}
1131
+ >
1132
+ {t('warranty_claims.form.addFromOrder.clear', 'Clear')}
1133
+ </Button>
1134
+ </span>
1135
+ </div>
1136
+ </div>
1137
+ ) : null}
1138
+ <div className="max-h-[50vh] space-y-3 overflow-y-auto pr-1">
1139
+ {visibleOrderLines.map((line) => {
1140
+ const checked = selectedOrderLineIds.has(line.id)
1141
+ const label = line.name ?? line.sku ?? t('warranty_claims.form.lines.unnamed', 'Unnamed line')
1142
+ return (
1143
+ <div key={line.id} className="flex items-start gap-3 rounded-md border border-border p-3">
1144
+ <Checkbox
1145
+ checked={checked}
1146
+ onCheckedChange={(next) => toggleOrderLine(line.id, next === true)}
1147
+ aria-label={t('warranty_claims.form.addFromOrder.selectLine', undefined, { name: label })}
1148
+ />
1149
+ <div className="min-w-0 flex-1 space-y-1">
1150
+ <div className="text-sm font-medium">{label}</div>
1151
+ <div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
1152
+ <span>{line.sku ?? t('warranty_claims.common.noValue')}</span>
1153
+ <span>{t('warranty_claims.form.addFromOrder.quantity', undefined, { quantity: formatQuantity(line.quantity, t('warranty_claims.common.noValue')) })}</span>
1154
+ </div>
1155
+ </div>
1156
+ </div>
1157
+ )
1158
+ })}
1159
+ </div>
1160
+ </div>
1161
+ <DialogFooter>
1162
+ <Button type="button" variant="outline" onClick={() => setOrderDialogOpen(false)}>
1163
+ {t('common.cancel', 'Cancel')}
1164
+ </Button>
1165
+ <Button type="button" onClick={insertSelectedOrderLines} disabled={!selectedOrderLineIds.size}>
1166
+ {t('warranty_claims.form.addFromOrder.confirm', 'Add selected lines')}
1167
+ </Button>
1168
+ </DialogFooter>
1169
+ </DialogContent>
1170
+ </Dialog>
1171
+ </div>
1172
+ )
1173
+ }
1174
+
1175
+ export default function CreateWarrantyClaimPage() {
1176
+ const t = useT()
1177
+ const router = useRouter()
1178
+ const searchParams = useSearchParams()
1179
+ const [selectedCustomerId, setSelectedCustomerId] = React.useState<string | null>(null)
1180
+ const [selectedClaimType, setSelectedClaimType] = React.useState<string | null>(() => normalizeClaimType(searchParams.get('claimType')))
1181
+ const [selectedReasonCode, setSelectedReasonCode] = React.useState<string | null>(null)
1182
+ const [orderAccessDenied, setOrderAccessDenied] = React.useState(false)
1183
+ const [faultCodeOptions, setFaultCodeOptions] = React.useState<CrudFieldOption[]>([])
1184
+ const [defaultWarrantyMonths, setDefaultWarrantyMonths] = React.useState<number | null>(null)
1185
+
1186
+ const loadDictionaryOptions = React.useCallback(async (dictionaryKey: string, kind: DictionaryLabelKind): Promise<CrudFieldOption[]> => {
1187
+ const dictionaries = await readApiResultOrThrow<{ items?: DictionaryListItem[] }>(
1188
+ '/api/dictionaries',
1189
+ undefined,
1190
+ {
1191
+ fallback: { items: [] },
1192
+ errorMessage: t('warranty_claims.form.error.dictionaryLoad'),
1193
+ },
1194
+ )
1195
+ const dictionary = (dictionaries.items ?? []).find((item) => item.key === dictionaryKey)
1196
+ if (!dictionary?.id) return []
1197
+ const entries = await readApiResultOrThrow<DictionaryEntriesResponse>(
1198
+ `/api/dictionaries/${encodeURIComponent(dictionary.id)}/entries`,
1199
+ undefined,
1200
+ {
1201
+ fallback: { items: [] },
1202
+ errorMessage: t('warranty_claims.form.error.dictionaryLoad'),
1203
+ },
1204
+ )
1205
+ return (entries.items ?? [])
1206
+ .map(normalizeDictionaryOption)
1207
+ .filter((option): option is CrudFieldOption => option !== null)
1208
+ .map((option) => ({
1209
+ value: option.value,
1210
+ label: localizeDictionaryLabel(t, kind, option.value, option.label),
1211
+ }))
1212
+ }, [t])
1213
+
1214
+ const loadCustomerOptions = React.useCallback(async (query?: string): Promise<CrudFieldOption[]> => {
1215
+ const params = new URLSearchParams({ page: '1', pageSize: '20' })
1216
+ const trimmed = query?.trim()
1217
+ if (trimmed) params.set('search', trimmed)
1218
+ const [people, companies] = await Promise.all([
1219
+ apiCall<{ items?: unknown[] }>(`/api/customers/people?${params.toString()}`, undefined, { fallback: { items: [] } }),
1220
+ apiCall<{ items?: unknown[] }>(`/api/customers/companies?${params.toString()}`, undefined, { fallback: { items: [] } }),
1221
+ ])
1222
+ const items = [
1223
+ ...(Array.isArray(people.result?.items) ? people.result.items : []),
1224
+ ...(Array.isArray(companies.result?.items) ? companies.result.items : []),
1225
+ ]
1226
+ return items.map((item) => normalizeOption(item, t)).filter((option): option is CrudFieldOption => option !== null)
1227
+ }, [t])
1228
+
1229
+ const loadOrderOptions = React.useCallback(async (query?: string): Promise<CrudFieldOption[]> => {
1230
+ return loadOrderOptionsShared(query, {
1231
+ customerId: selectedCustomerId,
1232
+ onAccessChange: setOrderAccessDenied,
1233
+ fallbackLabel: t('warranty_claims.form.orderUnavailable', 'Order unavailable'),
1234
+ })
1235
+ }, [selectedCustomerId, t])
1236
+
1237
+ React.useEffect(() => {
1238
+ let cancelled = false
1239
+ void loadDictionaryOptions(DICTIONARY_KEYS.faultCodes, 'fault')
1240
+ .then((options) => {
1241
+ if (!cancelled) setFaultCodeOptions(options)
1242
+ })
1243
+ .catch(() => {
1244
+ if (!cancelled) setFaultCodeOptions([])
1245
+ })
1246
+ return () => {
1247
+ cancelled = true
1248
+ }
1249
+ }, [loadDictionaryOptions])
1250
+
1251
+ React.useEffect(() => {
1252
+ let cancelled = false
1253
+ void apiCall<GeneralSettingsResponse>('/api/warranty_claims/settings-general', { headers: { 'x-om-forbidden-redirect': '0' } })
1254
+ .then((response) => {
1255
+ if (cancelled || !response.ok) return
1256
+ setDefaultWarrantyMonths(readDefaultWarrantyMonths(response.result))
1257
+ })
1258
+ .catch(() => {
1259
+ if (!cancelled) setDefaultWarrantyMonths(null)
1260
+ })
1261
+ return () => {
1262
+ cancelled = true
1263
+ }
1264
+ }, [])
1265
+
1266
+ const lineTranslations = React.useMemo<LineEditorTranslations>(() => ({
1267
+ addLine: t('warranty_claims.form.lines.add', 'Add line'),
1268
+ removeLine: t('warranty_claims.form.lines.remove', 'Remove line'),
1269
+ productName: t('warranty_claims.form.productName'),
1270
+ sku: t('warranty_claims.form.sku'),
1271
+ serialNumber: t('warranty_claims.form.serialNumber'),
1272
+ faultCode: t('warranty_claims.form.faultCode'),
1273
+ faultDescription: t('warranty_claims.form.faultDescription'),
1274
+ qtyClaimed: t('warranty_claims.form.qtyClaimed'),
1275
+ faultCodePlaceholder: t('warranty_claims.form.faultCode.placeholder', 'Select fault code'),
1276
+ }), [t])
1277
+
1278
+ // Short selects share a row (three thirds), the two record lookups share the
1279
+ // next (two halves); only the free-text fields span the full width. Ordered so
1280
+ // each row fills the 6-column grid instead of stacking one control per screen row.
1281
+ const fields = React.useMemo<CrudField[]>(() => [
1282
+ {
1283
+ id: 'claimType',
1284
+ label: t('warranty_claims.form.claimType'),
1285
+ type: 'custom',
1286
+ required: true,
1287
+ layout: 'half',
1288
+ component: ({ values, setFormValue }) => (
1289
+ <div className="grid grid-cols-2 gap-1 rounded-lg bg-muted p-1 sm:grid-cols-4">
1290
+ {CLAIM_TYPES.map((claimType) => (
1291
+ <Button
1292
+ key={claimType}
1293
+ type="button"
1294
+ size="sm"
1295
+ variant={values?.claimType === claimType ? 'default' : 'ghost'}
1296
+ onClick={() => setFormValue?.('claimType', claimType)}
1297
+ >
1298
+ {t(`warranty_claims.claimType.${claimType}`)}
1299
+ </Button>
1300
+ ))}
1301
+ </div>
1302
+ ),
1303
+ },
1304
+ {
1305
+ id: 'priority',
1306
+ label: t('warranty_claims.form.priority'),
1307
+ type: 'select',
1308
+ required: true,
1309
+ layout: 'half',
1310
+ options: CLAIM_PRIORITIES.map((priority) => ({
1311
+ value: priority,
1312
+ label: t(`warranty_claims.priority.${priority}`),
1313
+ })),
1314
+ },
1315
+ {
1316
+ id: 'reasonCode',
1317
+ label: t('warranty_claims.form.reasonCode'),
1318
+ type: 'select',
1319
+ layout: 'half',
1320
+ loadOptions: () => loadDictionaryOptions(DICTIONARY_KEYS.claimReasons, 'reason'),
1321
+ },
1322
+ {
1323
+ id: 'customerId',
1324
+ label: t('warranty_claims.form.customerId'),
1325
+ type: 'combobox',
1326
+ layout: 'half',
1327
+ loadOptions: loadCustomerOptions,
1328
+ allowCustomValues: false,
1329
+ placeholder: t('warranty_claims.form.customerId.placeholder'),
1330
+ seedOptions: [],
1331
+ },
1332
+ {
1333
+ id: 'orderId',
1334
+ label: t('warranty_claims.form.orderId'),
1335
+ type: 'combobox',
1336
+ layout: 'half',
1337
+ loadOptions: loadOrderOptions,
1338
+ allowCustomValues: false,
1339
+ placeholder: t('warranty_claims.form.orderId.placeholder'),
1340
+ description: orderAccessDenied ? t('warranty_claims.form.orderId.noAccess') : undefined,
1341
+ seedOptions: [],
1342
+ resolveLabel: (value: string) => resolveOrderLabel(value, t('warranty_claims.form.orderUnavailable', 'Order unavailable')),
1343
+ },
1344
+ {
1345
+ id: 'notes',
1346
+ label: t('warranty_claims.form.notes'),
1347
+ type: 'textarea',
1348
+ rows: 4,
1349
+ layout: 'half',
1350
+ },
1351
+ {
1352
+ id: 'resolutionSummary',
1353
+ label: t('warranty_claims.form.resolutionSummary', 'Resolution summary'),
1354
+ type: 'textarea',
1355
+ rows: 3,
1356
+ layout: 'full',
1357
+ },
1358
+ ], [loadCustomerOptions, loadDictionaryOptions, loadOrderOptions, orderAccessDenied, resolveOrderLabel, t])
1359
+
1360
+ const groups = React.useMemo<CrudFormGroup[]>(() => [
1361
+ {
1362
+ id: 'header',
1363
+ title: t('warranty_claims.create.basics', 'Claim basics'),
1364
+ column: 1,
1365
+ // Render order lives here, not in the field definitions. Keep the three
1366
+ // thirds adjacent and the two halves adjacent so each grid row fills.
1367
+ fields: ['claimType', 'priority', 'customerId', 'orderId', 'reasonCode', 'notes', 'resolutionSummary'],
1368
+ component: ({ values }) => (
1369
+ <>
1370
+ <CustomerSelectionObserver value={values.customerId} onChange={setSelectedCustomerId} />
1371
+ <ClaimTypeSelectionObserver value={values.claimType} onChange={setSelectedClaimType} />
1372
+ <ReasonCodeSelectionObserver value={values.reasonCode} onChange={setSelectedReasonCode} />
1373
+ </>
1374
+ ),
1375
+ },
1376
+ {
1377
+ id: 'troubleshooting',
1378
+ title: t('warranty_claims.form.troubleshooting.title', 'Guided troubleshooting'),
1379
+ column: 1,
1380
+ bare: true,
1381
+ component: ({ setValue }) => (
1382
+ <GuidedTroubleshootingSection
1383
+ claimType={selectedClaimType}
1384
+ reasonCode={selectedReasonCode}
1385
+ t={t}
1386
+ onResolve={(result) => {
1387
+ if (result.reasonCode) setValue('reasonCode', result.reasonCode)
1388
+ if (result.resolution) setValue('resolutionSummary', result.resolution)
1389
+ }}
1390
+ />
1391
+ ),
1392
+ },
1393
+ {
1394
+ id: 'line',
1395
+ title: t(resolveClaimTypeUiConfig(selectedClaimType).lineHeaderKey),
1396
+ column: 1,
1397
+ component: ({ values, setValue, errors }) => (
1398
+ <LineItemsEditor
1399
+ value={values.lines}
1400
+ setLines={(lines) => setValue('lines', lines)}
1401
+ orderId={nullableText(values.orderId)}
1402
+ defaultWarrantyMonths={defaultWarrantyMonths}
1403
+ error={errors.lines}
1404
+ faultCodeOptions={faultCodeOptions}
1405
+ translations={lineTranslations}
1406
+ t={t}
1407
+ />
1408
+ ),
1409
+ },
1410
+ {
1411
+ id: 'summary',
1412
+ column: 2,
1413
+ bare: true,
1414
+ component: ({ values }) => {
1415
+ const claimType = nullableText(values.claimType) ?? 'warranty'
1416
+ const lineCount = readSubmittedLineValues(values.lines).filter(lineHasContent).length
1417
+ return (
1418
+ <div className="sticky top-4 space-y-4">
1419
+ <aside className="overflow-hidden rounded-xl bg-foreground text-background shadow-sm">
1420
+ <div className="border-b border-background/20 p-5">
1421
+ <div className="flex items-center gap-2">
1422
+ <ShieldCheck className="size-5" aria-hidden />
1423
+ <h2 className="text-base font-semibold">{t('warranty_claims.create.summary.title', 'Claim summary')}</h2>
1424
+ </div>
1425
+ <p className="mt-2 text-sm text-background/70">
1426
+ {t('warranty_claims.create.summary.description', 'Review the intake context before creating the claim.')}
1427
+ </p>
1428
+ </div>
1429
+ <dl className="space-y-4 p-5 text-sm">
1430
+ <div className="flex items-center justify-between gap-3">
1431
+ <dt className="text-background/70">{t('warranty_claims.form.claimType')}</dt>
1432
+ <dd className="font-medium">{t(`warranty_claims.claimType.${claimType}`)}</dd>
1433
+ </div>
1434
+ <div className="flex items-center justify-between gap-3">
1435
+ <dt className="text-background/70">{t('warranty_claims.form.priority')}</dt>
1436
+ <dd className="font-medium">{t(`warranty_claims.priority.${nullableText(values.priority) ?? 'normal'}`)}</dd>
1437
+ </div>
1438
+ <div className="flex items-center justify-between gap-3">
1439
+ <dt className="text-background/70">{t('warranty_claims.create.summary.lines', 'Claim lines')}</dt>
1440
+ <dd className="font-medium tabular-nums">{lineCount}</dd>
1441
+ </div>
1442
+ <div className="flex items-center justify-between gap-3">
1443
+ <dt className="text-background/70">{t('warranty_claims.create.summary.customer', 'Customer')}</dt>
1444
+ <dd className="font-medium">
1445
+ {nullableText(values.customerId)
1446
+ ? t('warranty_claims.create.summary.selected', 'Selected')
1447
+ : t('warranty_claims.create.summary.notSelected', 'Not selected')}
1448
+ </dd>
1449
+ </div>
1450
+ <div className="flex items-center justify-between gap-3">
1451
+ <dt className="text-background/70">{t('warranty_claims.list.column.order')}</dt>
1452
+ <dd className="font-medium">
1453
+ {nullableText(values.orderId)
1454
+ ? t('warranty_claims.create.summary.linked', 'Linked')
1455
+ : t('warranty_claims.create.summary.notLinked', 'Not linked')}
1456
+ </dd>
1457
+ </div>
1458
+ </dl>
1459
+ </aside>
1460
+ <div className="rounded-xl border border-border bg-card p-5">
1461
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1462
+ {t('warranty_claims.create.summary.automation', 'Automation')}
1463
+ </p>
1464
+ <ul className="mt-3 space-y-3 text-sm text-foreground">
1465
+ <li className="flex gap-2">
1466
+ <CheckCircle2 className="mt-0.5 size-4 shrink-0" aria-hidden />
1467
+ {t('warranty_claims.create.summary.entitlement', 'Warranty entitlement is checked from the selected product and serial.')}
1468
+ </li>
1469
+ <li className="flex gap-2">
1470
+ <CheckCircle2 className="mt-0.5 size-4 shrink-0" aria-hidden />
1471
+ {t('warranty_claims.create.summary.sla', 'The SLA clock starts when the claim is submitted.')}
1472
+ </li>
1473
+ </ul>
1474
+ </div>
1475
+ </div>
1476
+ )
1477
+ },
1478
+ },
1479
+ ], [defaultWarrantyMonths, faultCodeOptions, lineTranslations, selectedClaimType, selectedReasonCode, t])
1480
+
1481
+ const initialValues = React.useMemo<Partial<ClaimCreateFormValues>>(() => ({
1482
+ claimType: normalizeClaimType(searchParams.get('claimType')),
1483
+ orderId: searchParams.get('orderId') ?? '',
1484
+ priority: 'normal',
1485
+ lines: [createDefaultLine()],
1486
+ }), [searchParams])
1487
+
1488
+ return (
1489
+ <Page>
1490
+ <PageBody>
1491
+ <div className="rounded-xl border border-border bg-card p-5">
1492
+ <CrudForm<ClaimCreateFormValues>
1493
+ title={t('warranty_claims.create.title')}
1494
+ backHref="/backend/warranty_claims"
1495
+ fields={fields}
1496
+ groups={groups}
1497
+ initialValues={initialValues}
1498
+ submitLabel={t('warranty_claims.create.submit', 'Create claim')}
1499
+ cancelHref="/backend/warranty_claims"
1500
+ onSubmit={async (values) => {
1501
+ const lines = readSubmittedLineValues(values.lines).filter(lineHasContent)
1502
+ if (!lines.length) {
1503
+ const message = t('warranty_claims.form.lines.error.required', 'Add at least one claim line.')
1504
+ throw createCrudFormError(message, { lines: message })
1505
+ }
1506
+ if (lines.some((line) => parsePositiveNumber(line.qtyClaimed) === null)) {
1507
+ const message = t('warranty_claims.form.lines.error.qtyPositive', 'Claimed quantity must be greater than zero.')
1508
+ throw createCrudFormError(message, { lines: message })
1509
+ }
1510
+ const payload: Record<string, unknown> = {
1511
+ claimType: values.claimType,
1512
+ channel: 'staff',
1513
+ priority: values.priority || 'normal',
1514
+ customerId: nullableText(values.customerId),
1515
+ orderId: nullableText(values.orderId),
1516
+ reasonCode: nullableText(values.reasonCode),
1517
+ resolutionSummary: nullableText(values.resolutionSummary),
1518
+ notes: nullableText(values.notes),
1519
+ lines: lines.map(normalizeLinePayload),
1520
+ }
1521
+ const { result } = await createCrud<{ id?: string | null }>('warranty_claims', payload, {
1522
+ errorMessage: t('warranty_claims.create.error'),
1523
+ })
1524
+ const id = result?.id ?? null
1525
+ flash(t('warranty_claims.create.success'), 'success')
1526
+ router.push(id ? `/backend/warranty_claims/${id}` : '/backend/warranty_claims')
1527
+ }}
1528
+ />
1529
+ </div>
1530
+ </PageBody>
1531
+ </Page>
1532
+ )
1533
+ }