@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,1903 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { ChevronDown, ChevronRight, Plus, RefreshCw, Save, Trash2 } from 'lucide-react'
5
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
7
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
8
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
9
+ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
10
+ import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
12
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
13
+ import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
14
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
15
+ import { Button } from '@open-mercato/ui/primitives/button'
16
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
17
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
18
+ import { Input } from '@open-mercato/ui/primitives/input'
19
+ import { Label } from '@open-mercato/ui/primitives/label'
20
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'
21
+ import { Switch } from '@open-mercato/ui/primitives/switch'
22
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
23
+ import { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'
24
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
25
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
26
+ import { ICON_SUGGESTIONS } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
27
+ import {
28
+ DictionaryForm,
29
+ type DictionaryFormValues,
30
+ } from '@open-mercato/core/modules/dictionaries/components/DictionaryForm'
31
+ import {
32
+ DictionaryTable,
33
+ type DictionaryTableEntry,
34
+ } from '@open-mercato/core/modules/dictionaries/components/DictionaryTable'
35
+ import {
36
+ fetchAssignableStaffMembersPage,
37
+ type AssignableStaffMember,
38
+ } from '@open-mercato/core/modules/customers/lib/assignableStaff'
39
+ import { localizeDictionaryLabel, type DictionaryLabelKind } from '../../../lib/dictionaryLabels'
40
+ import { parseEscalationTiers, type EscalationTier } from '../../../lib/escalation'
41
+ import type { BusinessWeekday } from '../../../lib/businessHours'
42
+ import {
43
+ buildBusinessHoursFormValue,
44
+ createBusinessHoursIntervalRow,
45
+ nextBusinessHoursRowKey,
46
+ serializeBusinessHoursRecord,
47
+ stringifyJsonValue,
48
+ validateBusinessHoursValue,
49
+ type BusinessHoursFormValue,
50
+ type BusinessHoursIntervalRow,
51
+ } from '../../../lib/businessHoursForm'
52
+
53
+ type WarrantyDictionaryKind =
54
+ | 'warranty-claim-fault-code'
55
+ | 'warranty-claim-reason'
56
+ | 'warranty-claim-rejection-reason'
57
+
58
+ type SectionDefinition = {
59
+ kind: WarrantyDictionaryKind
60
+ dictionaryKey: string
61
+ titleKey: string
62
+ descriptionKey: string
63
+ }
64
+
65
+ type DialogState =
66
+ | { mode: 'create'; kind: WarrantyDictionaryKind }
67
+ | { mode: 'edit'; kind: WarrantyDictionaryKind; entry: DictionaryTableEntry }
68
+
69
+ type DictionaryListItem = {
70
+ id?: string
71
+ key?: string
72
+ }
73
+
74
+ type EscalationTierRow = {
75
+ key: string
76
+ atPct: string
77
+ action: EscalationTier['action']
78
+ toUserId: string
79
+ }
80
+
81
+ type GeneralSettingsResult = {
82
+ slaHours: number
83
+ slaPauseOnInfoRequested: boolean
84
+ slaAtRiskThresholdPct: number
85
+ autoApproveEnabled: boolean
86
+ autoApproveMaxAmount: number | null
87
+ autoApproveCurrencyCode: string | null
88
+ autoApproveRequireInWarranty: boolean
89
+ defaultWarrantyMonths: number | null
90
+ returnWindowDays: number | null
91
+ businessHours: Record<string, unknown> | null
92
+ escalationTiers: unknown[] | null
93
+ adjudicationUseRules: boolean
94
+ quarantineGrades: string[] | null
95
+ returnLabelProvider: string | null
96
+ updatedAt: string | null
97
+ }
98
+
99
+ type GeneralSettingsEnvelope = {
100
+ ok?: boolean
101
+ result?: GeneralSettingsResult
102
+ error?: string
103
+ }
104
+
105
+ type GeneralSettingsFormValues = {
106
+ slaHours: string
107
+ slaPauseOnInfoRequested: boolean
108
+ slaAtRiskThresholdPct: string
109
+ autoApproveEnabled: boolean
110
+ autoApproveMaxAmount: string
111
+ autoApproveCurrencyCode: string
112
+ autoApproveRequireInWarranty: boolean
113
+ defaultWarrantyMonths: string
114
+ returnWindowDays: string
115
+ businessHours: BusinessHoursFormValue
116
+ escalationTiers: EscalationTierRow[]
117
+ adjudicationUseRules: boolean
118
+ quarantineGrades: string[]
119
+ returnLabelProvider: string
120
+ }
121
+
122
+ type EscalationTiersFieldTranslations = {
123
+ rowLabel: (index: number) => string
124
+ atPctLabel: string
125
+ actionLabel: string
126
+ actionNotify: string
127
+ actionReassign: string
128
+ toUserLabel: string
129
+ toUserPlaceholder: string
130
+ addRow: string
131
+ removeRow: string
132
+ empty: string
133
+ error: {
134
+ atPct: string
135
+ toUserId: string
136
+ }
137
+ }
138
+
139
+ type BusinessHoursFieldTranslations = {
140
+ dayLabels: Record<BusinessWeekday, string>
141
+ timezoneLabel: string
142
+ timezonePlaceholder: string
143
+ timezoneHelp: string
144
+ startLabel: string
145
+ endLabel: string
146
+ addWindow: string
147
+ removeWindow: string
148
+ closed: string
149
+ holidaysLabel: string
150
+ holidaysHelp: string
151
+ addHoliday: string
152
+ removeHoliday: string
153
+ holidayDateLabel: string
154
+ advancedToggle: string
155
+ advancedLabel: string
156
+ advancedHelp: string
157
+ error: {
158
+ window: string
159
+ holiday: string
160
+ rows: string
161
+ }
162
+ }
163
+
164
+ type GeneralSettingsTranslations = {
165
+ title: string
166
+ description: string
167
+ loading: string
168
+ saveError: string
169
+ loadError: string
170
+ invalidError: string
171
+ success: string
172
+ refresh: string
173
+ save: string
174
+ saving: string
175
+ sections: {
176
+ slaEscalation: string
177
+ adjudication: string
178
+ receiving: string
179
+ returns: string
180
+ }
181
+ jsonErrors: {
182
+ businessHours: string
183
+ escalationTiers: string
184
+ }
185
+ escalationTiers: EscalationTiersFieldTranslations
186
+ businessHours: BusinessHoursFieldTranslations
187
+ fields: {
188
+ slaHours: string
189
+ slaHoursHelp: string
190
+ slaPauseOnInfoRequested: string
191
+ slaPauseOnInfoRequestedHelp: string
192
+ slaAtRiskThresholdPct: string
193
+ slaAtRiskThresholdPctHelp: string
194
+ defaultWarrantyMonths: string
195
+ defaultWarrantyMonthsHelp: string
196
+ returnWindowDays: string
197
+ returnWindowDaysHelp: string
198
+ autoApproveEnabled: string
199
+ autoApproveEnabledHelp: string
200
+ autoApproveMaxAmount: string
201
+ autoApproveMaxAmountHelp: string
202
+ autoApproveCurrencyCode: string
203
+ autoApproveCurrencyCodeHelp: string
204
+ autoApproveRequireInWarranty: string
205
+ autoApproveRequireInWarrantyHelp: string
206
+ businessHours: string
207
+ businessHoursHelp: string
208
+ escalationTiers: string
209
+ escalationTiersHelp: string
210
+ adjudicationUseRules: string
211
+ adjudicationUseRulesHelp: string
212
+ quarantineGrades: string
213
+ quarantineGradesHelp: string
214
+ returnLabelProvider: string
215
+ returnLabelProviderHelp: string
216
+ }
217
+ }
218
+
219
+ type GeneralFieldErrors = {
220
+ businessHours?: string
221
+ businessHoursRowErrors?: Record<string, string>
222
+ escalationTiers?: string
223
+ escalationTierRowErrors?: Record<string, { atPct?: string; toUserId?: string }>
224
+ }
225
+
226
+ const DEFAULT_FORM_VALUES: DictionaryFormValues = {
227
+ value: '',
228
+ label: '',
229
+ color: null,
230
+ icon: null,
231
+ }
232
+
233
+ const SAVE_CONTEXT_ID = 'warranty-claims-settings'
234
+ const GENERAL_SETTINGS_FORM_ID = 'warranty-claims-general-settings'
235
+ const QUARANTINE_GRADE_OPTIONS = ['A', 'B', 'C', 'D'] as const
236
+
237
+ const DEFAULT_GENERAL_SETTINGS: GeneralSettingsResult = {
238
+ slaHours: 48,
239
+ slaPauseOnInfoRequested: true,
240
+ slaAtRiskThresholdPct: 75,
241
+ defaultWarrantyMonths: null,
242
+ returnWindowDays: null,
243
+ autoApproveEnabled: false,
244
+ autoApproveMaxAmount: null,
245
+ autoApproveCurrencyCode: null,
246
+ autoApproveRequireInWarranty: true,
247
+ updatedAt: null,
248
+ businessHours: null,
249
+ escalationTiers: null,
250
+ adjudicationUseRules: false,
251
+ quarantineGrades: null,
252
+ returnLabelProvider: null,
253
+ }
254
+
255
+ // Seeded entries store an English label, so the list must resolve the same
256
+ // localized option text the claim forms show. The stored label stays untouched
257
+ // and is what the edit dialog loads.
258
+ const DICTIONARY_LABEL_KINDS: Record<WarrantyDictionaryKind, DictionaryLabelKind> = {
259
+ 'warranty-claim-fault-code': 'fault',
260
+ 'warranty-claim-reason': 'reason',
261
+ 'warranty-claim-rejection-reason': 'rejection',
262
+ }
263
+
264
+ const SECTIONS: SectionDefinition[] = [
265
+ {
266
+ kind: 'warranty-claim-fault-code',
267
+ dictionaryKey: 'warranty_claims.warranty_claim_fault_code',
268
+ titleKey: 'warranty_claims.settings.dictionary.faultCodes',
269
+ descriptionKey: 'warranty_claims.settings.dictionary.faultCodes.description',
270
+ },
271
+ {
272
+ kind: 'warranty-claim-reason',
273
+ dictionaryKey: 'warranty_claims.warranty_claim_reason',
274
+ titleKey: 'warranty_claims.settings.dictionary.claimReasons',
275
+ descriptionKey: 'warranty_claims.settings.dictionary.claimReasons.description',
276
+ },
277
+ {
278
+ kind: 'warranty-claim-rejection-reason',
279
+ dictionaryKey: 'warranty_claims.warranty_claim_rejection_reason',
280
+ titleKey: 'warranty_claims.settings.dictionary.rejectionReasons',
281
+ descriptionKey: 'warranty_claims.settings.dictionary.rejectionReasons.description',
282
+ },
283
+ ]
284
+
285
+ function isRecord(value: unknown): value is Record<string, unknown> {
286
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
287
+ }
288
+
289
+ function toStringOrNull(value: unknown): string | null {
290
+ return typeof value === 'string' && value.trim().length ? value.trim() : null
291
+ }
292
+
293
+ type JsonParseResult<T> =
294
+ | { ok: true; value: T | null }
295
+ | { ok: false }
296
+
297
+ function parseNullableJsonObject(value: string): JsonParseResult<Record<string, unknown>> {
298
+ const trimmed = value.trim()
299
+ if (!trimmed) return { ok: true, value: null }
300
+ try {
301
+ const parsed: unknown = JSON.parse(trimmed)
302
+ if (isRecord(parsed)) return { ok: true, value: parsed }
303
+ } catch {
304
+ return { ok: false }
305
+ }
306
+ return { ok: false }
307
+ }
308
+
309
+ let escalationTierRowSeq = 0
310
+
311
+ function nextEscalationTierRowKey(): string {
312
+ escalationTierRowSeq += 1
313
+ return `escalation-tier-${escalationTierRowSeq}`
314
+ }
315
+
316
+ function buildEscalationTierRows(raw: unknown): EscalationTierRow[] {
317
+ return parseEscalationTiers(raw).map((tier) => ({
318
+ key: nextEscalationTierRowKey(),
319
+ atPct: String(tier.atPct),
320
+ action: tier.action,
321
+ toUserId: tier.toUserId ?? '',
322
+ }))
323
+ }
324
+
325
+ const ESCALATION_TIER_MIN_PCT = 1
326
+ const ESCALATION_TIER_MAX_PCT = 1000
327
+
328
+ type EscalationTierRowsValidation =
329
+ | { ok: true; value: EscalationTier[] | null }
330
+ | { ok: false; rowErrors: Record<string, { atPct?: string; toUserId?: string }> }
331
+
332
+ function validateEscalationTierRows(
333
+ rows: EscalationTierRow[],
334
+ translations: EscalationTiersFieldTranslations,
335
+ ): EscalationTierRowsValidation {
336
+ const rowErrors: Record<string, { atPct?: string; toUserId?: string }> = {}
337
+ const parsed: EscalationTier[] = []
338
+
339
+ for (const row of rows) {
340
+ const atPctNumber = Number(row.atPct)
341
+ const rowError: { atPct?: string; toUserId?: string } = {}
342
+ if (!Number.isInteger(atPctNumber) || atPctNumber < ESCALATION_TIER_MIN_PCT || atPctNumber > ESCALATION_TIER_MAX_PCT) {
343
+ rowError.atPct = translations.error.atPct
344
+ }
345
+ const toUserId = row.toUserId.trim()
346
+ if (row.action === 'reassign' && !toUserId) {
347
+ rowError.toUserId = translations.error.toUserId
348
+ }
349
+ if (Object.keys(rowError).length) {
350
+ rowErrors[row.key] = rowError
351
+ continue
352
+ }
353
+ parsed.push(row.action === 'reassign' ? { atPct: atPctNumber, action: row.action, toUserId } : { atPct: atPctNumber, action: row.action })
354
+ }
355
+
356
+ if (Object.keys(rowErrors).length) return { ok: false, rowErrors }
357
+
358
+ const sorted = [...parsed].sort((left, right) => left.atPct - right.atPct)
359
+ return { ok: true, value: sorted.length ? sorted : null }
360
+ }
361
+
362
+ function normalizeEntry(item: unknown): DictionaryTableEntry | null {
363
+ if (!isRecord(item)) return null
364
+ const id = toStringOrNull(item.id)
365
+ const value = toStringOrNull(item.value)
366
+ if (!id || !value) return null
367
+ return {
368
+ id,
369
+ value,
370
+ label: toStringOrNull(item.label) ?? value,
371
+ color: toStringOrNull(item.color),
372
+ icon: toStringOrNull(item.icon),
373
+ organizationId: toStringOrNull(item.organizationId),
374
+ tenantId: toStringOrNull(item.tenantId),
375
+ isInherited: item.isInherited === true,
376
+ createdAt: toStringOrNull(item.createdAt),
377
+ updatedAt: toStringOrNull(item.updatedAt),
378
+ }
379
+ }
380
+
381
+ function buildGeneralFormValues(settings: GeneralSettingsResult): GeneralSettingsFormValues {
382
+ return {
383
+ slaHours: String(settings.slaHours),
384
+ slaPauseOnInfoRequested: settings.slaPauseOnInfoRequested,
385
+ slaAtRiskThresholdPct: String(settings.slaAtRiskThresholdPct),
386
+ autoApproveEnabled: settings.autoApproveEnabled,
387
+ autoApproveMaxAmount: settings.autoApproveMaxAmount === null ? '' : String(settings.autoApproveMaxAmount),
388
+ autoApproveCurrencyCode: settings.autoApproveCurrencyCode ?? '',
389
+ autoApproveRequireInWarranty: settings.autoApproveRequireInWarranty,
390
+ defaultWarrantyMonths: settings.defaultWarrantyMonths === null ? '' : String(settings.defaultWarrantyMonths),
391
+ returnWindowDays: settings.returnWindowDays === null ? '' : String(settings.returnWindowDays),
392
+ businessHours: buildBusinessHoursFormValue(settings.businessHours),
393
+ escalationTiers: buildEscalationTierRows(settings.escalationTiers),
394
+ adjudicationUseRules: settings.adjudicationUseRules,
395
+ quarantineGrades: settings.quarantineGrades ?? [],
396
+ returnLabelProvider: settings.returnLabelProvider ?? '',
397
+ }
398
+ }
399
+
400
+ function normalizeCurrencyCodeInput(value: string): string {
401
+ return value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 3)
402
+ }
403
+
404
+ function translateApiErrorMessage(
405
+ call: { result: unknown },
406
+ fallbackMessage: string,
407
+ translate: (key: string, fallback?: string) => string,
408
+ ): string {
409
+ const payload = isRecord(call.result) ? call.result : {}
410
+ const message = typeof payload.error === 'string' ? payload.error : null
411
+ if (!message) return fallbackMessage
412
+ if (message === 'warranty_claims.errors.autoApproveConfigIncomplete') {
413
+ return translate(message, 'Auto-approve requires both a maximum amount and currency.')
414
+ }
415
+ if (message.startsWith('warranty_claims.')) return translate(message, fallbackMessage)
416
+ return message
417
+ }
418
+
419
+ function buildConflictError(
420
+ call: { status: number; result: unknown },
421
+ fallbackMessage: string,
422
+ translate?: (key: string, fallback?: string) => string,
423
+ ): Error & Record<string, unknown> {
424
+ const payload = isRecord(call.result) ? call.result : {}
425
+ const message = translate ? translateApiErrorMessage(call, fallbackMessage, translate) : typeof payload.error === 'string' ? payload.error : fallbackMessage
426
+ return Object.assign(new Error(message), { status: call.status }, payload)
427
+ }
428
+
429
+ function GeneralNumberField({
430
+ id,
431
+ label,
432
+ description,
433
+ value,
434
+ min,
435
+ max,
436
+ step = '1',
437
+ disabled,
438
+ onChange,
439
+ }: {
440
+ id: string
441
+ label: string
442
+ description: string
443
+ value: string
444
+ min: number
445
+ max?: number
446
+ step?: string
447
+ disabled?: boolean
448
+ onChange: (value: string) => void
449
+ }) {
450
+ return (
451
+ <div className="space-y-2">
452
+ <Label htmlFor={id}>{label}</Label>
453
+ <Input
454
+ id={id}
455
+ type="number"
456
+ min={min}
457
+ max={max}
458
+ step={step}
459
+ value={value}
460
+ disabled={disabled}
461
+ onChange={(event) => onChange(event.target.value)}
462
+ />
463
+ <p className="text-xs text-muted-foreground">{description}</p>
464
+ </div>
465
+ )
466
+ }
467
+
468
+ function GeneralTextField({
469
+ id,
470
+ label,
471
+ description,
472
+ value,
473
+ maxLength,
474
+ disabled,
475
+ onChange,
476
+ }: {
477
+ id: string
478
+ label: string
479
+ description: string
480
+ value: string
481
+ maxLength?: number
482
+ disabled?: boolean
483
+ onChange: (value: string) => void
484
+ }) {
485
+ return (
486
+ <div className="space-y-2">
487
+ <Label htmlFor={id}>{label}</Label>
488
+ <Input
489
+ id={id}
490
+ value={value}
491
+ maxLength={maxLength}
492
+ disabled={disabled}
493
+ onChange={(event) => onChange(event.target.value)}
494
+ />
495
+ <p className="text-xs text-muted-foreground">{description}</p>
496
+ </div>
497
+ )
498
+ }
499
+
500
+ function GeneralSwitchField({
501
+ id,
502
+ label,
503
+ description,
504
+ checked,
505
+ disabled,
506
+ onCheckedChange,
507
+ }: {
508
+ id: string
509
+ label: string
510
+ description: string
511
+ checked: boolean
512
+ disabled?: boolean
513
+ onCheckedChange: (checked: boolean) => void
514
+ }) {
515
+ return (
516
+ <div className="flex items-center justify-between gap-4 rounded-lg border border-border bg-background px-4 py-3">
517
+ <div className="space-y-1">
518
+ <Label htmlFor={id}>{label}</Label>
519
+ <p className="text-xs text-muted-foreground">{description}</p>
520
+ </div>
521
+ <Switch
522
+ id={id}
523
+ checked={checked}
524
+ disabled={disabled}
525
+ aria-label={label}
526
+ onCheckedChange={onCheckedChange}
527
+ />
528
+ </div>
529
+ )
530
+ }
531
+
532
+ function BusinessHoursField({
533
+ id,
534
+ label,
535
+ description,
536
+ value,
537
+ rowErrors,
538
+ error,
539
+ disabled,
540
+ translations,
541
+ onChange,
542
+ }: {
543
+ id: string
544
+ label: string
545
+ description: string
546
+ value: BusinessHoursFormValue
547
+ rowErrors?: Record<string, string>
548
+ error?: string
549
+ disabled?: boolean
550
+ translations: BusinessHoursFieldTranslations
551
+ onChange: (next: BusinessHoursFormValue) => void
552
+ }) {
553
+ const [advancedOpen, setAdvancedOpen] = React.useState(false)
554
+ const advancedVisible = advancedOpen || value.rawDirty
555
+ const controlsDisabled = disabled || value.rawDirty
556
+ const advancedRegionId = `${id}-advanced`
557
+
558
+ const timezoneOptions = React.useMemo<ComboboxOption[]>(() => {
559
+ const zones = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : []
560
+ return Array.from(new Set<string>(['UTC', ...zones])).map((zone) => ({ value: zone, label: zone }))
561
+ }, [])
562
+
563
+ const applyStructuredChange = React.useCallback((patch: Partial<BusinessHoursFormValue>) => {
564
+ const next = { ...value, ...patch }
565
+ onChange({ ...next, raw: stringifyJsonValue(serializeBusinessHoursRecord(next)), rawDirty: false })
566
+ }, [onChange, value])
567
+
568
+ const handleRawChange = React.useCallback((text: string) => {
569
+ const parsed = parseNullableJsonObject(text)
570
+ if (!parsed.ok) {
571
+ onChange({ ...value, raw: text, rawDirty: true })
572
+ return
573
+ }
574
+ onChange({ ...buildBusinessHoursFormValue(parsed.value), raw: text })
575
+ }, [onChange, value])
576
+
577
+ const toggleDay = React.useCallback((weekday: BusinessWeekday, enabled: boolean) => {
578
+ applyStructuredChange({
579
+ days: value.days.map((day) => (day.weekday === weekday ? { ...day, enabled } : day)),
580
+ })
581
+ }, [applyStructuredChange, value.days])
582
+
583
+ const updateInterval = React.useCallback((weekday: BusinessWeekday, key: string, patch: Partial<Pick<BusinessHoursIntervalRow, 'start' | 'end'>>) => {
584
+ applyStructuredChange({
585
+ days: value.days.map((day) => (day.weekday === weekday
586
+ ? { ...day, intervals: day.intervals.map((interval) => (interval.key === key ? { ...interval, ...patch } : interval)) }
587
+ : day)),
588
+ })
589
+ }, [applyStructuredChange, value.days])
590
+
591
+ const addInterval = React.useCallback((weekday: BusinessWeekday) => {
592
+ applyStructuredChange({
593
+ days: value.days.map((day) => (day.weekday === weekday
594
+ ? { ...day, intervals: [...day.intervals, createBusinessHoursIntervalRow('', '')] }
595
+ : day)),
596
+ })
597
+ }, [applyStructuredChange, value.days])
598
+
599
+ const removeInterval = React.useCallback((weekday: BusinessWeekday, key: string) => {
600
+ applyStructuredChange({
601
+ days: value.days.map((day) => (day.weekday === weekday
602
+ ? { ...day, intervals: day.intervals.filter((interval) => interval.key !== key) }
603
+ : day)),
604
+ })
605
+ }, [applyStructuredChange, value.days])
606
+
607
+ const addHoliday = React.useCallback(() => {
608
+ applyStructuredChange({
609
+ holidays: [...value.holidays, { key: nextBusinessHoursRowKey(), date: '' }],
610
+ })
611
+ }, [applyStructuredChange, value.holidays])
612
+
613
+ const updateHoliday = React.useCallback((key: string, date: string) => {
614
+ applyStructuredChange({
615
+ holidays: value.holidays.map((row) => (row.key === key ? { ...row, date } : row)),
616
+ })
617
+ }, [applyStructuredChange, value.holidays])
618
+
619
+ const removeHoliday = React.useCallback((key: string) => {
620
+ applyStructuredChange({
621
+ holidays: value.holidays.filter((row) => row.key !== key),
622
+ })
623
+ }, [applyStructuredChange, value.holidays])
624
+
625
+ return (
626
+ <div className="space-y-3">
627
+ <div className="space-y-1">
628
+ <h4 className="text-sm font-medium">{label}</h4>
629
+ <p className="text-xs text-muted-foreground">{description}</p>
630
+ </div>
631
+ <div className="space-y-2 sm:max-w-sm">
632
+ <Label htmlFor={`${id}-timezone`}>{translations.timezoneLabel}</Label>
633
+ <ComboboxInput
634
+ value={value.timezone}
635
+ onChange={(timezone) => applyStructuredChange({ timezone })}
636
+ placeholder={translations.timezonePlaceholder}
637
+ suggestions={timezoneOptions}
638
+ allowCustomValues
639
+ clearable
640
+ disabled={controlsDisabled}
641
+ />
642
+ <p className="text-xs text-muted-foreground">{translations.timezoneHelp}</p>
643
+ </div>
644
+ <div className="space-y-2">
645
+ {value.days.map((day) => {
646
+ const dayCheckboxId = `${id}-${day.weekday}-enabled`
647
+ return (
648
+ <div key={day.weekday} className="rounded-md border border-border bg-background p-3">
649
+ <div className="flex flex-wrap items-start gap-4">
650
+ <div className="flex w-28 items-center gap-2 pt-1.5">
651
+ <Checkbox
652
+ id={dayCheckboxId}
653
+ checked={day.enabled}
654
+ disabled={controlsDisabled}
655
+ onCheckedChange={(checked) => toggleDay(day.weekday, checked === true)}
656
+ />
657
+ <Label htmlFor={dayCheckboxId} className="text-sm font-normal">
658
+ {translations.dayLabels[day.weekday]}
659
+ </Label>
660
+ </div>
661
+ {day.enabled ? (
662
+ <div className="flex-1 space-y-2">
663
+ {day.intervals.map((interval) => {
664
+ const intervalError = rowErrors?.[interval.key]
665
+ const startInputId = `${id}-${day.weekday}-${interval.key}-start`
666
+ const endInputId = `${id}-${day.weekday}-${interval.key}-end`
667
+ return (
668
+ <div key={interval.key} className="space-y-1">
669
+ <div className="flex items-end gap-2">
670
+ <div className="space-y-1">
671
+ <Label htmlFor={startInputId} className="text-xs text-muted-foreground">
672
+ {translations.startLabel}
673
+ </Label>
674
+ <Input
675
+ id={startInputId}
676
+ type="time"
677
+ className="w-32"
678
+ value={interval.start}
679
+ disabled={controlsDisabled}
680
+ aria-invalid={intervalError ? true : undefined}
681
+ onChange={(event) => updateInterval(day.weekday, interval.key, { start: event.target.value })}
682
+ />
683
+ </div>
684
+ <span className="pb-2 text-sm text-muted-foreground" aria-hidden="true">–</span>
685
+ <div className="space-y-1">
686
+ <Label htmlFor={endInputId} className="text-xs text-muted-foreground">
687
+ {translations.endLabel}
688
+ </Label>
689
+ <Input
690
+ id={endInputId}
691
+ type="time"
692
+ className="w-32"
693
+ value={interval.end}
694
+ disabled={controlsDisabled}
695
+ aria-invalid={intervalError ? true : undefined}
696
+ onChange={(event) => updateInterval(day.weekday, interval.key, { end: event.target.value })}
697
+ />
698
+ </div>
699
+ {day.intervals.length > 1 ? (
700
+ <IconButton
701
+ type="button"
702
+ variant="ghost"
703
+ size="sm"
704
+ aria-label={translations.removeWindow}
705
+ disabled={controlsDisabled}
706
+ onClick={() => removeInterval(day.weekday, interval.key)}
707
+ >
708
+ <Trash2 className="size-4" aria-hidden="true" />
709
+ </IconButton>
710
+ ) : null}
711
+ </div>
712
+ {intervalError ? <p className="text-sm text-status-error-text">{intervalError}</p> : null}
713
+ </div>
714
+ )
715
+ })}
716
+ <Button
717
+ type="button"
718
+ variant="outline"
719
+ size="sm"
720
+ disabled={controlsDisabled}
721
+ onClick={() => addInterval(day.weekday)}
722
+ >
723
+ <Plus className="size-4" aria-hidden="true" />
724
+ {translations.addWindow}
725
+ </Button>
726
+ </div>
727
+ ) : (
728
+ <p className="pt-1.5 text-sm text-muted-foreground">{translations.closed}</p>
729
+ )}
730
+ </div>
731
+ </div>
732
+ )
733
+ })}
734
+ </div>
735
+ <div className="space-y-2">
736
+ <div className="space-y-1">
737
+ <Label>{translations.holidaysLabel}</Label>
738
+ <p className="text-xs text-muted-foreground">{translations.holidaysHelp}</p>
739
+ </div>
740
+ {value.holidays.map((row) => {
741
+ const holidayError = rowErrors?.[row.key]
742
+ const holidayInputId = `${id}-${row.key}-holiday`
743
+ return (
744
+ <div key={row.key} className="space-y-1">
745
+ <div className="flex items-end gap-2">
746
+ <div className="space-y-1">
747
+ <Label htmlFor={holidayInputId} className="text-xs text-muted-foreground">
748
+ {translations.holidayDateLabel}
749
+ </Label>
750
+ <Input
751
+ id={holidayInputId}
752
+ type="date"
753
+ className="w-44"
754
+ value={row.date}
755
+ disabled={controlsDisabled}
756
+ aria-invalid={holidayError ? true : undefined}
757
+ onChange={(event) => updateHoliday(row.key, event.target.value)}
758
+ />
759
+ </div>
760
+ <IconButton
761
+ type="button"
762
+ variant="ghost"
763
+ size="sm"
764
+ aria-label={translations.removeHoliday}
765
+ disabled={controlsDisabled}
766
+ onClick={() => removeHoliday(row.key)}
767
+ >
768
+ <Trash2 className="size-4" aria-hidden="true" />
769
+ </IconButton>
770
+ </div>
771
+ {holidayError ? <p className="text-sm text-status-error-text">{holidayError}</p> : null}
772
+ </div>
773
+ )
774
+ })}
775
+ <Button type="button" variant="outline" size="sm" disabled={controlsDisabled} onClick={addHoliday}>
776
+ <Plus className="size-4" aria-hidden="true" />
777
+ {translations.addHoliday}
778
+ </Button>
779
+ </div>
780
+ <div className="space-y-2">
781
+ <Button
782
+ type="button"
783
+ variant="ghost"
784
+ size="sm"
785
+ aria-expanded={advancedVisible}
786
+ aria-controls={advancedRegionId}
787
+ disabled={disabled}
788
+ onClick={() => setAdvancedOpen((open) => !open)}
789
+ >
790
+ {advancedVisible
791
+ ? <ChevronDown className="size-4" aria-hidden="true" />
792
+ : <ChevronRight className="size-4" aria-hidden="true" />}
793
+ {translations.advancedToggle}
794
+ </Button>
795
+ {advancedVisible ? (
796
+ <div id={advancedRegionId} className="space-y-2">
797
+ <Label htmlFor={`${id}-raw`}>{translations.advancedLabel}</Label>
798
+ <Textarea
799
+ id={`${id}-raw`}
800
+ rows={6}
801
+ value={value.raw}
802
+ disabled={disabled}
803
+ aria-invalid={error ? true : undefined}
804
+ onChange={(event) => handleRawChange(event.target.value)}
805
+ />
806
+ <p className="text-xs text-muted-foreground">{translations.advancedHelp}</p>
807
+ </div>
808
+ ) : null}
809
+ </div>
810
+ {error ? <ErrorMessage label={error} /> : null}
811
+ </div>
812
+ )
813
+ }
814
+
815
+ function GeneralQuarantineGradesField({
816
+ id,
817
+ label,
818
+ description,
819
+ selected,
820
+ disabled,
821
+ onToggle,
822
+ }: {
823
+ id: string
824
+ label: string
825
+ description: string
826
+ selected: string[]
827
+ disabled?: boolean
828
+ onToggle: (grade: string, checked: boolean) => void
829
+ }) {
830
+ return (
831
+ <div className="space-y-3">
832
+ <div className="space-y-1">
833
+ <Label id={`${id}-label`}>{label}</Label>
834
+ <p id={`${id}-description`} className="text-xs text-muted-foreground">{description}</p>
835
+ </div>
836
+ <div
837
+ role="group"
838
+ aria-labelledby={`${id}-label`}
839
+ aria-describedby={`${id}-description`}
840
+ className="grid gap-2 sm:grid-cols-4"
841
+ >
842
+ {QUARANTINE_GRADE_OPTIONS.map((grade) => {
843
+ const checkboxId = `${id}-${grade.toLowerCase()}`
844
+ return (
845
+ <div
846
+ key={grade}
847
+ className="flex items-center gap-2 rounded-md border border-border bg-background p-3"
848
+ >
849
+ <Checkbox
850
+ id={checkboxId}
851
+ checked={selected.includes(grade)}
852
+ disabled={disabled}
853
+ onCheckedChange={(checked) => onToggle(grade, checked === true)}
854
+ />
855
+ <Label htmlFor={checkboxId} className="text-sm font-normal">
856
+ {grade}
857
+ </Label>
858
+ </div>
859
+ )
860
+ })}
861
+ </div>
862
+ </div>
863
+ )
864
+ }
865
+
866
+ function EscalationTiersField({
867
+ id,
868
+ label,
869
+ description,
870
+ rows,
871
+ rowErrors,
872
+ error,
873
+ disabled,
874
+ translations,
875
+ loadStaffOptions,
876
+ resolveStaffLabel,
877
+ onAddRow,
878
+ onRemoveRow,
879
+ onAtPctChange,
880
+ onActionChange,
881
+ onUserChange,
882
+ }: {
883
+ id: string
884
+ label: string
885
+ description: string
886
+ rows: EscalationTierRow[]
887
+ rowErrors?: Record<string, { atPct?: string; toUserId?: string }>
888
+ error?: string
889
+ disabled?: boolean
890
+ translations: EscalationTiersFieldTranslations
891
+ loadStaffOptions: (query?: string) => Promise<ComboboxOption[]>
892
+ resolveStaffLabel: (userId: string) => Promise<string>
893
+ onAddRow: () => void
894
+ onRemoveRow: (key: string) => void
895
+ onAtPctChange: (key: string, value: string) => void
896
+ onActionChange: (key: string, value: EscalationTierRow['action']) => void
897
+ onUserChange: (key: string, value: string) => void
898
+ }) {
899
+ return (
900
+ <div className="space-y-3">
901
+ <div className="space-y-1">
902
+ <h4 className="text-sm font-medium">{label}</h4>
903
+ <p className="text-xs text-muted-foreground">{description}</p>
904
+ </div>
905
+ {rows.length === 0 ? (
906
+ <p className="rounded-md border border-dashed border-border bg-background p-4 text-sm text-muted-foreground">
907
+ {translations.empty}
908
+ </p>
909
+ ) : (
910
+ <div className="space-y-3">
911
+ {rows.map((row, index) => {
912
+ const rowError = rowErrors?.[row.key]
913
+ const atPctFieldId = `${id}-${row.key}-atPct`
914
+ const actionFieldId = `${id}-${row.key}-action`
915
+ const toUserFieldId = `${id}-${row.key}-toUserId`
916
+ return (
917
+ <div key={row.key} className="space-y-3 rounded-md border border-border bg-background p-4">
918
+ <div className="flex items-center justify-between gap-3">
919
+ <h5 className="text-sm font-medium">{translations.rowLabel(index + 1)}</h5>
920
+ <IconButton
921
+ type="button"
922
+ variant="ghost"
923
+ size="sm"
924
+ aria-label={translations.removeRow}
925
+ disabled={disabled}
926
+ onClick={() => onRemoveRow(row.key)}
927
+ >
928
+ <Trash2 className="size-4" aria-hidden="true" />
929
+ </IconButton>
930
+ </div>
931
+ <div className="grid gap-4 sm:grid-cols-3">
932
+ <div className="space-y-2">
933
+ <Label htmlFor={atPctFieldId}>{translations.atPctLabel}</Label>
934
+ <Input
935
+ id={atPctFieldId}
936
+ type="number"
937
+ min={ESCALATION_TIER_MIN_PCT}
938
+ max={ESCALATION_TIER_MAX_PCT}
939
+ value={row.atPct}
940
+ disabled={disabled}
941
+ aria-invalid={rowError?.atPct ? true : undefined}
942
+ onChange={(event) => onAtPctChange(row.key, event.target.value)}
943
+ />
944
+ {rowError?.atPct ? <p className="text-sm text-status-error-text">{rowError.atPct}</p> : null}
945
+ </div>
946
+ <div className="space-y-2">
947
+ <Label htmlFor={actionFieldId}>{translations.actionLabel}</Label>
948
+ <Select
949
+ value={row.action}
950
+ onValueChange={(value) => onActionChange(row.key, value === 'reassign' ? 'reassign' : 'notify')}
951
+ >
952
+ <SelectTrigger id={actionFieldId} disabled={disabled}>
953
+ <SelectValue />
954
+ </SelectTrigger>
955
+ <SelectContent>
956
+ <SelectItem value="notify">{translations.actionNotify}</SelectItem>
957
+ <SelectItem value="reassign">{translations.actionReassign}</SelectItem>
958
+ </SelectContent>
959
+ </Select>
960
+ </div>
961
+ {row.action === 'reassign' ? (
962
+ <div className="space-y-2">
963
+ <Label htmlFor={toUserFieldId}>{translations.toUserLabel}</Label>
964
+ <ComboboxInput
965
+ value={row.toUserId}
966
+ onChange={(value) => onUserChange(row.key, value)}
967
+ placeholder={translations.toUserPlaceholder}
968
+ loadSuggestions={loadStaffOptions}
969
+ resolveLabel={resolveStaffLabel}
970
+ allowCustomValues={false}
971
+ disabled={disabled}
972
+ />
973
+ {rowError?.toUserId ? <p className="text-sm text-status-error-text">{rowError.toUserId}</p> : null}
974
+ </div>
975
+ ) : null}
976
+ </div>
977
+ </div>
978
+ )
979
+ })}
980
+ </div>
981
+ )}
982
+ <Button type="button" variant="outline" size="sm" disabled={disabled} onClick={onAddRow}>
983
+ <Plus className="size-4" aria-hidden="true" />
984
+ {translations.addRow}
985
+ </Button>
986
+ {error ? <ErrorMessage label={error} /> : null}
987
+ </div>
988
+ )
989
+ }
990
+
991
+ function GeneralSettingsSubsection({
992
+ title,
993
+ children,
994
+ }: {
995
+ title: string
996
+ children: React.ReactNode
997
+ }) {
998
+ return (
999
+ <div className="space-y-4">
1000
+ <h3 className="text-sm font-medium">{title}</h3>
1001
+ {children}
1002
+ </div>
1003
+ )
1004
+ }
1005
+
1006
+ export default function WarrantyClaimSettingsPage() {
1007
+ const t = useT()
1008
+ const scopeVersion = useOrganizationScopeVersion()
1009
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
1010
+ const [entriesByKind, setEntriesByKind] = React.useState<Record<WarrantyDictionaryKind, DictionaryTableEntry[]>>({
1011
+ 'warranty-claim-fault-code': [],
1012
+ 'warranty-claim-reason': [],
1013
+ 'warranty-claim-rejection-reason': [],
1014
+ })
1015
+ const [dictionaryIds, setDictionaryIds] = React.useState<Record<WarrantyDictionaryKind, string | null>>({
1016
+ 'warranty-claim-fault-code': null,
1017
+ 'warranty-claim-reason': null,
1018
+ 'warranty-claim-rejection-reason': null,
1019
+ })
1020
+ const [loadingKind, setLoadingKind] = React.useState<Record<WarrantyDictionaryKind, boolean>>({
1021
+ 'warranty-claim-fault-code': false,
1022
+ 'warranty-claim-reason': false,
1023
+ 'warranty-claim-rejection-reason': false,
1024
+ })
1025
+ const [dialog, setDialog] = React.useState<DialogState | null>(null)
1026
+ const [submitting, setSubmitting] = React.useState(false)
1027
+ const [generalSettings, setGeneralSettings] = React.useState<GeneralSettingsResult | null>(null)
1028
+ const [generalForm, setGeneralForm] = React.useState<GeneralSettingsFormValues>(() => buildGeneralFormValues(DEFAULT_GENERAL_SETTINGS))
1029
+ const [generalLoading, setGeneralLoading] = React.useState(true)
1030
+ const [generalSaving, setGeneralSaving] = React.useState(false)
1031
+ const [generalLoadError, setGeneralLoadError] = React.useState<string | null>(null)
1032
+ const [generalSaveError, setGeneralSaveError] = React.useState<string | null>(null)
1033
+ const [generalFieldErrors, setGeneralFieldErrors] = React.useState<GeneralFieldErrors>({})
1034
+
1035
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
1036
+ formId: string
1037
+ resourceKind: string
1038
+ retryLastMutation: () => Promise<boolean>
1039
+ }>({
1040
+ contextId: SAVE_CONTEXT_ID,
1041
+ blockedMessage: t('warranty_claims.common.saveBlocked'),
1042
+ })
1043
+ const mutationContext = React.useMemo(() => ({
1044
+ formId: SAVE_CONTEXT_ID,
1045
+ resourceKind: 'warranty_claims.dictionaries',
1046
+ retryLastMutation,
1047
+ }), [retryLastMutation])
1048
+
1049
+ const generalMutationContext = React.useMemo(() => ({
1050
+ formId: GENERAL_SETTINGS_FORM_ID,
1051
+ resourceKind: 'warranty_claims.settings',
1052
+ retryLastMutation,
1053
+ }), [retryLastMutation])
1054
+
1055
+ const generalTranslations = React.useMemo<GeneralSettingsTranslations>(() => ({
1056
+ title: t('warranty_claims.settings.general.title', 'General'),
1057
+ description: t('warranty_claims.settings.general.description', 'Configure SLA timing and light auto-approval defaults for warranty claims.'),
1058
+ loading: t('warranty_claims.settings.general.loading', 'Loading general settings...'),
1059
+ saveError: t('warranty_claims.settings.general.error.save', 'Failed to save general settings.'),
1060
+ loadError: t('warranty_claims.settings.general.error.load', 'Failed to load general settings.'),
1061
+ invalidError: t('warranty_claims.settings.general.error.invalid', 'Review the highlighted numeric and currency limits.'),
1062
+ success: t('warranty_claims.settings.general.success.save', 'General settings saved.'),
1063
+ refresh: t('warranty_claims.settings.general.actions.refresh', 'Refresh'),
1064
+ save: t('warranty_claims.settings.general.actions.save', 'Save settings'),
1065
+ saving: t('warranty_claims.settings.general.actions.saving', 'Saving...'),
1066
+ sections: {
1067
+ slaEscalation: t('warranty_claims.settings.general.sections.slaEscalation', 'SLA escalation'),
1068
+ adjudication: t('warranty_claims.settings.general.sections.adjudication', 'Adjudication'),
1069
+ receiving: t('warranty_claims.settings.general.sections.receiving', 'Receiving'),
1070
+ returns: t('warranty_claims.settings.general.sections.returns', 'Returns'),
1071
+ },
1072
+ jsonErrors: {
1073
+ businessHours: t('warranty_claims.settings.general.error.businessHoursJson', 'Invalid business hours JSON'),
1074
+ escalationTiers: t('warranty_claims.settings.general.error.escalationTiersInvalid', 'Fix the highlighted escalation tier rows.'),
1075
+ },
1076
+ fields: {
1077
+ slaHours: t('warranty_claims.settings.general.fields.slaHours.label', 'SLA hours'),
1078
+ slaHoursHelp: t('warranty_claims.settings.general.fields.slaHours.help', 'Target response window in hours, from 1 to 8760.'),
1079
+ slaPauseOnInfoRequested: t('warranty_claims.settings.general.fields.slaPauseOnInfoRequested.label', 'Pause SLA while info is requested'),
1080
+ slaPauseOnInfoRequestedHelp: t('warranty_claims.settings.general.fields.slaPauseOnInfoRequested.help', 'Stop the SLA clock when a claim waits on customer information.'),
1081
+ slaAtRiskThresholdPct: t('warranty_claims.settings.general.fields.slaAtRiskThresholdPct.label', 'SLA at-risk threshold'),
1082
+ slaAtRiskThresholdPctHelp: t('warranty_claims.settings.general.fields.slaAtRiskThresholdPct.help', 'Percent of the SLA window elapsed before a claim is marked at risk.'),
1083
+ defaultWarrantyMonths: t('warranty_claims.settings.general.fields.defaultWarrantyMonths.label', 'Default warranty months'),
1084
+ defaultWarrantyMonthsHelp: t('warranty_claims.settings.general.fields.defaultWarrantyMonths.help', 'Prefills warranty months on new claim lines and estimates portal entitlement. Leave empty to disable.'),
1085
+ returnWindowDays: t('warranty_claims.settings.returnWindowDays.label', 'Return window (days)'),
1086
+ returnWindowDaysHelp: t('warranty_claims.settings.returnWindowDays.help', 'Flag return and core-return claims submitted after this many days from order placement. Leave empty to disable.'),
1087
+ autoApproveEnabled: t('warranty_claims.settings.general.fields.autoApproveEnabled.label', 'Enable auto-approve'),
1088
+ autoApproveEnabledHelp: t('warranty_claims.settings.general.fields.autoApproveEnabled.help', 'Automatically approve eligible submitted claims when all configured limits match.'),
1089
+ autoApproveMaxAmount: t('warranty_claims.settings.general.fields.autoApproveMaxAmount.label', 'Auto-approve max amount'),
1090
+ autoApproveMaxAmountHelp: t('warranty_claims.settings.general.fields.autoApproveMaxAmount.help', 'Maximum claimed total allowed for auto-approval.'),
1091
+ autoApproveCurrencyCode: t('warranty_claims.settings.general.fields.autoApproveCurrencyCode.label', 'Auto-approve currency'),
1092
+ autoApproveCurrencyCodeHelp: t('warranty_claims.settings.general.fields.autoApproveCurrencyCode.help', 'Three-letter uppercase currency code used by the auto-approval limit.'),
1093
+ autoApproveRequireInWarranty: t('warranty_claims.settings.general.fields.autoApproveRequireInWarranty.label', 'Require in-warranty lines'),
1094
+ autoApproveRequireInWarrantyHelp: t('warranty_claims.settings.general.fields.autoApproveRequireInWarranty.help', 'Only auto-approve when every line is still in warranty.'),
1095
+ businessHours: t('warranty_claims.settings.general.fields.businessHoursEditor.label', 'Business hours'),
1096
+ businessHoursHelp: t('warranty_claims.settings.general.fields.businessHoursEditor.help', 'Working windows per weekday used by SLA timing. Leave every day unchecked to count wall-clock time.'),
1097
+ escalationTiers: t('warranty_claims.settings.general.fields.escalationTiersEditor.label', 'Escalation tiers'),
1098
+ escalationTiersHelp: t('warranty_claims.settings.general.fields.escalationTiersEditor.help', 'Notify or reassign a claim once its SLA has elapsed past a percentage threshold. Tiers apply in ascending order. Leave empty to disable.'),
1099
+ adjudicationUseRules: t('warranty_claims.settings.general.fields.adjudicationUseRules.label', 'Use business rules for adjudication'),
1100
+ adjudicationUseRulesHelp: t('warranty_claims.settings.general.fields.adjudicationUseRules.help', 'When on and the business_rules module is present, claim submission is evaluated by the rule engine; otherwise the built-in light rule is used.'),
1101
+ quarantineGrades: t('warranty_claims.settings.general.fields.quarantineGrades.label', 'Quarantine grades'),
1102
+ quarantineGradesHelp: t('warranty_claims.settings.general.fields.quarantineGrades.help', 'Grades that automatically hold a claim on receiving.'),
1103
+ returnLabelProvider: t('warranty_claims.settings.general.fields.returnLabelProvider.label', 'Return label provider'),
1104
+ returnLabelProviderHelp: t('warranty_claims.settings.general.fields.returnLabelProvider.help', 'Optional provider key for the return-label seam. Leave empty for manual entry only.'),
1105
+ },
1106
+ escalationTiers: {
1107
+ rowLabel: (index: number) => t('warranty_claims.settings.general.escalationTiers.rowLabel', 'Tier {number}', { number: index }),
1108
+ atPctLabel: t('warranty_claims.settings.general.escalationTiers.atPctLabel', 'SLA elapsed (%)'),
1109
+ actionLabel: t('warranty_claims.settings.general.escalationTiers.actionLabel', 'Action'),
1110
+ actionNotify: t('warranty_claims.settings.general.escalationTiers.action.notify', 'Notify'),
1111
+ actionReassign: t('warranty_claims.settings.general.escalationTiers.action.reassign', 'Reassign'),
1112
+ toUserLabel: t('warranty_claims.settings.general.escalationTiers.toUserLabel', 'Reassign to'),
1113
+ toUserPlaceholder: t('warranty_claims.settings.general.escalationTiers.toUserPlaceholder', 'Search staff'),
1114
+ addRow: t('warranty_claims.settings.general.escalationTiers.addRow', 'Add tier'),
1115
+ removeRow: t('warranty_claims.settings.general.escalationTiers.removeRow', 'Remove tier'),
1116
+ empty: t('warranty_claims.settings.general.escalationTiers.empty', 'No escalation tiers configured. Claims will not auto-escalate.'),
1117
+ error: {
1118
+ atPct: t('warranty_claims.settings.general.escalationTiers.error.atPct', 'Enter a whole number between 1 and 1000.'),
1119
+ toUserId: t('warranty_claims.settings.general.escalationTiers.error.toUserId', 'Select a staff member to reassign to.'),
1120
+ },
1121
+ },
1122
+ businessHours: {
1123
+ dayLabels: {
1124
+ mon: t('warranty_claims.settings.general.businessHours.day.mon', 'Monday'),
1125
+ tue: t('warranty_claims.settings.general.businessHours.day.tue', 'Tuesday'),
1126
+ wed: t('warranty_claims.settings.general.businessHours.day.wed', 'Wednesday'),
1127
+ thu: t('warranty_claims.settings.general.businessHours.day.thu', 'Thursday'),
1128
+ fri: t('warranty_claims.settings.general.businessHours.day.fri', 'Friday'),
1129
+ sat: t('warranty_claims.settings.general.businessHours.day.sat', 'Saturday'),
1130
+ sun: t('warranty_claims.settings.general.businessHours.day.sun', 'Sunday'),
1131
+ },
1132
+ timezoneLabel: t('warranty_claims.settings.general.businessHours.timezoneLabel', 'Timezone'),
1133
+ timezonePlaceholder: t('warranty_claims.settings.general.businessHours.timezonePlaceholder', 'UTC'),
1134
+ timezoneHelp: t('warranty_claims.settings.general.businessHours.timezoneHelp', 'IANA timezone the weekly windows are evaluated in. Leave empty for UTC.'),
1135
+ startLabel: t('warranty_claims.settings.general.businessHours.startLabel', 'Opens at'),
1136
+ endLabel: t('warranty_claims.settings.general.businessHours.endLabel', 'Closes at'),
1137
+ addWindow: t('warranty_claims.settings.general.businessHours.addWindow', 'Add window'),
1138
+ removeWindow: t('warranty_claims.settings.general.businessHours.removeWindow', 'Remove window'),
1139
+ closed: t('warranty_claims.settings.general.businessHours.closed', 'Closed'),
1140
+ holidaysLabel: t('warranty_claims.settings.general.businessHours.holidaysLabel', 'Holidays'),
1141
+ holidaysHelp: t('warranty_claims.settings.general.businessHours.holidaysHelp', 'Dates skipped entirely when counting SLA business hours.'),
1142
+ addHoliday: t('warranty_claims.settings.general.businessHours.addHoliday', 'Add holiday'),
1143
+ removeHoliday: t('warranty_claims.settings.general.businessHours.removeHoliday', 'Remove holiday'),
1144
+ holidayDateLabel: t('warranty_claims.settings.general.businessHours.holidayDateLabel', 'Holiday date'),
1145
+ advancedToggle: t('warranty_claims.settings.general.businessHours.advancedToggle', 'Advanced: edit raw JSON'),
1146
+ advancedLabel: t('warranty_claims.settings.general.fields.businessHours.label', 'Business hours JSON'),
1147
+ advancedHelp: t('warranty_claims.settings.general.fields.businessHours.help', 'Shape: { timezone, week: { mon:[{start,end}], ... }, holidays:[...] }. Leave empty to disable.'),
1148
+ error: {
1149
+ window: t('warranty_claims.settings.general.businessHours.error.window', 'Enter a start time earlier than the end time.'),
1150
+ holiday: t('warranty_claims.settings.general.businessHours.error.holiday', 'Pick a holiday date.'),
1151
+ rows: t('warranty_claims.settings.general.businessHours.error.rows', 'Fix the highlighted business hours entries.'),
1152
+ },
1153
+ },
1154
+ }), [t])
1155
+
1156
+ const applyGeneralSettings = React.useCallback((settings: GeneralSettingsResult) => {
1157
+ setGeneralSettings(settings)
1158
+ setGeneralForm(buildGeneralFormValues(settings))
1159
+ setGeneralFieldErrors({})
1160
+ }, [])
1161
+
1162
+ const loadGeneralSettings = React.useCallback(async () => {
1163
+ setGeneralLoading(true)
1164
+ setGeneralLoadError(null)
1165
+ try {
1166
+ const call = await apiCall<GeneralSettingsEnvelope>('/api/warranty_claims/settings-general')
1167
+ const settings = call.result?.result ?? null
1168
+ if (!call.ok || !settings) {
1169
+ setGeneralLoadError(translateApiErrorMessage(call, generalTranslations.loadError, t))
1170
+ return
1171
+ }
1172
+ applyGeneralSettings(settings)
1173
+ setGeneralSaveError(null)
1174
+ } catch (err) {
1175
+ const message = err instanceof Error ? err.message : generalTranslations.loadError
1176
+ setGeneralLoadError(message)
1177
+ } finally {
1178
+ setGeneralLoading(false)
1179
+ }
1180
+ }, [applyGeneralSettings, generalTranslations.loadError, t])
1181
+
1182
+ const updateGeneralForm = React.useCallback((patch: Partial<GeneralSettingsFormValues>) => {
1183
+ setGeneralForm((prev) => ({ ...prev, ...patch }))
1184
+ }, [])
1185
+
1186
+ const updateBusinessHours = React.useCallback((next: BusinessHoursFormValue) => {
1187
+ setGeneralForm((prev) => ({ ...prev, businessHours: next }))
1188
+ setGeneralFieldErrors((prev) => {
1189
+ if (!prev.businessHours && !prev.businessHoursRowErrors) return prev
1190
+ return { ...prev, businessHours: undefined, businessHoursRowErrors: undefined }
1191
+ })
1192
+ }, [])
1193
+
1194
+ const staffOptionLabel = React.useCallback((member: AssignableStaffMember): string => (
1195
+ member.email && member.email !== member.displayName
1196
+ ? `${member.displayName} (${member.email})`
1197
+ : member.displayName
1198
+ ), [])
1199
+
1200
+ const loadEscalationStaffOptions = React.useCallback(async (query?: string): Promise<ComboboxOption[]> => {
1201
+ const page = await fetchAssignableStaffMembersPage(query ?? '', { pageSize: 24 })
1202
+ return page.items.map((member) => ({ value: member.userId, label: staffOptionLabel(member) }))
1203
+ }, [staffOptionLabel])
1204
+
1205
+ const resolveStaffUserLabel = React.useCallback(async (userId: string): Promise<string> => {
1206
+ const unknownUserLabel = t('warranty_claims.detail.unknownUser')
1207
+ const response = await apiCall<{ items?: Array<Record<string, unknown>> }>(
1208
+ `/api/warranty_claims/assignees?ids=${encodeURIComponent(userId)}`,
1209
+ )
1210
+ if (!response.ok || !response.result) return unknownUserLabel
1211
+ const user = (response.result.items ?? [])[0]
1212
+ if (!user) return unknownUserLabel
1213
+ return toStringOrNull(user.name) ?? unknownUserLabel
1214
+ }, [t])
1215
+
1216
+ const addEscalationTierRow = React.useCallback(() => {
1217
+ setGeneralForm((prev) => ({
1218
+ ...prev,
1219
+ escalationTiers: [
1220
+ ...prev.escalationTiers,
1221
+ { key: nextEscalationTierRowKey(), atPct: '', action: 'notify', toUserId: '' },
1222
+ ],
1223
+ }))
1224
+ }, [])
1225
+
1226
+ const clearEscalationTierRowError = React.useCallback((key: string, field: 'atPct' | 'toUserId') => {
1227
+ setGeneralFieldErrors((prev) => {
1228
+ if (!prev.escalationTierRowErrors?.[key]?.[field]) return prev
1229
+ const nextRowErrors = { ...prev.escalationTierRowErrors, [key]: { ...prev.escalationTierRowErrors[key], [field]: undefined } }
1230
+ const hasRemainingErrors = Object.values(nextRowErrors).some((rowError) => rowError?.atPct || rowError?.toUserId)
1231
+ return { ...prev, escalationTierRowErrors: nextRowErrors, escalationTiers: hasRemainingErrors ? prev.escalationTiers : undefined }
1232
+ })
1233
+ }, [])
1234
+
1235
+ const removeEscalationTierRow = React.useCallback((key: string) => {
1236
+ setGeneralForm((prev) => ({
1237
+ ...prev,
1238
+ escalationTiers: prev.escalationTiers.filter((row) => row.key !== key),
1239
+ }))
1240
+ setGeneralFieldErrors((prev) => {
1241
+ if (!prev.escalationTierRowErrors?.[key]) return prev
1242
+ const nextRowErrors = { ...prev.escalationTierRowErrors }
1243
+ delete nextRowErrors[key]
1244
+ const hasRemainingErrors = Object.values(nextRowErrors).some((rowError) => rowError?.atPct || rowError?.toUserId)
1245
+ return { ...prev, escalationTierRowErrors: nextRowErrors, escalationTiers: hasRemainingErrors ? prev.escalationTiers : undefined }
1246
+ })
1247
+ }, [])
1248
+
1249
+ const updateEscalationTierAtPct = React.useCallback((key: string, value: string) => {
1250
+ setGeneralForm((prev) => ({
1251
+ ...prev,
1252
+ escalationTiers: prev.escalationTiers.map((row) => (row.key === key ? { ...row, atPct: value } : row)),
1253
+ }))
1254
+ clearEscalationTierRowError(key, 'atPct')
1255
+ }, [clearEscalationTierRowError])
1256
+
1257
+ const updateEscalationTierAction = React.useCallback((key: string, value: EscalationTierRow['action']) => {
1258
+ setGeneralForm((prev) => ({
1259
+ ...prev,
1260
+ escalationTiers: prev.escalationTiers.map((row) => (row.key === key ? { ...row, action: value } : row)),
1261
+ }))
1262
+ clearEscalationTierRowError(key, 'toUserId')
1263
+ }, [clearEscalationTierRowError])
1264
+
1265
+ const updateEscalationTierUser = React.useCallback((key: string, value: string) => {
1266
+ setGeneralForm((prev) => ({
1267
+ ...prev,
1268
+ escalationTiers: prev.escalationTiers.map((row) => (row.key === key ? { ...row, toUserId: value } : row)),
1269
+ }))
1270
+ clearEscalationTierRowError(key, 'toUserId')
1271
+ }, [clearEscalationTierRowError])
1272
+
1273
+ const handleGeneralSubmit = React.useCallback(async (event: React.FormEvent<HTMLFormElement>) => {
1274
+ event.preventDefault()
1275
+ setGeneralSaveError(null)
1276
+ setGeneralFieldErrors({})
1277
+
1278
+ const slaHours = Number(generalForm.slaHours)
1279
+ const slaAtRiskThresholdPct = Number(generalForm.slaAtRiskThresholdPct)
1280
+ const amountText = generalForm.autoApproveMaxAmount.trim()
1281
+ const autoApproveMaxAmount = amountText.length ? Number(amountText) : null
1282
+ const warrantyMonthsText = generalForm.defaultWarrantyMonths.trim()
1283
+ const defaultWarrantyMonths = warrantyMonthsText.length ? Number(warrantyMonthsText) : null
1284
+ const returnWindowDaysText = generalForm.returnWindowDays.trim()
1285
+ const returnWindowDays = returnWindowDaysText.length ? Number(returnWindowDaysText) : null
1286
+ const currencyCode = toStringOrNull(generalForm.autoApproveCurrencyCode)?.toUpperCase() ?? null
1287
+ const businessHoursResult = validateBusinessHoursValue(generalForm.businessHours, generalTranslations.businessHours)
1288
+ const escalationTiersResult = validateEscalationTierRows(generalForm.escalationTiers, generalTranslations.escalationTiers)
1289
+ const returnLabelProvider = toStringOrNull(generalForm.returnLabelProvider)
1290
+ const quarantineGrades = Array.from(new Set(
1291
+ generalForm.quarantineGrades
1292
+ .map((grade) => grade.trim())
1293
+ .filter((grade) => grade.length > 0),
1294
+ ))
1295
+
1296
+ if (!businessHoursResult.ok || !escalationTiersResult.ok) {
1297
+ setGeneralFieldErrors({
1298
+ businessHours: businessHoursResult.ok
1299
+ ? undefined
1300
+ : businessHoursResult.reason === 'json'
1301
+ ? generalTranslations.jsonErrors.businessHours
1302
+ : generalTranslations.businessHours.error.rows,
1303
+ businessHoursRowErrors: !businessHoursResult.ok && businessHoursResult.reason === 'rows'
1304
+ ? businessHoursResult.rowErrors
1305
+ : undefined,
1306
+ escalationTiers: escalationTiersResult.ok ? undefined : generalTranslations.jsonErrors.escalationTiers,
1307
+ escalationTierRowErrors: escalationTiersResult.ok ? undefined : escalationTiersResult.rowErrors,
1308
+ })
1309
+ return
1310
+ }
1311
+
1312
+ if (
1313
+ !Number.isInteger(slaHours) ||
1314
+ slaHours < 1 ||
1315
+ slaHours > 8760 ||
1316
+ !Number.isInteger(slaAtRiskThresholdPct) ||
1317
+ slaAtRiskThresholdPct < 1 ||
1318
+ slaAtRiskThresholdPct > 100 ||
1319
+ (autoApproveMaxAmount !== null && (!Number.isFinite(autoApproveMaxAmount) || autoApproveMaxAmount < 0)) ||
1320
+ (defaultWarrantyMonths !== null && (!Number.isInteger(defaultWarrantyMonths) || defaultWarrantyMonths < 0 || defaultWarrantyMonths > 600)) ||
1321
+ (returnWindowDays !== null && (!Number.isInteger(returnWindowDays) || returnWindowDays < 1 || returnWindowDays > 3650)) ||
1322
+ (currencyCode !== null && !/^[A-Z]{3}$/.test(currencyCode))
1323
+ ) {
1324
+ setGeneralSaveError(generalTranslations.invalidError)
1325
+ return
1326
+ }
1327
+
1328
+ const payload = {
1329
+ slaHours,
1330
+ slaPauseOnInfoRequested: generalForm.slaPauseOnInfoRequested,
1331
+ slaAtRiskThresholdPct,
1332
+ autoApproveEnabled: generalForm.autoApproveEnabled,
1333
+ autoApproveMaxAmount,
1334
+ autoApproveCurrencyCode: currencyCode,
1335
+ autoApproveRequireInWarranty: generalForm.autoApproveRequireInWarranty,
1336
+ defaultWarrantyMonths,
1337
+ returnWindowDays,
1338
+ businessHours: businessHoursResult.value,
1339
+ escalationTiers: escalationTiersResult.value,
1340
+ adjudicationUseRules: generalForm.adjudicationUseRules,
1341
+ quarantineGrades,
1342
+ returnLabelProvider,
1343
+ }
1344
+
1345
+ setGeneralSaving(true)
1346
+ try {
1347
+ await runMutation({
1348
+ operation: async () => {
1349
+ const save = () => apiCall<GeneralSettingsEnvelope>('/api/warranty_claims/settings-general', {
1350
+ method: 'PUT',
1351
+ headers: { 'content-type': 'application/json' },
1352
+ body: JSON.stringify(payload),
1353
+ })
1354
+ const updatedAt = generalSettings?.updatedAt ?? null
1355
+ const call = updatedAt
1356
+ ? await withScopedApiRequestHeaders(buildOptimisticLockHeader(updatedAt), save)
1357
+ : await save()
1358
+ if (!call.ok) throw buildConflictError(call, generalTranslations.saveError, t)
1359
+ const settings = call.result?.result ?? null
1360
+ if (!settings) throw new Error(generalTranslations.saveError)
1361
+ return settings
1362
+ },
1363
+ context: generalMutationContext,
1364
+ mutationPayload: payload,
1365
+ })
1366
+ flash(generalTranslations.success, 'success')
1367
+ await loadGeneralSettings()
1368
+ } catch (err) {
1369
+ if (surfaceRecordConflict(err, t, { onRefresh: () => { void loadGeneralSettings() } })) return
1370
+ const message = err instanceof Error ? err.message : generalTranslations.saveError
1371
+ setGeneralSaveError(message)
1372
+ flash(message, 'error')
1373
+ } finally {
1374
+ setGeneralSaving(false)
1375
+ }
1376
+ }, [
1377
+ generalForm,
1378
+ generalMutationContext,
1379
+ generalSettings?.updatedAt,
1380
+ generalTranslations.businessHours,
1381
+ generalTranslations.escalationTiers,
1382
+ generalTranslations.invalidError,
1383
+ generalTranslations.jsonErrors.businessHours,
1384
+ generalTranslations.jsonErrors.escalationTiers,
1385
+ generalTranslations.saveError,
1386
+ generalTranslations.success,
1387
+ loadGeneralSettings,
1388
+ runMutation,
1389
+ t,
1390
+ ])
1391
+
1392
+ React.useEffect(() => {
1393
+ void loadGeneralSettings()
1394
+ }, [loadGeneralSettings, scopeVersion])
1395
+
1396
+ const loadEntries = React.useCallback(async (section: SectionDefinition) => {
1397
+ setLoadingKind((prev) => ({ ...prev, [section.kind]: true }))
1398
+ try {
1399
+ const dictionaries = await readApiResultOrThrow<{ items?: DictionaryListItem[] }>(
1400
+ '/api/dictionaries',
1401
+ undefined,
1402
+ {
1403
+ fallback: { items: [] },
1404
+ errorMessage: t('warranty_claims.settings.error.load'),
1405
+ },
1406
+ )
1407
+ const dictionary = (dictionaries.items ?? []).find((item) => item.key === section.dictionaryKey)
1408
+ const dictionaryId = dictionary?.id ?? null
1409
+ setDictionaryIds((prev) => ({ ...prev, [section.kind]: dictionaryId }))
1410
+ if (!dictionaryId) {
1411
+ setEntriesByKind((prev) => ({ ...prev, [section.kind]: [] }))
1412
+ return
1413
+ }
1414
+ const entries = await readApiResultOrThrow<{ items?: unknown[] }>(
1415
+ `/api/dictionaries/${encodeURIComponent(dictionaryId)}/entries`,
1416
+ undefined,
1417
+ {
1418
+ fallback: { items: [] },
1419
+ errorMessage: t('warranty_claims.settings.error.load'),
1420
+ },
1421
+ )
1422
+ setEntriesByKind((prev) => ({
1423
+ ...prev,
1424
+ [section.kind]: (entries.items ?? [])
1425
+ .map(normalizeEntry)
1426
+ .filter((entry): entry is DictionaryTableEntry => entry !== null),
1427
+ }))
1428
+ } catch (err) {
1429
+ const message = err instanceof Error ? err.message : t('warranty_claims.settings.error.load')
1430
+ flash(message, 'error')
1431
+ } finally {
1432
+ setLoadingKind((prev) => ({ ...prev, [section.kind]: false }))
1433
+ }
1434
+ }, [t])
1435
+
1436
+ React.useEffect(() => {
1437
+ for (const section of SECTIONS) {
1438
+ void loadEntries(section)
1439
+ }
1440
+ }, [loadEntries, scopeVersion])
1441
+
1442
+ const tableTranslations = React.useMemo(() => ({
1443
+ valueColumn: t('warranty_claims.settings.table.value'),
1444
+ labelColumn: t('warranty_claims.settings.table.label'),
1445
+ appearanceColumn: t('warranty_claims.settings.table.appearance'),
1446
+ addLabel: t('warranty_claims.settings.actions.add'),
1447
+ editLabel: t('warranty_claims.settings.actions.edit'),
1448
+ deleteLabel: t('warranty_claims.settings.actions.delete'),
1449
+ refreshLabel: t('warranty_claims.settings.actions.refresh'),
1450
+ inheritedLabel: t('warranty_claims.settings.table.inherited'),
1451
+ inheritedTooltip: t('warranty_claims.settings.table.inheritedTooltip'),
1452
+ emptyLabel: t('warranty_claims.settings.table.empty'),
1453
+ searchPlaceholder: t('warranty_claims.settings.table.search'),
1454
+ }), [t])
1455
+
1456
+ const formTranslations = React.useMemo(() => ({
1457
+ createTitle: t('warranty_claims.settings.dialog.createTitle'),
1458
+ editTitle: t('warranty_claims.settings.dialog.editTitle'),
1459
+ valueLabel: t('warranty_claims.settings.dialog.valueLabel'),
1460
+ labelLabel: t('warranty_claims.settings.dialog.labelLabel'),
1461
+ saveLabel: t('warranty_claims.settings.dialog.save'),
1462
+ cancelLabel: t('warranty_claims.settings.dialog.cancel'),
1463
+ appearance: {
1464
+ colorLabel: t('warranty_claims.settings.dialog.colorLabel'),
1465
+ colorHelp: t('warranty_claims.settings.dialog.colorHelp'),
1466
+ colorClearLabel: t('warranty_claims.settings.dialog.colorClear'),
1467
+ iconLabel: t('warranty_claims.settings.dialog.iconLabel'),
1468
+ iconPlaceholder: t('warranty_claims.settings.dialog.iconPlaceholder'),
1469
+ iconPickerTriggerLabel: t('warranty_claims.settings.dialog.iconBrowse'),
1470
+ iconSearchPlaceholder: t('warranty_claims.settings.dialog.iconSearchPlaceholder'),
1471
+ iconSearchEmptyLabel: t('warranty_claims.settings.dialog.iconSearchEmpty'),
1472
+ iconSuggestionsLabel: t('warranty_claims.settings.dialog.iconSuggestions'),
1473
+ iconClearLabel: t('warranty_claims.settings.dialog.iconClear'),
1474
+ previewEmptyLabel: t('warranty_claims.settings.dialog.previewEmpty'),
1475
+ },
1476
+ }), [t])
1477
+
1478
+ const closeDialog = React.useCallback(() => {
1479
+ setDialog(null)
1480
+ }, [])
1481
+
1482
+ const startCreate = React.useCallback((kind: WarrantyDictionaryKind) => {
1483
+ setDialog({ mode: 'create', kind })
1484
+ }, [])
1485
+
1486
+ // The table receives entries whose label is localized for display, so edit and
1487
+ // delete must map back to the stored row before touching it.
1488
+ const resolveRawEntry = React.useCallback((
1489
+ kind: WarrantyDictionaryKind,
1490
+ entry: DictionaryTableEntry,
1491
+ ): DictionaryTableEntry => (entriesByKind[kind] ?? []).find((row) => row.id === entry.id) ?? entry,
1492
+ [entriesByKind])
1493
+
1494
+ const startEdit = React.useCallback((kind: WarrantyDictionaryKind, entry: DictionaryTableEntry) => {
1495
+ setDialog({ mode: 'edit', kind, entry })
1496
+ }, [])
1497
+
1498
+ const sectionByKind = React.useCallback((kind: WarrantyDictionaryKind) => {
1499
+ return SECTIONS.find((section) => section.kind === kind) ?? null
1500
+ }, [])
1501
+
1502
+ const deleteEntry = React.useCallback(async (kind: WarrantyDictionaryKind, entry: DictionaryTableEntry) => {
1503
+ const section = sectionByKind(kind)
1504
+ const dictionaryId = dictionaryIds[kind]
1505
+ if (!section || !dictionaryId) return
1506
+ const confirmed = await confirm({
1507
+ title: t('warranty_claims.settings.confirm.delete', undefined, { value: entry.label || entry.value }),
1508
+ variant: 'destructive',
1509
+ })
1510
+ if (!confirmed) return
1511
+ try {
1512
+ let conflictSurfaced = false
1513
+ await runMutation({
1514
+ operation: async () => {
1515
+ const call = await withScopedApiRequestHeaders(
1516
+ buildOptimisticLockHeader(entry.updatedAt),
1517
+ () => apiCall(`/api/dictionaries/${encodeURIComponent(dictionaryId)}/entries/${encodeURIComponent(entry.id)}`, {
1518
+ method: 'DELETE',
1519
+ }),
1520
+ )
1521
+ if (!call.ok) {
1522
+ const errorObject = buildConflictError(call, t('warranty_claims.settings.error.save'))
1523
+ if (surfaceRecordConflict(errorObject, t, { onRefresh: () => { void loadEntries(section) } })) {
1524
+ conflictSurfaced = true
1525
+ return call
1526
+ }
1527
+ await raiseCrudError(call.response, t('warranty_claims.settings.error.delete'))
1528
+ }
1529
+ return call
1530
+ },
1531
+ context: mutationContext,
1532
+ mutationPayload: { action: 'delete', kind, id: entry.id },
1533
+ })
1534
+ if (conflictSurfaced) return
1535
+ flash(t('warranty_claims.settings.success.delete'), 'success')
1536
+ await loadEntries(section)
1537
+ } catch (err) {
1538
+ if (surfaceRecordConflict(err, t, { onRefresh: () => { void loadEntries(section) } })) return
1539
+ const message = err instanceof Error ? err.message : t('warranty_claims.settings.error.delete')
1540
+ flash(message, 'error')
1541
+ }
1542
+ }, [confirm, dictionaryIds, loadEntries, mutationContext, runMutation, sectionByKind, t])
1543
+
1544
+ const submitForm = React.useCallback(async (values: DictionaryFormValues) => {
1545
+ if (!dialog) return
1546
+ const section = sectionByKind(dialog.kind)
1547
+ const dictionaryId = dictionaryIds[dialog.kind]
1548
+ if (!section || !dictionaryId) return
1549
+ setSubmitting(true)
1550
+ try {
1551
+ if (dialog.mode === 'create') {
1552
+ await runMutation({
1553
+ operation: async () => {
1554
+ const call = await apiCall(`/api/dictionaries/${encodeURIComponent(dictionaryId)}/entries`, {
1555
+ method: 'POST',
1556
+ headers: { 'content-type': 'application/json' },
1557
+ body: JSON.stringify(values),
1558
+ })
1559
+ if (!call.ok) await raiseCrudError(call.response, t('warranty_claims.settings.error.save'))
1560
+ return call
1561
+ },
1562
+ context: mutationContext,
1563
+ mutationPayload: { action: 'create', kind: dialog.kind, ...values },
1564
+ })
1565
+ } else {
1566
+ const entry = dialog.entry
1567
+ const payload: Record<string, unknown> = {}
1568
+ if (values.value !== entry.value) payload.value = values.value
1569
+ if (values.label !== entry.label) payload.label = values.label
1570
+ if ((values.color ?? null) !== (entry.color ?? null)) payload.color = values.color ?? null
1571
+ if ((values.icon ?? null) !== (entry.icon ?? null)) payload.icon = values.icon ?? null
1572
+ let conflictSurfaced = false
1573
+ await runMutation({
1574
+ operation: async () => {
1575
+ const call = await withScopedApiRequestHeaders(
1576
+ buildOptimisticLockHeader(entry.updatedAt),
1577
+ () => apiCall(`/api/dictionaries/${encodeURIComponent(dictionaryId)}/entries/${encodeURIComponent(entry.id)}`, {
1578
+ method: 'PATCH',
1579
+ headers: { 'content-type': 'application/json' },
1580
+ body: JSON.stringify(payload),
1581
+ }),
1582
+ )
1583
+ if (!call.ok) {
1584
+ const errorObject = buildConflictError(call, t('warranty_claims.settings.error.save'))
1585
+ if (surfaceRecordConflict(errorObject, t, { onRefresh: () => { void loadEntries(section) } })) {
1586
+ conflictSurfaced = true
1587
+ return call
1588
+ }
1589
+ await raiseCrudError(call.response, t('warranty_claims.settings.error.save'))
1590
+ }
1591
+ return call
1592
+ },
1593
+ context: mutationContext,
1594
+ mutationPayload: { action: 'update', kind: dialog.kind, id: entry.id, ...payload },
1595
+ })
1596
+ if (conflictSurfaced) return
1597
+ }
1598
+ flash(t('warranty_claims.settings.success.save'), 'success')
1599
+ closeDialog()
1600
+ await loadEntries(section)
1601
+ } catch (err) {
1602
+ if (surfaceRecordConflict(err, t, { onRefresh: () => { void loadEntries(section) } })) return
1603
+ const message = err instanceof Error ? err.message : t('warranty_claims.settings.error.save')
1604
+ flash(message, 'error')
1605
+ throw err instanceof Error ? err : new Error(message)
1606
+ } finally {
1607
+ setSubmitting(false)
1608
+ }
1609
+ }, [closeDialog, dialog, dictionaryIds, loadEntries, mutationContext, runMutation, sectionByKind, t])
1610
+
1611
+ const currentValues = React.useMemo<DictionaryFormValues>(() => {
1612
+ if (dialog?.mode === 'edit') {
1613
+ return {
1614
+ value: dialog.entry.value,
1615
+ label: dialog.entry.label,
1616
+ color: dialog.entry.color,
1617
+ icon: dialog.entry.icon,
1618
+ }
1619
+ }
1620
+ return DEFAULT_FORM_VALUES
1621
+ }, [dialog])
1622
+
1623
+ return (
1624
+ <Page>
1625
+ <PageBody>
1626
+ <div className="overflow-hidden rounded-xl border border-border bg-card text-card-foreground">
1627
+ <div className="border-b border-border px-7 py-4">
1628
+ <h1 className="text-xl font-semibold text-foreground">{t('warranty_claims.settings.title', 'Claim settings')}</h1>
1629
+ </div>
1630
+ <section>
1631
+ <div className="space-y-1 border-b border-border px-6 py-4">
1632
+ <h2 className="text-lg font-medium">{generalTranslations.title}</h2>
1633
+ <p className="text-sm text-muted-foreground">{generalTranslations.description}</p>
1634
+ </div>
1635
+ <div className="px-6 py-4">
1636
+ {generalLoading ? (
1637
+ <LoadingMessage label={generalTranslations.loading} />
1638
+ ) : generalLoadError ? (
1639
+ <ErrorMessage
1640
+ label={generalLoadError}
1641
+ action={(
1642
+ <Button
1643
+ type="button"
1644
+ variant="outline"
1645
+ size="sm"
1646
+ onClick={() => { void loadGeneralSettings() }}
1647
+ >
1648
+ <RefreshCw className="size-4" aria-hidden="true" />
1649
+ {generalTranslations.refresh}
1650
+ </Button>
1651
+ )}
1652
+ />
1653
+ ) : (
1654
+ <form id={GENERAL_SETTINGS_FORM_ID} className="space-y-6" noValidate onSubmit={handleGeneralSubmit}>
1655
+ {generalSaveError ? <ErrorMessage label={generalSaveError} /> : null}
1656
+ <div className="grid gap-4 md:grid-cols-2">
1657
+ <GeneralNumberField
1658
+ id="warranty-claims-sla-hours"
1659
+ label={generalTranslations.fields.slaHours}
1660
+ description={generalTranslations.fields.slaHoursHelp}
1661
+ value={generalForm.slaHours}
1662
+ min={1}
1663
+ max={8760}
1664
+ disabled={generalSaving}
1665
+ onChange={(value) => updateGeneralForm({ slaHours: value })}
1666
+ />
1667
+ <GeneralNumberField
1668
+ id="warranty-claims-sla-threshold"
1669
+ label={generalTranslations.fields.slaAtRiskThresholdPct}
1670
+ description={generalTranslations.fields.slaAtRiskThresholdPctHelp}
1671
+ value={generalForm.slaAtRiskThresholdPct}
1672
+ min={1}
1673
+ max={100}
1674
+ disabled={generalSaving}
1675
+ onChange={(value) => updateGeneralForm({ slaAtRiskThresholdPct: value })}
1676
+ />
1677
+ <GeneralNumberField
1678
+ id="warranty-claims-default-warranty-months"
1679
+ label={generalTranslations.fields.defaultWarrantyMonths}
1680
+ description={generalTranslations.fields.defaultWarrantyMonthsHelp}
1681
+ value={generalForm.defaultWarrantyMonths}
1682
+ min={0}
1683
+ max={600}
1684
+ disabled={generalSaving}
1685
+ onChange={(value) => updateGeneralForm({ defaultWarrantyMonths: value })}
1686
+ />
1687
+ <GeneralNumberField
1688
+ id="warranty-claims-return-window-days"
1689
+ label={generalTranslations.fields.returnWindowDays}
1690
+ description={generalTranslations.fields.returnWindowDaysHelp}
1691
+ value={generalForm.returnWindowDays}
1692
+ min={1}
1693
+ max={3650}
1694
+ disabled={generalSaving}
1695
+ onChange={(value) => updateGeneralForm({ returnWindowDays: value })}
1696
+ />
1697
+ <GeneralSwitchField
1698
+ id="warranty-claims-sla-pause"
1699
+ label={generalTranslations.fields.slaPauseOnInfoRequested}
1700
+ description={generalTranslations.fields.slaPauseOnInfoRequestedHelp}
1701
+ checked={generalForm.slaPauseOnInfoRequested}
1702
+ disabled={generalSaving}
1703
+ onCheckedChange={(checked) => updateGeneralForm({ slaPauseOnInfoRequested: checked })}
1704
+ />
1705
+ </div>
1706
+
1707
+ <GeneralSettingsSubsection title={generalTranslations.sections.slaEscalation}>
1708
+ <div className="space-y-4">
1709
+ <BusinessHoursField
1710
+ id="warranty-claims-business-hours"
1711
+ label={generalTranslations.fields.businessHours}
1712
+ description={generalTranslations.fields.businessHoursHelp}
1713
+ value={generalForm.businessHours}
1714
+ rowErrors={generalFieldErrors.businessHoursRowErrors}
1715
+ error={generalFieldErrors.businessHours}
1716
+ disabled={generalSaving}
1717
+ translations={generalTranslations.businessHours}
1718
+ onChange={updateBusinessHours}
1719
+ />
1720
+ <EscalationTiersField
1721
+ id="warranty-claims-escalation-tiers"
1722
+ label={generalTranslations.fields.escalationTiers}
1723
+ description={generalTranslations.fields.escalationTiersHelp}
1724
+ rows={generalForm.escalationTiers}
1725
+ rowErrors={generalFieldErrors.escalationTierRowErrors}
1726
+ error={generalFieldErrors.escalationTiers}
1727
+ disabled={generalSaving}
1728
+ translations={generalTranslations.escalationTiers}
1729
+ loadStaffOptions={loadEscalationStaffOptions}
1730
+ resolveStaffLabel={resolveStaffUserLabel}
1731
+ onAddRow={addEscalationTierRow}
1732
+ onRemoveRow={removeEscalationTierRow}
1733
+ onAtPctChange={updateEscalationTierAtPct}
1734
+ onActionChange={updateEscalationTierAction}
1735
+ onUserChange={updateEscalationTierUser}
1736
+ />
1737
+ </div>
1738
+ </GeneralSettingsSubsection>
1739
+
1740
+ <GeneralSettingsSubsection title={generalTranslations.sections.adjudication}>
1741
+ <GeneralSwitchField
1742
+ id="warranty-claims-adjudication-use-rules"
1743
+ label={generalTranslations.fields.adjudicationUseRules}
1744
+ description={generalTranslations.fields.adjudicationUseRulesHelp}
1745
+ checked={generalForm.adjudicationUseRules}
1746
+ disabled={generalSaving}
1747
+ onCheckedChange={(checked) => updateGeneralForm({ adjudicationUseRules: checked })}
1748
+ />
1749
+ </GeneralSettingsSubsection>
1750
+
1751
+ <div className="grid gap-4 md:grid-cols-2">
1752
+ <GeneralSwitchField
1753
+ id="warranty-claims-auto-approve-enabled"
1754
+ label={generalTranslations.fields.autoApproveEnabled}
1755
+ description={generalTranslations.fields.autoApproveEnabledHelp}
1756
+ checked={generalForm.autoApproveEnabled}
1757
+ disabled={generalSaving}
1758
+ onCheckedChange={(checked) => updateGeneralForm({ autoApproveEnabled: checked })}
1759
+ />
1760
+ <GeneralSwitchField
1761
+ id="warranty-claims-auto-approve-require-warranty"
1762
+ label={generalTranslations.fields.autoApproveRequireInWarranty}
1763
+ description={generalTranslations.fields.autoApproveRequireInWarrantyHelp}
1764
+ checked={generalForm.autoApproveRequireInWarranty}
1765
+ disabled={generalSaving || !generalForm.autoApproveEnabled}
1766
+ onCheckedChange={(checked) => updateGeneralForm({ autoApproveRequireInWarranty: checked })}
1767
+ />
1768
+ </div>
1769
+
1770
+ <div className="grid gap-4 md:grid-cols-2">
1771
+ <GeneralNumberField
1772
+ id="warranty-claims-auto-approve-max-amount"
1773
+ label={generalTranslations.fields.autoApproveMaxAmount}
1774
+ description={generalTranslations.fields.autoApproveMaxAmountHelp}
1775
+ value={generalForm.autoApproveMaxAmount}
1776
+ min={0}
1777
+ step="0.01"
1778
+ disabled={generalSaving || !generalForm.autoApproveEnabled}
1779
+ onChange={(value) => updateGeneralForm({ autoApproveMaxAmount: value })}
1780
+ />
1781
+ <GeneralTextField
1782
+ id="warranty-claims-auto-approve-currency"
1783
+ label={generalTranslations.fields.autoApproveCurrencyCode}
1784
+ description={generalTranslations.fields.autoApproveCurrencyCodeHelp}
1785
+ value={generalForm.autoApproveCurrencyCode}
1786
+ maxLength={3}
1787
+ disabled={generalSaving || !generalForm.autoApproveEnabled}
1788
+ onChange={(value) => updateGeneralForm({ autoApproveCurrencyCode: normalizeCurrencyCodeInput(value) })}
1789
+ />
1790
+ </div>
1791
+
1792
+ <GeneralSettingsSubsection title={generalTranslations.sections.receiving}>
1793
+ <GeneralQuarantineGradesField
1794
+ id="warranty-claims-quarantine-grades"
1795
+ label={generalTranslations.fields.quarantineGrades}
1796
+ description={generalTranslations.fields.quarantineGradesHelp}
1797
+ selected={generalForm.quarantineGrades}
1798
+ disabled={generalSaving}
1799
+ onToggle={(grade, checked) => updateGeneralForm({
1800
+ quarantineGrades: checked
1801
+ ? Array.from(new Set([...generalForm.quarantineGrades, grade]))
1802
+ : generalForm.quarantineGrades.filter((item) => item !== grade),
1803
+ })}
1804
+ />
1805
+ </GeneralSettingsSubsection>
1806
+
1807
+ <GeneralSettingsSubsection title={generalTranslations.sections.returns}>
1808
+ <div className="grid gap-4 md:grid-cols-2">
1809
+ <GeneralTextField
1810
+ id="warranty-claims-return-label-provider"
1811
+ label={generalTranslations.fields.returnLabelProvider}
1812
+ description={generalTranslations.fields.returnLabelProviderHelp}
1813
+ value={generalForm.returnLabelProvider}
1814
+ maxLength={120}
1815
+ disabled={generalSaving}
1816
+ onChange={(value) => updateGeneralForm({ returnLabelProvider: value })}
1817
+ />
1818
+ </div>
1819
+ </GeneralSettingsSubsection>
1820
+
1821
+ <div className="flex flex-wrap justify-end gap-2 border-t border-border pt-4">
1822
+ <Button
1823
+ type="button"
1824
+ variant="outline"
1825
+ disabled={generalSaving}
1826
+ onClick={() => { void loadGeneralSettings() }}
1827
+ >
1828
+ <RefreshCw className="size-4" aria-hidden="true" />
1829
+ {generalTranslations.refresh}
1830
+ </Button>
1831
+ <Button type="submit" disabled={generalSaving}>
1832
+ <Save className="size-4" aria-hidden="true" />
1833
+ {generalSaving ? generalTranslations.saving : generalTranslations.save}
1834
+ </Button>
1835
+ </div>
1836
+ </form>
1837
+ )}
1838
+ </div>
1839
+ </section>
1840
+
1841
+ {SECTIONS.map((section) => (
1842
+ <section key={section.kind} className="border-t border-border bg-card text-card-foreground">
1843
+ <div className="space-y-1 border-b border-border px-6 py-4">
1844
+ <h2 className="text-lg font-medium">{t(section.titleKey)}</h2>
1845
+ <p className="text-sm text-muted-foreground">{t(section.descriptionKey)}</p>
1846
+ </div>
1847
+ <div className="px-2 py-4 sm:px-4">
1848
+ <DictionaryTable
1849
+ entries={(entriesByKind[section.kind] ?? []).map((entry) => ({
1850
+ ...entry,
1851
+ label: localizeDictionaryLabel(t, DICTIONARY_LABEL_KINDS[section.kind], entry.value, entry.label),
1852
+ }))}
1853
+ loading={loadingKind[section.kind] ?? false}
1854
+ canManage
1855
+ onCreate={() => startCreate(section.kind)}
1856
+ onEdit={(entry) => startEdit(section.kind, resolveRawEntry(section.kind, entry))}
1857
+ onDelete={(entry) => { void deleteEntry(section.kind, resolveRawEntry(section.kind, entry)) }}
1858
+ onRefresh={() => { void loadEntries(section) }}
1859
+ translations={{ ...tableTranslations, title: t(section.titleKey) }}
1860
+ />
1861
+ </div>
1862
+ </section>
1863
+ ))}
1864
+ </div>
1865
+ </PageBody>
1866
+
1867
+ <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>
1868
+ <DialogContent
1869
+ className="max-w-lg"
1870
+ onKeyDown={(event) => {
1871
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
1872
+ event.preventDefault()
1873
+ event.currentTarget.querySelector('form')?.requestSubmit()
1874
+ }
1875
+ }}
1876
+ >
1877
+ <DialogHeader>
1878
+ <DialogTitle>
1879
+ {dialog?.mode === 'edit' ? formTranslations.editTitle : formTranslations.createTitle}
1880
+ </DialogTitle>
1881
+ </DialogHeader>
1882
+ <DictionaryForm
1883
+ mode={dialog?.mode === 'edit' ? 'edit' : 'create'}
1884
+ initialValues={currentValues}
1885
+ onSubmit={submitForm}
1886
+ onCancel={closeDialog}
1887
+ submitting={submitting}
1888
+ translations={{
1889
+ title: dialog?.mode === 'edit' ? formTranslations.editTitle : formTranslations.createTitle,
1890
+ valueLabel: formTranslations.valueLabel,
1891
+ labelLabel: formTranslations.labelLabel,
1892
+ saveLabel: formTranslations.saveLabel,
1893
+ cancelLabel: formTranslations.cancelLabel,
1894
+ appearance: formTranslations.appearance,
1895
+ }}
1896
+ iconSuggestions={ICON_SUGGESTIONS}
1897
+ />
1898
+ </DialogContent>
1899
+ </Dialog>
1900
+ {ConfirmDialogElement}
1901
+ </Page>
1902
+ )
1903
+ }