@endora-commerce/mod-catalog 0.100.0

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 (443) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/admin/components/PackagingUnitsEditor.d.ts +12 -0
  4. package/dist/admin/components/PackagingUnitsEditor.d.ts.map +1 -0
  5. package/dist/admin/components/PackagingUnitsEditor.js +99 -0
  6. package/dist/admin/components/PackagingUnitsEditor.js.map +1 -0
  7. package/dist/admin/components/ProductAttributesTab.d.ts +13 -0
  8. package/dist/admin/components/ProductAttributesTab.d.ts.map +1 -0
  9. package/dist/admin/components/ProductAttributesTab.js +140 -0
  10. package/dist/admin/components/ProductAttributesTab.js.map +1 -0
  11. package/dist/admin/components/ProductInventoryTab.d.ts +14 -0
  12. package/dist/admin/components/ProductInventoryTab.d.ts.map +1 -0
  13. package/dist/admin/components/ProductInventoryTab.js +284 -0
  14. package/dist/admin/components/ProductInventoryTab.js.map +1 -0
  15. package/dist/admin/components/ProductScopeEditor.d.ts +94 -0
  16. package/dist/admin/components/ProductScopeEditor.d.ts.map +1 -0
  17. package/dist/admin/components/ProductScopeEditor.js +395 -0
  18. package/dist/admin/components/ProductScopeEditor.js.map +1 -0
  19. package/dist/admin/components/ProductsBulkEditDialog.d.ts +11 -0
  20. package/dist/admin/components/ProductsBulkEditDialog.d.ts.map +1 -0
  21. package/dist/admin/components/ProductsBulkEditDialog.js +304 -0
  22. package/dist/admin/components/ProductsBulkEditDialog.js.map +1 -0
  23. package/dist/admin/index.d.ts +45 -0
  24. package/dist/admin/index.d.ts.map +1 -0
  25. package/dist/admin/index.js +184 -0
  26. package/dist/admin/index.js.map +1 -0
  27. package/dist/admin/lib/resolve-product-selection.d.ts +21 -0
  28. package/dist/admin/lib/resolve-product-selection.d.ts.map +1 -0
  29. package/dist/admin/lib/resolve-product-selection.js +25 -0
  30. package/dist/admin/lib/resolve-product-selection.js.map +1 -0
  31. package/dist/admin/pages/AttachmentTypesPage.d.ts +18 -0
  32. package/dist/admin/pages/AttachmentTypesPage.d.ts.map +1 -0
  33. package/dist/admin/pages/AttachmentTypesPage.js +92 -0
  34. package/dist/admin/pages/AttachmentTypesPage.js.map +1 -0
  35. package/dist/admin/pages/AttributeSetsPage.d.ts +21 -0
  36. package/dist/admin/pages/AttributeSetsPage.d.ts.map +1 -0
  37. package/dist/admin/pages/AttributeSetsPage.js +158 -0
  38. package/dist/admin/pages/AttributeSetsPage.js.map +1 -0
  39. package/dist/admin/pages/AttributesManager.d.ts +9 -0
  40. package/dist/admin/pages/AttributesManager.d.ts.map +1 -0
  41. package/dist/admin/pages/AttributesManager.js +305 -0
  42. package/dist/admin/pages/AttributesManager.js.map +1 -0
  43. package/dist/admin/pages/BulkOperationDetailPage.d.ts +9 -0
  44. package/dist/admin/pages/BulkOperationDetailPage.d.ts.map +1 -0
  45. package/dist/admin/pages/BulkOperationDetailPage.js +187 -0
  46. package/dist/admin/pages/BulkOperationDetailPage.js.map +1 -0
  47. package/dist/admin/pages/BulkOperationsPage.d.ts +9 -0
  48. package/dist/admin/pages/BulkOperationsPage.d.ts.map +1 -0
  49. package/dist/admin/pages/BulkOperationsPage.js +98 -0
  50. package/dist/admin/pages/BulkOperationsPage.js.map +1 -0
  51. package/dist/admin/pages/CategoriesTree.d.ts +9 -0
  52. package/dist/admin/pages/CategoriesTree.d.ts.map +1 -0
  53. package/dist/admin/pages/CategoriesTree.js +176 -0
  54. package/dist/admin/pages/CategoriesTree.js.map +1 -0
  55. package/dist/admin/pages/ProductEditor.d.ts +13 -0
  56. package/dist/admin/pages/ProductEditor.d.ts.map +1 -0
  57. package/dist/admin/pages/ProductEditor.js +1050 -0
  58. package/dist/admin/pages/ProductEditor.js.map +1 -0
  59. package/dist/admin/pages/ProductsList.d.ts +9 -0
  60. package/dist/admin/pages/ProductsList.d.ts.map +1 -0
  61. package/dist/admin/pages/ProductsList.js +399 -0
  62. package/dist/admin/pages/ProductsList.js.map +1 -0
  63. package/dist/backend/commands/attribute-commands.d.ts +111 -0
  64. package/dist/backend/commands/attribute-commands.d.ts.map +1 -0
  65. package/dist/backend/commands/attribute-commands.js +482 -0
  66. package/dist/backend/commands/attribute-commands.js.map +1 -0
  67. package/dist/backend/demo/reset.d.ts +23 -0
  68. package/dist/backend/demo/reset.d.ts.map +1 -0
  69. package/dist/backend/demo/reset.js +41 -0
  70. package/dist/backend/demo/reset.js.map +1 -0
  71. package/dist/backend/demo/rows.d.ts +103 -0
  72. package/dist/backend/demo/rows.d.ts.map +1 -0
  73. package/dist/backend/demo/rows.js +162 -0
  74. package/dist/backend/demo/rows.js.map +1 -0
  75. package/dist/backend/demo/seed.d.ts +35 -0
  76. package/dist/backend/demo/seed.d.ts.map +1 -0
  77. package/dist/backend/demo/seed.js +177 -0
  78. package/dist/backend/demo/seed.js.map +1 -0
  79. package/dist/backend/entities/attachment-type.entity.d.ts +16 -0
  80. package/dist/backend/entities/attachment-type.entity.d.ts.map +1 -0
  81. package/dist/backend/entities/attachment-type.entity.js +57 -0
  82. package/dist/backend/entities/attachment-type.entity.js.map +1 -0
  83. package/dist/backend/entities/attribute-set-attribute.entity.d.ts +26 -0
  84. package/dist/backend/entities/attribute-set-attribute.entity.d.ts.map +1 -0
  85. package/dist/backend/entities/attribute-set-attribute.entity.js +55 -0
  86. package/dist/backend/entities/attribute-set-attribute.entity.js.map +1 -0
  87. package/dist/backend/entities/attribute-set.entity.d.ts +22 -0
  88. package/dist/backend/entities/attribute-set.entity.d.ts.map +1 -0
  89. package/dist/backend/entities/attribute-set.entity.js +67 -0
  90. package/dist/backend/entities/attribute-set.entity.js.map +1 -0
  91. package/dist/backend/entities/bulk-operation.entity.d.ts +78 -0
  92. package/dist/backend/entities/bulk-operation.entity.d.ts.map +1 -0
  93. package/dist/backend/entities/bulk-operation.entity.js +138 -0
  94. package/dist/backend/entities/bulk-operation.entity.js.map +1 -0
  95. package/dist/backend/entities/bundle-slot-option.entity.d.ts +23 -0
  96. package/dist/backend/entities/bundle-slot-option.entity.d.ts.map +1 -0
  97. package/dist/backend/entities/bundle-slot-option.entity.js +68 -0
  98. package/dist/backend/entities/bundle-slot-option.entity.js.map +1 -0
  99. package/dist/backend/entities/bundle-slot.entity.d.ts +25 -0
  100. package/dist/backend/entities/bundle-slot.entity.d.ts.map +1 -0
  101. package/dist/backend/entities/bundle-slot.entity.js +74 -0
  102. package/dist/backend/entities/bundle-slot.entity.js.map +1 -0
  103. package/dist/backend/entities/category.entity.d.ts +26 -0
  104. package/dist/backend/entities/category.entity.d.ts.map +1 -0
  105. package/dist/backend/entities/category.entity.js +119 -0
  106. package/dist/backend/entities/category.entity.js.map +1 -0
  107. package/dist/backend/entities/gallery-item-label.entity.d.ts +22 -0
  108. package/dist/backend/entities/gallery-item-label.entity.d.ts.map +1 -0
  109. package/dist/backend/entities/gallery-item-label.entity.js +50 -0
  110. package/dist/backend/entities/gallery-item-label.entity.js.map +1 -0
  111. package/dist/backend/entities/gallery-item.entity.d.ts +17 -0
  112. package/dist/backend/entities/gallery-item.entity.d.ts.map +1 -0
  113. package/dist/backend/entities/gallery-item.entity.js +58 -0
  114. package/dist/backend/entities/gallery-item.entity.js.map +1 -0
  115. package/dist/backend/entities/grouped-item.entity.d.ts +23 -0
  116. package/dist/backend/entities/grouped-item.entity.d.ts.map +1 -0
  117. package/dist/backend/entities/grouped-item.entity.js +68 -0
  118. package/dist/backend/entities/grouped-item.entity.js.map +1 -0
  119. package/dist/backend/entities/product-attachment.entity.d.ts +23 -0
  120. package/dist/backend/entities/product-attachment.entity.d.ts.map +1 -0
  121. package/dist/backend/entities/product-attachment.entity.js +77 -0
  122. package/dist/backend/entities/product-attachment.entity.js.map +1 -0
  123. package/dist/backend/entities/product-attribute.entity.d.ts +72 -0
  124. package/dist/backend/entities/product-attribute.entity.d.ts.map +1 -0
  125. package/dist/backend/entities/product-attribute.entity.js +161 -0
  126. package/dist/backend/entities/product-attribute.entity.js.map +1 -0
  127. package/dist/backend/entities/product-editor-preference.entity.d.ts +28 -0
  128. package/dist/backend/entities/product-editor-preference.entity.d.ts.map +1 -0
  129. package/dist/backend/entities/product-editor-preference.entity.js +63 -0
  130. package/dist/backend/entities/product-editor-preference.entity.js.map +1 -0
  131. package/dist/backend/entities/product-link.entity.d.ts +25 -0
  132. package/dist/backend/entities/product-link.entity.d.ts.map +1 -0
  133. package/dist/backend/entities/product-link.entity.js +70 -0
  134. package/dist/backend/entities/product-link.entity.js.map +1 -0
  135. package/dist/backend/entities/product-packaging-unit.entity.d.ts +26 -0
  136. package/dist/backend/entities/product-packaging-unit.entity.d.ts.map +1 -0
  137. package/dist/backend/entities/product-packaging-unit.entity.js +76 -0
  138. package/dist/backend/entities/product-packaging-unit.entity.js.map +1 -0
  139. package/dist/backend/entities/product-value-override.entity.d.ts +43 -0
  140. package/dist/backend/entities/product-value-override.entity.d.ts.map +1 -0
  141. package/dist/backend/entities/product-value-override.entity.js +90 -0
  142. package/dist/backend/entities/product-value-override.entity.js.map +1 -0
  143. package/dist/backend/entities/product-variant.entity.d.ts +18 -0
  144. package/dist/backend/entities/product-variant.entity.d.ts.map +1 -0
  145. package/dist/backend/entities/product-variant.entity.js +68 -0
  146. package/dist/backend/entities/product-variant.entity.js.map +1 -0
  147. package/dist/backend/entities/product.entity.d.ts +108 -0
  148. package/dist/backend/entities/product.entity.d.ts.map +1 -0
  149. package/dist/backend/entities/product.entity.js +230 -0
  150. package/dist/backend/entities/product.entity.js.map +1 -0
  151. package/dist/backend/index.d.ts +262 -0
  152. package/dist/backend/index.d.ts.map +1 -0
  153. package/dist/backend/index.js +642 -0
  154. package/dist/backend/index.js.map +1 -0
  155. package/dist/backend/plugin.d.ts +216 -0
  156. package/dist/backend/plugin.d.ts.map +1 -0
  157. package/dist/backend/plugin.js +223 -0
  158. package/dist/backend/plugin.js.map +1 -0
  159. package/dist/backend/prompt-tools.d.ts +47 -0
  160. package/dist/backend/prompt-tools.d.ts.map +1 -0
  161. package/dist/backend/prompt-tools.js +284 -0
  162. package/dist/backend/prompt-tools.js.map +1 -0
  163. package/dist/backend/routes.admin.d.ts +105 -0
  164. package/dist/backend/routes.admin.d.ts.map +1 -0
  165. package/dist/backend/routes.admin.js +1162 -0
  166. package/dist/backend/routes.admin.js.map +1 -0
  167. package/dist/backend/routes.api-key.d.ts +33 -0
  168. package/dist/backend/routes.api-key.d.ts.map +1 -0
  169. package/dist/backend/routes.api-key.js +43 -0
  170. package/dist/backend/routes.api-key.js.map +1 -0
  171. package/dist/backend/routes.external.d.ts +32 -0
  172. package/dist/backend/routes.external.d.ts.map +1 -0
  173. package/dist/backend/routes.external.js +115 -0
  174. package/dist/backend/routes.external.js.map +1 -0
  175. package/dist/backend/routes.public.d.ts +46 -0
  176. package/dist/backend/routes.public.d.ts.map +1 -0
  177. package/dist/backend/routes.public.js +318 -0
  178. package/dist/backend/routes.public.js.map +1 -0
  179. package/dist/backend/services/asset-references.d.ts +4 -0
  180. package/dist/backend/services/asset-references.d.ts.map +1 -0
  181. package/dist/backend/services/asset-references.js +104 -0
  182. package/dist/backend/services/asset-references.js.map +1 -0
  183. package/dist/backend/services/attachment.service.d.ts +45 -0
  184. package/dist/backend/services/attachment.service.d.ts.map +1 -0
  185. package/dist/backend/services/attachment.service.js +268 -0
  186. package/dist/backend/services/attachment.service.js.map +1 -0
  187. package/dist/backend/services/attribute-option-validator.d.ts +47 -0
  188. package/dist/backend/services/attribute-option-validator.d.ts.map +1 -0
  189. package/dist/backend/services/attribute-option-validator.js +69 -0
  190. package/dist/backend/services/attribute-option-validator.js.map +1 -0
  191. package/dist/backend/services/attribute-set-validations.d.ts +65 -0
  192. package/dist/backend/services/attribute-set-validations.d.ts.map +1 -0
  193. package/dist/backend/services/attribute-set-validations.js +89 -0
  194. package/dist/backend/services/attribute-set-validations.js.map +1 -0
  195. package/dist/backend/services/attribute-set.service.d.ts +50 -0
  196. package/dist/backend/services/attribute-set.service.d.ts.map +1 -0
  197. package/dist/backend/services/attribute-set.service.js +401 -0
  198. package/dist/backend/services/attribute-set.service.js.map +1 -0
  199. package/dist/backend/services/attribute-type-mapping.d.ts +66 -0
  200. package/dist/backend/services/attribute-type-mapping.d.ts.map +1 -0
  201. package/dist/backend/services/attribute-type-mapping.js +121 -0
  202. package/dist/backend/services/attribute-type-mapping.js.map +1 -0
  203. package/dist/backend/services/attribute-value-key.service.d.ts +48 -0
  204. package/dist/backend/services/attribute-value-key.service.d.ts.map +1 -0
  205. package/dist/backend/services/attribute-value-key.service.js +118 -0
  206. package/dist/backend/services/attribute-value-key.service.js.map +1 -0
  207. package/dist/backend/services/audit-references.d.ts +13 -0
  208. package/dist/backend/services/audit-references.d.ts.map +1 -0
  209. package/dist/backend/services/audit-references.js +51 -0
  210. package/dist/backend/services/audit-references.js.map +1 -0
  211. package/dist/backend/services/bulk-operation-queue.d.ts +26 -0
  212. package/dist/backend/services/bulk-operation-queue.d.ts.map +1 -0
  213. package/dist/backend/services/bulk-operation-queue.js +34 -0
  214. package/dist/backend/services/bulk-operation-queue.js.map +1 -0
  215. package/dist/backend/services/bulk-operation.service.d.ts +195 -0
  216. package/dist/backend/services/bulk-operation.service.d.ts.map +1 -0
  217. package/dist/backend/services/bulk-operation.service.js +510 -0
  218. package/dist/backend/services/bulk-operation.service.js.map +1 -0
  219. package/dist/backend/services/bundle.service.d.ts +87 -0
  220. package/dist/backend/services/bundle.service.d.ts.map +1 -0
  221. package/dist/backend/services/bundle.service.js +286 -0
  222. package/dist/backend/services/bundle.service.js.map +1 -0
  223. package/dist/backend/services/catalog-admin.service.d.ts +398 -0
  224. package/dist/backend/services/catalog-admin.service.d.ts.map +1 -0
  225. package/dist/backend/services/catalog-admin.service.js +1516 -0
  226. package/dist/backend/services/catalog-admin.service.js.map +1 -0
  227. package/dist/backend/services/catalog-attribute-read.service.d.ts +78 -0
  228. package/dist/backend/services/catalog-attribute-read.service.d.ts.map +1 -0
  229. package/dist/backend/services/catalog-attribute-read.service.js +216 -0
  230. package/dist/backend/services/catalog-attribute-read.service.js.map +1 -0
  231. package/dist/backend/services/catalog-bulk-import.service.d.ts +30 -0
  232. package/dist/backend/services/catalog-bulk-import.service.d.ts.map +1 -0
  233. package/dist/backend/services/catalog-bulk-import.service.js +136 -0
  234. package/dist/backend/services/catalog-bulk-import.service.js.map +1 -0
  235. package/dist/backend/services/catalog-bulk-update.service.d.ts +91 -0
  236. package/dist/backend/services/catalog-bulk-update.service.d.ts.map +1 -0
  237. package/dist/backend/services/catalog-bulk-update.service.js +330 -0
  238. package/dist/backend/services/catalog-bulk-update.service.js.map +1 -0
  239. package/dist/backend/services/catalog-category-read.service.d.ts +66 -0
  240. package/dist/backend/services/catalog-category-read.service.d.ts.map +1 -0
  241. package/dist/backend/services/catalog-category-read.service.js +248 -0
  242. package/dist/backend/services/catalog-category-read.service.js.map +1 -0
  243. package/dist/backend/services/catalog-org-price-decorator.d.ts +90 -0
  244. package/dist/backend/services/catalog-org-price-decorator.d.ts.map +1 -0
  245. package/dist/backend/services/catalog-org-price-decorator.js +184 -0
  246. package/dist/backend/services/catalog-org-price-decorator.js.map +1 -0
  247. package/dist/backend/services/catalog-product-filter.service.d.ts +82 -0
  248. package/dist/backend/services/catalog-product-filter.service.d.ts.map +1 -0
  249. package/dist/backend/services/catalog-product-filter.service.js +275 -0
  250. package/dist/backend/services/catalog-product-filter.service.js.map +1 -0
  251. package/dist/backend/services/catalog-product-read.service.d.ts +46 -0
  252. package/dist/backend/services/catalog-product-read.service.d.ts.map +1 -0
  253. package/dist/backend/services/catalog-product-read.service.js +197 -0
  254. package/dist/backend/services/catalog-product-read.service.js.map +1 -0
  255. package/dist/backend/services/catalog-query.service.d.ts +444 -0
  256. package/dist/backend/services/catalog-query.service.d.ts.map +1 -0
  257. package/dist/backend/services/catalog-query.service.js +1633 -0
  258. package/dist/backend/services/catalog-query.service.js.map +1 -0
  259. package/dist/backend/services/catalog-quick-search.service.d.ts +78 -0
  260. package/dist/backend/services/catalog-quick-search.service.d.ts.map +1 -0
  261. package/dist/backend/services/catalog-quick-search.service.js +159 -0
  262. package/dist/backend/services/catalog-quick-search.service.js.map +1 -0
  263. package/dist/backend/services/catalog-write-ports.d.ts +25 -0
  264. package/dist/backend/services/catalog-write-ports.d.ts.map +1 -0
  265. package/dist/backend/services/catalog-write-ports.js +63 -0
  266. package/dist/backend/services/catalog-write-ports.js.map +1 -0
  267. package/dist/backend/services/category-admin.service.d.ts +102 -0
  268. package/dist/backend/services/category-admin.service.d.ts.map +1 -0
  269. package/dist/backend/services/category-admin.service.js +271 -0
  270. package/dist/backend/services/category-admin.service.js.map +1 -0
  271. package/dist/backend/services/gallery.service.d.ts +85 -0
  272. package/dist/backend/services/gallery.service.d.ts.map +1 -0
  273. package/dist/backend/services/gallery.service.js +326 -0
  274. package/dist/backend/services/gallery.service.js.map +1 -0
  275. package/dist/backend/services/grouped.service.d.ts +39 -0
  276. package/dist/backend/services/grouped.service.d.ts.map +1 -0
  277. package/dist/backend/services/grouped.service.js +136 -0
  278. package/dist/backend/services/grouped.service.js.map +1 -0
  279. package/dist/backend/services/label-resolver.d.ts +16 -0
  280. package/dist/backend/services/label-resolver.d.ts.map +1 -0
  281. package/dist/backend/services/label-resolver.js +43 -0
  282. package/dist/backend/services/label-resolver.js.map +1 -0
  283. package/dist/backend/services/packaging-unit.service.d.ts +28 -0
  284. package/dist/backend/services/packaging-unit.service.d.ts.map +1 -0
  285. package/dist/backend/services/packaging-unit.service.js +197 -0
  286. package/dist/backend/services/packaging-unit.service.js.map +1 -0
  287. package/dist/backend/services/primary-asset-url.d.ts +4 -0
  288. package/dist/backend/services/primary-asset-url.d.ts.map +1 -0
  289. package/dist/backend/services/primary-asset-url.js +55 -0
  290. package/dist/backend/services/primary-asset-url.js.map +1 -0
  291. package/dist/backend/services/product-editor-preferences.service.d.ts +20 -0
  292. package/dist/backend/services/product-editor-preferences.service.d.ts.map +1 -0
  293. package/dist/backend/services/product-editor-preferences.service.js +44 -0
  294. package/dist/backend/services/product-editor-preferences.service.js.map +1 -0
  295. package/dist/backend/services/product-link.service.d.ts +169 -0
  296. package/dist/backend/services/product-link.service.d.ts.map +1 -0
  297. package/dist/backend/services/product-link.service.js +297 -0
  298. package/dist/backend/services/product-link.service.js.map +1 -0
  299. package/dist/backend/services/product-overrides.service.d.ts +77 -0
  300. package/dist/backend/services/product-overrides.service.d.ts.map +1 -0
  301. package/dist/backend/services/product-overrides.service.js +204 -0
  302. package/dist/backend/services/product-overrides.service.js.map +1 -0
  303. package/dist/backend/services/product-scope-context.service.d.ts +36 -0
  304. package/dist/backend/services/product-scope-context.service.d.ts.map +1 -0
  305. package/dist/backend/services/product-scope-context.service.js +106 -0
  306. package/dist/backend/services/product-scope-context.service.js.map +1 -0
  307. package/dist/backend/services/product-type-validations.d.ts +42 -0
  308. package/dist/backend/services/product-type-validations.d.ts.map +1 -0
  309. package/dist/backend/services/product-type-validations.js +55 -0
  310. package/dist/backend/services/product-type-validations.js.map +1 -0
  311. package/dist/backend/services/product-value-resolver.service.d.ts +59 -0
  312. package/dist/backend/services/product-value-resolver.service.d.ts.map +1 -0
  313. package/dist/backend/services/product-value-resolver.service.js +111 -0
  314. package/dist/backend/services/product-value-resolver.service.js.map +1 -0
  315. package/dist/backend/services/system-attribute-scopes.d.ts +18 -0
  316. package/dist/backend/services/system-attribute-scopes.d.ts.map +1 -0
  317. package/dist/backend/services/system-attribute-scopes.js +18 -0
  318. package/dist/backend/services/system-attribute-scopes.js.map +1 -0
  319. package/dist/backend/services/viewer-organization.d.ts +25 -0
  320. package/dist/backend/services/viewer-organization.d.ts.map +1 -0
  321. package/dist/backend/services/viewer-organization.js +31 -0
  322. package/dist/backend/services/viewer-organization.js.map +1 -0
  323. package/dist/manifest.d.ts +204 -0
  324. package/dist/manifest.d.ts.map +1 -0
  325. package/dist/manifest.js +686 -0
  326. package/dist/manifest.js.map +1 -0
  327. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.d.ts +33 -0
  328. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.d.ts.map +1 -0
  329. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.js +118 -0
  330. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.js.map +1 -0
  331. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.d.ts +24 -0
  332. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.d.ts.map +1 -0
  333. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.js +31 -0
  334. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.js.map +1 -0
  335. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.d.ts +32 -0
  336. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.d.ts.map +1 -0
  337. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.js +58 -0
  338. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.js.map +1 -0
  339. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.d.ts +22 -0
  340. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.d.ts.map +1 -0
  341. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.js +66 -0
  342. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.js.map +1 -0
  343. package/dist/migrations/20260429T112543_catalog_product_attachments.d.ts +18 -0
  344. package/dist/migrations/20260429T112543_catalog_product_attachments.d.ts.map +1 -0
  345. package/dist/migrations/20260429T112543_catalog_product_attachments.js +73 -0
  346. package/dist/migrations/20260429T112543_catalog_product_attachments.js.map +1 -0
  347. package/dist/migrations/20260429T123726_catalog_product_links.d.ts +21 -0
  348. package/dist/migrations/20260429T123726_catalog_product_links.d.ts.map +1 -0
  349. package/dist/migrations/20260429T123726_catalog_product_links.js +48 -0
  350. package/dist/migrations/20260429T123726_catalog_product_links.js.map +1 -0
  351. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.d.ts +23 -0
  352. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.d.ts.map +1 -0
  353. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.js +94 -0
  354. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.js.map +1 -0
  355. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.d.ts +24 -0
  356. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.d.ts.map +1 -0
  357. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.js +31 -0
  358. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.js.map +1 -0
  359. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.d.ts +31 -0
  360. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.d.ts.map +1 -0
  361. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.js +133 -0
  362. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.js.map +1 -0
  363. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.d.ts +16 -0
  364. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.d.ts.map +1 -0
  365. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.js +23 -0
  366. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.js.map +1 -0
  367. package/dist/migrations/20260526T124736_catalog_product_status_inactive.d.ts +10 -0
  368. package/dist/migrations/20260526T124736_catalog_product_status_inactive.d.ts.map +1 -0
  369. package/dist/migrations/20260526T124736_catalog_product_status_inactive.js +22 -0
  370. package/dist/migrations/20260526T124736_catalog_product_status_inactive.js.map +1 -0
  371. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.d.ts +38 -0
  372. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.d.ts.map +1 -0
  373. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.js +98 -0
  374. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.js.map +1 -0
  375. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.d.ts +13 -0
  376. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.d.ts.map +1 -0
  377. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.js +17 -0
  378. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.js.map +1 -0
  379. package/dist/migrations/20260611T140407_catalog_bulk_operations.d.ts +16 -0
  380. package/dist/migrations/20260611T140407_catalog_bulk_operations.d.ts.map +1 -0
  381. package/dist/migrations/20260611T140407_catalog_bulk_operations.js +42 -0
  382. package/dist/migrations/20260611T140407_catalog_bulk_operations.js.map +1 -0
  383. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.d.ts +14 -0
  384. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.d.ts.map +1 -0
  385. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.js +18 -0
  386. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.js.map +1 -0
  387. package/dist/migrations/20260611T140412_catalog_product_packaging_units.d.ts +11 -0
  388. package/dist/migrations/20260611T140412_catalog_product_packaging_units.d.ts.map +1 -0
  389. package/dist/migrations/20260611T140412_catalog_product_packaging_units.js +29 -0
  390. package/dist/migrations/20260611T140412_catalog_product_packaging_units.js.map +1 -0
  391. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.d.ts +23 -0
  392. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.d.ts.map +1 -0
  393. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.js +35 -0
  394. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.js.map +1 -0
  395. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.d.ts +12 -0
  396. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.d.ts.map +1 -0
  397. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.js +16 -0
  398. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.js.map +1 -0
  399. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.d.ts +32 -0
  400. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.d.ts.map +1 -0
  401. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.js +286 -0
  402. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.js.map +1 -0
  403. package/dist/migrations/20260804T152604_catalog_widen_product_sku.d.ts +36 -0
  404. package/dist/migrations/20260804T152604_catalog_widen_product_sku.d.ts.map +1 -0
  405. package/dist/migrations/20260804T152604_catalog_widen_product_sku.js +63 -0
  406. package/dist/migrations/20260804T152604_catalog_widen_product_sku.js.map +1 -0
  407. package/dist/migrations/20260804T160244_catalog_category_activation.d.ts +18 -0
  408. package/dist/migrations/20260804T160244_catalog_category_activation.d.ts.map +1 -0
  409. package/dist/migrations/20260804T160244_catalog_category_activation.js +22 -0
  410. package/dist/migrations/20260804T160244_catalog_category_activation.js.map +1 -0
  411. package/dist/migrations/20260912T094557_catalog_sales_channel_products.d.ts +27 -0
  412. package/dist/migrations/20260912T094557_catalog_sales_channel_products.d.ts.map +1 -0
  413. package/dist/migrations/20260912T094557_catalog_sales_channel_products.js +44 -0
  414. package/dist/migrations/20260912T094557_catalog_sales_channel_products.js.map +1 -0
  415. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.d.ts +27 -0
  416. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.d.ts.map +1 -0
  417. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.js +44 -0
  418. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.js.map +1 -0
  419. package/dist/migrations/20260925T125527_catalog_inventory_columns.d.ts +36 -0
  420. package/dist/migrations/20260925T125527_catalog_inventory_columns.d.ts.map +1 -0
  421. package/dist/migrations/20260925T125527_catalog_inventory_columns.js +87 -0
  422. package/dist/migrations/20260925T125527_catalog_inventory_columns.js.map +1 -0
  423. package/dist/migrations/index.d.ts +55 -0
  424. package/dist/migrations/index.d.ts.map +1 -0
  425. package/dist/migrations/index.js +80 -0
  426. package/dist/migrations/index.js.map +1 -0
  427. package/dist/ports/index.d.ts +96 -0
  428. package/dist/ports/index.d.ts.map +1 -0
  429. package/dist/ports/index.js +2 -0
  430. package/dist/ports/index.js.map +1 -0
  431. package/docs/catalog/attachments.md +69 -0
  432. package/docs/catalog/attribute-sets.md +63 -0
  433. package/docs/catalog/attributes.md +244 -0
  434. package/docs/catalog/composite-products.md +115 -0
  435. package/docs/catalog/gallery-and-labels.md +73 -0
  436. package/docs/catalog/packaging-units.md +67 -0
  437. package/docs/catalog/per-channel-per-language-overrides.md +234 -0
  438. package/docs/catalog/product-links.md +79 -0
  439. package/docs/catalog.md +207 -0
  440. package/i18n/en.json +649 -0
  441. package/i18n/pl.json +649 -0
  442. package/package.json +109 -0
  443. package/tailwind.css +14 -0
@@ -0,0 +1,1050 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
+ import { useNavigate, useParams } from 'react-router-dom';
4
+ import { Trash2, ChevronLeft, Copy, Eye, FileText as FileTextIcon, Globe, GripVertical, Image as ImageIcon, CircleDollarSign, Layers, Link as LinkIcon, Paperclip, Save, SlidersHorizontal, Store as StoreIcon, Warehouse as WarehouseIcon, } from 'lucide-react';
5
+ import { ApiError, apiClient, useUnsavedChangesPrompt, toAbsoluteAssetUrl, cn } from '@endora-commerce/admin-kit/lib';
6
+ import { Alert, AlertDescription, Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Select, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@endora-commerce/admin-kit/ui';
7
+ import { AssetPicker, StickyFormActions, TouchReorderButtons, CategoryTreePicker, ProductPicker } from '@endora-commerce/admin-kit/components';
8
+ import { useTranslation } from '@endora-commerce/admin-kit/i18n';
9
+ import { AdminZone, useAdminZone } from '@endora-commerce/admin-kit/zones';
10
+ import { ProductInventoryTab } from '../components/ProductInventoryTab.js';
11
+ import { PackagingUnitsEditor } from '../components/PackagingUnitsEditor.js';
12
+ import { ProductAttributesTab } from '../components/ProductAttributesTab.js';
13
+ import { ProductScopeEditor, } from '../components/ProductScopeEditor.js';
14
+ const LOCALES = ['en-US', 'pl-PL'];
15
+ const PRODUCT_TYPES = [
16
+ 'simple',
17
+ 'configurable',
18
+ 'grouped',
19
+ 'bundle',
20
+ 'virtual',
21
+ ];
22
+ const VISIBILITIES = ['public', 'logged_in_only', 'organization_restricted'];
23
+ const STATUSES = ['draft', 'active', 'inactive'];
24
+ /**
25
+ * Order-insensitive JSON snapshot of the editable form fields, used to detect
26
+ * unsaved changes (object keys + array members are sorted so re-ordering alone
27
+ * never reads as a change).
28
+ */
29
+ function stableSnapshot(values) {
30
+ return JSON.stringify(values, (_key, value) => {
31
+ if (Array.isArray(value))
32
+ return [...value].sort();
33
+ if (value && typeof value === 'object') {
34
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
35
+ }
36
+ return value;
37
+ });
38
+ }
39
+ export function ProductEditor() {
40
+ const t = useTranslation('catalog');
41
+ const params = useParams();
42
+ const navigate = useNavigate();
43
+ const isNew = params.id === 'new';
44
+ const id = isNew ? null : params.id ?? null;
45
+ // Feature 091 / P7a, Z15 — the Channels tab's button is a count of its own
46
+ // body. Declared here rather than beside the tab table because the table is
47
+ // built below an early return and a hook may not be.
48
+ const channelContributions = useAdminZone('product.editor.channels', {
49
+ productId: id ?? '',
50
+ });
51
+ const [loading, setLoading] = useState(!isNew);
52
+ const [error, setError] = useState(null);
53
+ const [info, setInfo] = useState(null);
54
+ const [categories, setCategories] = useState([]);
55
+ const [sku, setSku] = useState('');
56
+ const [type, setType] = useState('simple');
57
+ const [status, setStatus] = useState('draft');
58
+ const [visibility, setVisibility] = useState('public');
59
+ const [name, setName] = useState({ 'en-US': '', 'pl-PL': '' });
60
+ const [description, setDescription] = useState({
61
+ 'en-US': '',
62
+ 'pl-PL': '',
63
+ });
64
+ const [defaultPrice, setDefaultPrice] = useState('');
65
+ const [categoryIds, setCategoryIds] = useState([]);
66
+ const [activeTab, setActiveTab] = useState('details');
67
+ // Feature 002 (T034) — Attribute Set selector
68
+ const [attributeSets, setAttributeSets] = useState([]);
69
+ const [attributeSetId, setAttributeSetId] = useState('');
70
+ // Editable attribute values keyed by attribute `key` (Attributes tab).
71
+ const [attrValues, setAttrValues] = useState({});
72
+ // Feature 023 — imperative handle on the scope-editor so the page Save
73
+ // can flush every pending channel-scoped override in a single PATCH.
74
+ const scopeEditorRef = useRef(null);
75
+ // Media tab — page-Save flushes staged gallery label/order edits via this
76
+ // imperative handle (mirrors the scope-editor pattern above).
77
+ const galleryRef = useRef(null);
78
+ // Unsaved-changes guard — a stable snapshot of the editable fields is taken
79
+ // on load (and re-taken after each save via refresh()); the form is "dirty"
80
+ // whenever the live values diverge from it.
81
+ const initialSnapshotRef = useRef('');
82
+ const currentSnapshot = stableSnapshot({
83
+ sku,
84
+ type,
85
+ status,
86
+ visibility,
87
+ name,
88
+ description,
89
+ defaultPrice,
90
+ categoryIds,
91
+ attributeSetId,
92
+ attrValues,
93
+ });
94
+ const dirty = !loading &&
95
+ initialSnapshotRef.current !== '' &&
96
+ currentSnapshot !== initialSnapshotRef.current;
97
+ useUnsavedChangesPrompt(dirty);
98
+ const refresh = useCallback(async () => {
99
+ setLoading(true);
100
+ setError(null);
101
+ try {
102
+ const [cats, sets] = await Promise.all([
103
+ apiClient.get('/api/v1/admin/catalog/categories'),
104
+ apiClient.get('/api/v1/admin/catalog/attribute-sets'),
105
+ ]);
106
+ setCategories(cats.data);
107
+ setAttributeSets(sets.data);
108
+ // Pick the system Default as the initial selection for new Products.
109
+ const defaultSet = sets.data.find((s) => s.code === 'default');
110
+ if (isNew && defaultSet)
111
+ setAttributeSetId(defaultSet.id);
112
+ if (isNew) {
113
+ // Baseline for a brand-new product matches the useState defaults.
114
+ initialSnapshotRef.current = stableSnapshot({
115
+ sku: '',
116
+ type: 'simple',
117
+ status: 'draft',
118
+ visibility: 'public',
119
+ name: { 'en-US': '', 'pl-PL': '' },
120
+ description: { 'en-US': '', 'pl-PL': '' },
121
+ defaultPrice: '',
122
+ categoryIds: [],
123
+ attributeSetId: defaultSet?.id ?? '',
124
+ attrValues: {},
125
+ });
126
+ }
127
+ if (id) {
128
+ const res = await apiClient.get(`/api/v1/admin/catalog/products/${id}`);
129
+ const p = res.data;
130
+ setSku(p.sku);
131
+ setType(p.type);
132
+ setStatus(p.status);
133
+ setVisibility(p.visibility);
134
+ const loadedName = {
135
+ 'en-US': p.name['en-US'] ?? '',
136
+ 'pl-PL': p.name['pl-PL'] ?? '',
137
+ };
138
+ setName(loadedName);
139
+ const loadedDescription = {
140
+ 'en-US': p.description['en-US'] ?? '',
141
+ 'pl-PL': p.description['pl-PL'] ?? '',
142
+ };
143
+ setDescription(loadedDescription);
144
+ const price = p.attributeValues['defaultPrice'] ?? null;
145
+ const loadedPrice = price != null ? String(price) : '';
146
+ setDefaultPrice(loadedPrice);
147
+ setAttrValues(p.attributeValues ?? {});
148
+ setAttributeSetId(p.attributeSetId);
149
+ setCategoryIds(p.categoryIds ?? []);
150
+ // Capture the loaded state as the clean baseline for dirty-tracking.
151
+ initialSnapshotRef.current = stableSnapshot({
152
+ sku: p.sku,
153
+ type: p.type,
154
+ status: p.status,
155
+ visibility: p.visibility,
156
+ name: loadedName,
157
+ description: loadedDescription,
158
+ defaultPrice: loadedPrice,
159
+ categoryIds: p.categoryIds ?? [],
160
+ attributeSetId: p.attributeSetId,
161
+ attrValues: p.attributeValues ?? {},
162
+ });
163
+ }
164
+ }
165
+ catch (err) {
166
+ setError(err instanceof ApiError ? err.envelope.error.message : 'Failed to load.');
167
+ }
168
+ finally {
169
+ setLoading(false);
170
+ }
171
+ }, [id, isNew]);
172
+ useEffect(() => {
173
+ void refresh();
174
+ }, [refresh]);
175
+ const handleSave = useCallback(async (e) => {
176
+ e.preventDefault();
177
+ setError(null);
178
+ setInfo(null);
179
+ const trimmedName = {};
180
+ const trimmedDescription = {};
181
+ for (const l of LOCALES) {
182
+ if (name[l])
183
+ trimmedName[l] = name[l];
184
+ if (description[l])
185
+ trimmedDescription[l] = description[l];
186
+ }
187
+ if (Object.keys(trimmedName).length === 0) {
188
+ setError('At least one locale name is required.');
189
+ return;
190
+ }
191
+ // Seed from the Attributes-tab edits; `defaultPrice` is a virtual
192
+ // attribute owned by the Details tab and re-applied just below.
193
+ const attributeValues = { ...attrValues };
194
+ delete attributeValues['defaultPrice'];
195
+ if (defaultPrice.trim()) {
196
+ const v = Number(defaultPrice);
197
+ if (!Number.isFinite(v) || v < 0) {
198
+ setError('Default price must be a number ≥ 0.');
199
+ return;
200
+ }
201
+ attributeValues['defaultPrice'] = v;
202
+ }
203
+ try {
204
+ if (isNew) {
205
+ const res = await apiClient.post('/api/v1/admin/catalog/products', {
206
+ sku,
207
+ type,
208
+ name: trimmedName,
209
+ description: trimmedDescription,
210
+ categoryIds,
211
+ attributeValues,
212
+ visibility,
213
+ ...(attributeSetId ? { attributeSetId } : {}),
214
+ });
215
+ navigate(`/catalog/products/${res.data.id}`);
216
+ }
217
+ else if (id) {
218
+ // Feature 023 — page-Save commits both the per-language baseline
219
+ // (carried by `trimmedName` / `trimmedDescription` via the existing
220
+ // PATCH) AND any pending channel-scoped override drafts held in
221
+ // the ProductScopeEditor panel. Run them in parallel; if the
222
+ // override flush rejects, the page PATCH still committed.
223
+ await Promise.all([
224
+ apiClient.patch(`/api/v1/admin/catalog/products/${id}`, {
225
+ sku: sku.trim(),
226
+ status,
227
+ name: trimmedName,
228
+ description: trimmedDescription,
229
+ categoryIds,
230
+ attributeValues,
231
+ visibility,
232
+ ...(attributeSetId ? { attributeSetId } : {}),
233
+ }),
234
+ scopeEditorRef.current?.flushOverrides() ?? Promise.resolve(),
235
+ galleryRef.current?.flushGallery() ?? Promise.resolve(),
236
+ ]);
237
+ setInfo('Saved.');
238
+ await refresh();
239
+ }
240
+ }
241
+ catch (err) {
242
+ setError(err instanceof ApiError ? err.envelope.error.message : 'Save failed.');
243
+ }
244
+ }, [isNew, id, sku, status, type, name, description, categoryIds, defaultPrice, attrValues, attributeSetId, visibility, navigate, refresh]);
245
+ const handleAttrChange = useCallback((key, value) => {
246
+ setAttrValues((prev) => ({ ...prev, [key]: value }));
247
+ }, []);
248
+ // Swapping the Attribute Set drops the values whose keys are not in the
249
+ // newly-selected set, so the save payload doesn't carry orphan keys left
250
+ // over from the previous set. `defaultPrice` / `price` are virtual keys
251
+ // owned by the Details tab (not bound to any set) and are always kept.
252
+ const handleAttributeSetChange = useCallback(async (newSetId) => {
253
+ setAttributeSetId(newSetId);
254
+ if (!newSetId)
255
+ return;
256
+ try {
257
+ const res = await apiClient.get(`/api/v1/admin/catalog/attribute-sets/${newSetId}`);
258
+ const allowed = new Set(res.data.attributes.map((a) => a.key));
259
+ setAttrValues((prev) => {
260
+ const next = {};
261
+ for (const [k, v] of Object.entries(prev)) {
262
+ if (allowed.has(k) || k === 'defaultPrice' || k === 'price') {
263
+ next[k] = v;
264
+ }
265
+ }
266
+ return next;
267
+ });
268
+ }
269
+ catch {
270
+ // Non-fatal — the backend tolerates round-tripped keys, so leaving
271
+ // the values untouched on a fetch failure still saves correctly.
272
+ }
273
+ }, []);
274
+ const handleDeleteProduct = useCallback(async () => {
275
+ if (!id)
276
+ return;
277
+ if (!confirm(t('productEditor.deleteConfirm')))
278
+ return;
279
+ try {
280
+ await apiClient.delete(`/api/v1/admin/catalog/products/${id}`);
281
+ navigate('/catalog/products');
282
+ }
283
+ catch (err) {
284
+ setError(err instanceof ApiError ? err.envelope.error.message : 'Delete failed.');
285
+ }
286
+ }, [id, navigate, t]);
287
+ const [duplicating, setDuplicating] = useState(false);
288
+ const handleDuplicate = useCallback(async () => {
289
+ if (!id || duplicating)
290
+ return;
291
+ if (!confirm('Duplicate this product? A draft copy will be created.'))
292
+ return;
293
+ setDuplicating(true);
294
+ setError(null);
295
+ try {
296
+ const res = await apiClient.post(`/api/v1/admin/catalog/products/${id}/duplicate`, {});
297
+ navigate(`/catalog/products/${res.data.id}`);
298
+ }
299
+ catch (err) {
300
+ setError(err instanceof ApiError ? err.envelope.error.message : 'Duplicate failed.');
301
+ }
302
+ finally {
303
+ setDuplicating(false);
304
+ }
305
+ }, [id, duplicating, navigate]);
306
+ if (loading)
307
+ return (_jsx("div", { className: "b2b-page b2b-page--wide", children: _jsx("div", { style: { padding: 32, color: 'var(--fg-muted)', fontSize: 13 }, children: t('productEditor.loading') }) }));
308
+ const tabs = [
309
+ { id: 'details', label: 'Details', icon: _jsx(FileTextIcon, { size: 14 }), show: true },
310
+ { id: 'attributes', label: 'Attributes', icon: _jsx(SlidersHorizontal, { size: 14 }), show: !isNew },
311
+ { id: 'pricing', label: 'Pricing', icon: _jsx(CircleDollarSign, { size: 14 }), show: !isNew },
312
+ {
313
+ id: 'variants',
314
+ label: 'Variants',
315
+ icon: _jsx(Layers, { size: 14 }),
316
+ show: !isNew && type === 'configurable',
317
+ },
318
+ {
319
+ id: 'composite',
320
+ label: type === 'grouped' ? 'Grouped items' : 'Bundle slots',
321
+ icon: _jsx(Layers, { size: 14 }),
322
+ show: !isNew && (type === 'grouped' || type === 'bundle'),
323
+ },
324
+ { id: 'media', label: 'Media', icon: _jsx(ImageIcon, { size: 14 }), show: !isNew },
325
+ { id: 'inventory', label: 'Inventory', icon: _jsx(WarehouseIcon, { size: 14 }), show: !isNew },
326
+ { id: 'attachments', label: 'Attachments', icon: _jsx(Paperclip, { size: 14 }), show: !isNew },
327
+ { id: 'links', label: 'Related', icon: _jsx(LinkIcon, { size: 14 }), show: !isNew },
328
+ // Feature 091 / P7a, Z15 — the tab body is a zone, so the button is shown
329
+ // by counting it. `useAdminZone` has already applied both presence axes and
330
+ // each contributor's permission, so its length is the honest answer to "is
331
+ // there anything behind this tab" — and a tab that opens on an empty panel
332
+ // is worse than no tab. The label stays this screen's own word for the
333
+ // place (Z14); what it stops knowing is which module fills it.
334
+ {
335
+ id: 'channels',
336
+ label: 'Channels',
337
+ icon: _jsx(StoreIcon, { size: 14 }),
338
+ show: !isNew && channelContributions.length > 0,
339
+ },
340
+ { id: 'seo', label: 'SEO', icon: _jsx(Globe, { size: 14 }), show: !isNew },
341
+ ];
342
+ const visibleTabs = tabs.filter((t) => t.show);
343
+ return (_jsxs("div", { className: "b2b-page b2b-page--wide", children: [_jsxs("div", { className: "b2b-page-head", children: [_jsxs("div", { className: "b2b-grow", children: [_jsxs("button", { type: "button", className: "b2b-page-head__back", onClick: () => { navigate('/catalog/products'); }, children: [_jsx(ChevronLeft, { size: 14 }), " Products"] }), _jsxs("div", { className: "b2b-page-head__title", children: [_jsx("span", { children: isNew ? 'New product' : pickName(name) || sku }), !isNew ? _jsx(StatusPill, { status: status }) : null] }), !isNew ? (_jsxs("div", { className: "b2b-page-head__sub", children: [_jsx("span", { className: "b2b-mono", children: sku }), " \u00B7 ", _jsx("span", { style: { textTransform: 'capitalize' }, children: type })] })) : null] }), _jsxs(StickyFormActions, { className: "b2b-page-head__actions", children: [!isNew ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "b2b-btn b2b-btn--default b2b-btn--sm", children: [_jsx(Eye, { size: 13 }), " Preview"] }), _jsxs("button", { type: "button", className: "b2b-btn b2b-btn--default b2b-btn--sm", onClick: () => void handleDuplicate(), disabled: duplicating, children: [_jsx(Copy, { size: 13 }), " ", duplicating ? 'Duplicating…' : 'Duplicate'] }), _jsxs("button", { type: "button", className: "b2b-btn b2b-btn--danger b2b-btn--sm", onClick: () => void handleDeleteProduct(), children: [_jsx(Trash2, { size: 13 }), " ", t('productEditor.action.delete')] })] })) : null, _jsxs("button", { type: "button", className: "b2b-btn b2b-btn--primary", onClick: (e) => {
344
+ void handleSave(e);
345
+ }, children: [_jsx(Save, { size: 14 }), " Save"] })] })] }), error ? (_jsx(Alert, { variant: "destructive", className: "mb-4", children: _jsx(AlertDescription, { children: error }) })) : null, info ? (_jsx(Alert, { variant: "success", className: "mb-4", children: _jsx(AlertDescription, { children: info }) })) : null, _jsxs("div", { className: "b2b-card", children: [_jsx("div", { style: { padding: '4px 4px 0', overflow: 'hidden' }, children: _jsx("div", { className: "b2b-tabs-scroll", children: _jsx("div", { className: "b2b-tabs", role: "tablist", children: visibleTabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": activeTab === t.id, className: cn('b2b-tab', activeTab === t.id && 'is-active'), onClick: () => setActiveTab(t.id), children: [t.icon, t.label] }, t.id))) }) }) }), _jsxs("div", { className: "b2b-card__body", children: [activeTab === 'details' ? (_jsx("form", { id: "product-details-form", onSubmit: handleSave, children: _jsxs("div", { className: "b2b-col", style: { gap: 18 }, children: [id ? (_jsx(AdminZone, { name: "product.editor.details.before", props: { productId: id } })) : null, id ? (_jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: [_jsx(AdminZone, { name: "product.editor.field.after", props: { productId: id, fieldPath: 'name', languageCodes: LOCALES } }), _jsx(AdminZone, { name: "product.editor.field.after", props: { productId: id, fieldPath: 'description', languageCodes: LOCALES } })] })) : null, id ? (_jsx(ProductScopeEditor, { ref: scopeEditorRef, productId: id, baselineName: name, baselineDescription: description, onBaselineNameChange: (next) => setName({
346
+ 'en-US': next['en-US'] ?? '',
347
+ 'pl-PL': next['pl-PL'] ?? '',
348
+ }), onBaselineDescriptionChange: (next) => setDescription({
349
+ 'en-US': next['en-US'] ?? '',
350
+ 'pl-PL': next['pl-PL'] ?? '',
351
+ }) })) : null, _jsxs("div", { children: [_jsx("div", { className: "b2b-label", children: t('productEditor.section.identity') }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }, children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "sku", children: t('productEditor.field.sku') }), _jsx(Input, { id: "sku", value: sku, onChange: (e) => setSku(e.target.value), required: true })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "ptype", children: t('productEditor.field.type') }), _jsx(Select, { id: "ptype", value: type, onChange: (e) => setType(e.target.value), disabled: !isNew, children: PRODUCT_TYPES.map((pt) => (_jsx("option", { value: pt, children: pt }, pt))) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "pvis", children: t('productEditor.field.visibility') }), _jsx(Select, { id: "pvis", value: visibility, onChange: (e) => setVisibility(e.target.value), children: VISIBILITIES.map((v) => (_jsx("option", { value: v, children: v }, v))) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "pattrset", children: t('productEditor.field.attributeSet') }), _jsx(Select, { id: "pattrset", value: attributeSetId, onChange: (e) => void handleAttributeSetChange(e.target.value), children: attributeSets.length === 0 ? (_jsx("option", { value: "", children: t('productEditor.attributeSet.loading') })) : (attributeSets.map((s) => (_jsxs("option", { value: s.id, children: [s.code, s.isSystem ? t('productEditor.attributeSet.systemSuffix') : ''] }, s.id)))) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "pstatus", children: t('productEditor.field.status') }), _jsx(Select, { id: "pstatus", value: status, onChange: (e) => setStatus(e.target.value), children: STATUSES.map((s) => (_jsx("option", { value: s, children: s }, s))) }), _jsx("p", { className: "b2b-help", children: t('productEditor.field.status.help') })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "dp", children: t('productEditor.field.defaultPrice') }), _jsx(Input, { id: "dp", type: "number", step: "0.01", min: "0", value: defaultPrice, onChange: (e) => setDefaultPrice(e.target.value) }), _jsx("p", { className: "b2b-help", children: t('productEditor.field.defaultPrice.help') })] })] })] }), isNew ? (_jsxs(_Fragment, { children: [_jsx("hr", { className: "b2b-hr" }), _jsxs("div", { children: [_jsx("div", { className: "b2b-label", children: t('productEditor.section.localizedContent') }), _jsx("p", { className: "b2b-help", style: { marginTop: 0, marginBottom: 12 }, children: t('productEditor.section.localizedContent.help') }), _jsx("div", { className: "b2b-col", style: { gap: 18 }, children: LOCALES.map((l) => (_jsxs("div", { className: "b2b-col", style: { gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: `name-${l}`, children: t('productEditor.field.localizedName', { locale: l }) }), _jsx(Input, { id: `name-${l}`, value: name[l], onChange: (e) => setName((prev) => ({ ...prev, [l]: e.target.value })) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: `desc-${l}`, children: t('productEditor.field.localizedDescription', { locale: l }) }), _jsx("textarea", { id: `desc-${l}`, rows: 3, className: "b2b-field", value: description[l], onChange: (e) => setDescription((prev) => ({ ...prev, [l]: e.target.value })) })] })] }, l))) })] })] })) : null, _jsx("hr", { className: "b2b-hr" }), _jsxs("div", { children: [_jsxs("div", { className: "b2b-label", children: [t('productEditor.section.categories'), id ? (_jsx(AdminZone, { name: "product.editor.field.after", props: { productId: id, fieldPath: 'categories', languageCodes: null } })) : null] }), _jsx(CategoryTreePicker, { categories: categories, selectedIds: categoryIds, onChange: setCategoryIds, loading: loading }), _jsx("p", { className: "b2b-help", children: t('productEditor.section.categories.help') })] })] }) })) : null, activeTab === 'attributes' && id ? (_jsx(ProductAttributesTab, { productId: id, attributeSetId: attributeSetId, values: attrValues, onChange: handleAttrChange })) : null, activeTab === 'pricing' && id ? (_jsxs("div", { className: "b2b-col", style: { gap: 16 }, children: [_jsx(AdminZone, { name: "product.editor.pricing.before", props: { productId: id } }), _jsx(AdminZone, { name: "product.editor.pricing.after", props: { productId: id } })] })) : null, activeTab === 'variants' && id ? _jsx(VariantsSection, { productId: id }) : null, activeTab === 'composite' && id ? (type === 'grouped' ? (_jsx(GroupedItemsSection, { productId: id })) : (_jsx(BundleSlotsSection, { productId: id }))) : null, activeTab === 'media' && id ? (_jsxs(_Fragment, { children: [_jsx(AdminZone, { name: "product.editor.field.after", props: { productId: id, fieldPath: 'gallery', languageCodes: null } }), _jsx(GallerySection, { ref: galleryRef, productId: id })] })) : null, activeTab === 'inventory' && id ? (_jsxs("div", { className: "b2b-col", style: { gap: 16 }, children: [_jsx(ProductInventoryTab, { productId: id }), type === 'simple' || type === 'configurable' ? (_jsx(PackagingUnitsEditor, { productId: id })) : null] })) : null, activeTab === 'attachments' && id ? (_jsxs(_Fragment, { children: [_jsx(AdminZone, { name: "product.editor.field.after", props: { productId: id, fieldPath: 'attachments', languageCodes: null } }), _jsx(AttachmentsSection, { productId: id })] })) : null, activeTab === 'links' && id ? _jsx(ProductLinksSection, { productId: id }) : null, activeTab === 'channels' && id ? (_jsx(AdminZone, { name: "product.editor.channels", props: { productId: id } })) : null, activeTab === 'seo' ? _jsx(SeoStub, { name: name }) : null] })] })] }));
352
+ }
353
+ function StatusPill({ status }) {
354
+ const map = {
355
+ active: { cls: 'b2b-badge--success', label: 'Active' },
356
+ draft: { cls: 'b2b-badge--warn', label: 'Draft' },
357
+ inactive: { cls: '', label: 'Inactive' },
358
+ };
359
+ const v = map[status];
360
+ return _jsx("span", { className: cn('b2b-badge', 'b2b-badge--dot', v.cls), children: v.label });
361
+ }
362
+ function pickName(name) {
363
+ return name['en-US'] ?? Object.values(name)[0] ?? '';
364
+ }
365
+ function SeoStub({ name }) {
366
+ const t = useTranslation('catalog');
367
+ return (_jsxs("div", { className: "b2b-col", style: { gap: 12 }, children: [_jsx("div", { className: "b2b-label", children: t('productEditor.seo.title') }), _jsx("div", { className: "b2b-help", style: { marginTop: 0 }, children: t('productEditor.seo.description') }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "seo-title", children: t('productEditor.seo.pageTitlePreview') }), _jsx(Input, { id: "seo-title", defaultValue: pickName(name) })] })] }));
368
+ }
369
+ function VariantsSection({ productId }) {
370
+ const t = useTranslation('catalog');
371
+ const [variants, setVariants] = useState([]);
372
+ const [loading, setLoading] = useState(true);
373
+ const [error, setError] = useState(null);
374
+ const [info, setInfo] = useState(null);
375
+ const refresh = useCallback(async () => {
376
+ setLoading(true);
377
+ setError(null);
378
+ try {
379
+ // The PDP returns variants nested in productDetail; admin needs a
380
+ // standalone read to keep the forms quick. Foundation didn't ship
381
+ // a GET /admin/.../variants, but the public productDetail under
382
+ // /api/v1/catalog/products/<idOrSlug> includes them — use that.
383
+ const res = await apiClient.get(`/api/v1/catalog/products/${productId}`);
384
+ setVariants(res.data.variants ?? []);
385
+ }
386
+ catch (err) {
387
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.variants.error.load'));
388
+ }
389
+ finally {
390
+ setLoading(false);
391
+ }
392
+ }, [productId, t]);
393
+ useEffect(() => {
394
+ void refresh();
395
+ }, [refresh]);
396
+ const handleCreate = useCallback(async (input) => {
397
+ // attrs is a comma-separated list of `key=value` pairs.
398
+ const variantAttributeValues = {};
399
+ for (const pair of input.attrs.split(',')) {
400
+ const trimmed = pair.trim();
401
+ if (!trimmed)
402
+ continue;
403
+ const [k, v] = trimmed.split('=');
404
+ if (k && v !== undefined)
405
+ variantAttributeValues[k.trim()] = v.trim();
406
+ }
407
+ try {
408
+ const payload = {
409
+ sku: input.sku,
410
+ variantAttributeValues,
411
+ };
412
+ if (input.priceOverride.trim()) {
413
+ payload['priceOverride'] = Number(input.priceOverride);
414
+ }
415
+ if (input.stockLevel.trim()) {
416
+ payload['stockLevel'] = Number(input.stockLevel);
417
+ }
418
+ await apiClient.post(`/api/v1/admin/catalog/products/${productId}/variants`, payload);
419
+ setInfo(t('productEditor.variants.success.create', { sku: input.sku }));
420
+ await refresh();
421
+ }
422
+ catch (err) {
423
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.variants.error.create'));
424
+ }
425
+ }, [productId, refresh, t]);
426
+ const handleDelete = useCallback(async (variantId, sku) => {
427
+ if (!confirm(t('productEditor.variants.deleteConfirm', { sku })))
428
+ return;
429
+ try {
430
+ await apiClient.delete(`/api/v1/admin/catalog/products/${productId}/variants/${variantId}`);
431
+ setInfo(t('productEditor.variants.success.delete', { sku }));
432
+ await refresh();
433
+ }
434
+ catch (err) {
435
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.variants.error.delete'));
436
+ }
437
+ }, [productId, refresh, t]);
438
+ return (_jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('productEditor.variants.title') }) }), _jsxs(CardContent, { className: "space-y-4", children: [error ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) })) : null, info ? (_jsx(Alert, { children: _jsx(AlertDescription, { children: info }) })) : null, loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.loading') })) : variants.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.variants.empty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('productEditor.variants.column.sku') }), _jsx(TableHead, { children: t('productEditor.variants.column.attributes') }), _jsx(TableHead, { children: t('productEditor.variants.column.priceOverride') }), _jsx(TableHead, { children: t('productEditor.variants.column.stock') }), _jsx(TableHead, { className: "w-[1%] whitespace-nowrap", children: t('productEditor.variants.column.actions') })] }) }), _jsx(TableBody, { children: variants.map((v) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono", children: v.sku }), _jsx(TableCell, { children: Object.entries(v.variantAttributeValues)
439
+ .map(([k, val]) => `${k}=${String(val)}`)
440
+ .join(', ') || '—' }), _jsx(TableCell, { children: v.priceOverride ?? '—' }), _jsx(TableCell, { children: v.stockLevel ?? '—' }), _jsx(TableCell, { children: _jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: () => void handleDelete(v.id, v.sku), children: t('productEditor.variants.action.delete') }) })] }, v.id))) })] })), _jsx(CreateVariantInline, { onCreate: handleCreate })] })] }));
441
+ }
442
+ function CreateVariantInline({ onCreate, }) {
443
+ const t = useTranslation('catalog');
444
+ const [sku, setSku] = useState('');
445
+ const [attrs, setAttrs] = useState('');
446
+ const [priceOverride, setPriceOverride] = useState('');
447
+ const [stockLevel, setStockLevel] = useState('');
448
+ return (_jsxs("div", { className: "grid gap-3 md:grid-cols-5 border-t pt-4", role: "group", "aria-label": t('productEditor.variants.createTitle'), children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "vsku", children: t('productEditor.variants.field.sku') }), _jsx(Input, { id: "vsku", value: sku, onChange: (e) => setSku(e.target.value), required: true })] }), _jsxs("div", { className: "space-y-1 md:col-span-2", children: [_jsx(Label, { htmlFor: "vattrs", children: t('productEditor.variants.field.attributes') }), _jsx(Input, { id: "vattrs", value: attrs, onChange: (e) => setAttrs(e.target.value), placeholder: t('productEditor.variants.field.attributesPlaceholder') })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "vprice", children: t('productEditor.variants.field.priceOverride') }), _jsx(Input, { id: "vprice", type: "number", step: "0.01", min: "0", value: priceOverride, onChange: (e) => setPriceOverride(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "vstock", children: t('productEditor.variants.field.stock') }), _jsx(Input, { id: "vstock", type: "number", min: "0", value: stockLevel, onChange: (e) => setStockLevel(e.target.value) })] }), _jsx("div", { className: "md:col-span-5", children: _jsx(Button, { type: "button", onClick: () => {
449
+ void onCreate({ sku, attrs, priceOverride, stockLevel }).then(() => {
450
+ setSku('');
451
+ setAttrs('');
452
+ setPriceOverride('');
453
+ setStockLevel('');
454
+ });
455
+ }, children: t('productEditor.variants.action.add') }) })] }));
456
+ }
457
+ // ---------------------------------------------------------------------------
458
+ // Gallery section (T085) — admin CRUD for the Product's gallery items.
459
+ // Foundation lacks an admin Asset upload endpoint, so for now the form
460
+ // accepts an existing assetId; future work adds drag-and-drop on top of
461
+ // an Assets module admin upload route.
462
+ // ---------------------------------------------------------------------------
463
+ const GALLERY_LABELS = ['base_image', 'small_image', 'thumbnail'];
464
+ /** Immutable array move used by drag-and-drop / arrow reordering. */
465
+ function moveGalleryItem(items, from, to) {
466
+ if (to < 0 || to >= items.length || from === to)
467
+ return items;
468
+ const next = items.slice();
469
+ const [moved] = next.splice(from, 1);
470
+ if (moved === undefined)
471
+ return items;
472
+ next.splice(to, 0, moved);
473
+ return next;
474
+ }
475
+ const GallerySection = forwardRef(function GallerySection({ productId }, ref) {
476
+ const t = useTranslation('catalog');
477
+ const [items, setItems] = useState([]);
478
+ const [images, setImages] = useState([]);
479
+ const [loading, setLoading] = useState(true);
480
+ const [busy, setBusy] = useState(false);
481
+ const [error, setError] = useState(null);
482
+ const [info, setInfo] = useState(null);
483
+ const [previewUrl, setPreviewUrl] = useState(null);
484
+ const dragIdRef = useRef(null);
485
+ // Server-truth snapshot (id order + labels) used to diff the staged edits
486
+ // that the page-Save button flushes.
487
+ const baselineRef = useRef('');
488
+ const snapshot = (list) => JSON.stringify(list.map((g) => ({ id: g.id, labels: [...g.labels].sort() })));
489
+ const refresh = useCallback(async () => {
490
+ setLoading(true);
491
+ setError(null);
492
+ try {
493
+ // Public PDP carries the resolved gallery (with asset urls) so
494
+ // admins see the same shape customers do; the admin GET returns
495
+ // bare ids without urls. The PDP also carries the product's flat
496
+ // `assets` (legacy product_assets) — the storefront renders those
497
+ // when the label-aware gallery is empty, so we surface them here
498
+ // too, otherwise a product whose images live on product_assets
499
+ // looks empty in the admin while showing fine on the storefront.
500
+ const res = await apiClient.get(`/api/v1/catalog/products/${productId}`);
501
+ const gallery = res.data.gallery ?? [];
502
+ const mapped = gallery
503
+ .slice()
504
+ .sort((a, b) => a.position - b.position)
505
+ .map((g) => ({
506
+ id: g.id,
507
+ productId,
508
+ assetId: g.asset.id,
509
+ position: g.position,
510
+ labels: g.labels,
511
+ asset: { url: g.asset.url, kind: g.asset.kind },
512
+ }));
513
+ setItems(mapped);
514
+ baselineRef.current = snapshot(mapped);
515
+ const assets = res.data.assets ?? [];
516
+ setImages(assets
517
+ .filter((a) => a.kind === 'image' || a.kind === 'video')
518
+ .map((a) => ({
519
+ id: a.id,
520
+ kind: a.kind,
521
+ url: a.url,
522
+ altText: a.altText ?? null,
523
+ })));
524
+ }
525
+ catch (err) {
526
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.gallery.error.load'));
527
+ }
528
+ finally {
529
+ setLoading(false);
530
+ }
531
+ }, [productId, t]);
532
+ useEffect(() => {
533
+ void refresh();
534
+ }, [refresh]);
535
+ // --- Staged label edits: each label lives on at most one item; assigning it
536
+ // to one item strips it from every other (radio semantics). Persisted on
537
+ // page Save, so no per-row Save button and no "already taken" error. ---
538
+ const assignLabel = useCallback((label, itemId) => {
539
+ setInfo(null);
540
+ setItems((prev) => prev.map((it) => {
541
+ const without = it.labels.filter((l) => l !== label);
542
+ return { ...it, labels: it.id === itemId ? [...without, label] : without };
543
+ }));
544
+ }, []);
545
+ const clearLabel = useCallback((label) => {
546
+ setInfo(null);
547
+ setItems((prev) => prev.map((it) => ({ ...it, labels: it.labels.filter((l) => l !== label) })));
548
+ }, []);
549
+ // --- Staged reorder (array order === display order). ---
550
+ const moveItem = useCallback((from, to) => {
551
+ setInfo(null);
552
+ setItems((prev) => moveGalleryItem(prev, from, to));
553
+ }, []);
554
+ const handleDrop = useCallback((index) => {
555
+ const draggedId = dragIdRef.current;
556
+ if (!draggedId)
557
+ return;
558
+ setItems((prev) => {
559
+ const from = prev.findIndex((it) => it.id === draggedId);
560
+ return from < 0 || from === index ? prev : moveGalleryItem(prev, from, index);
561
+ });
562
+ }, []);
563
+ // --- Immediate CRUD (add / remove) — these hit the server directly and
564
+ // re-load, which also resets the staged baseline. ---
565
+ const handleCreate = useCallback(async (assetId) => {
566
+ setBusy(true);
567
+ try {
568
+ await apiClient.post(`/api/v1/admin/catalog/products/${productId}/gallery`, {
569
+ assetId,
570
+ labels: [],
571
+ });
572
+ setInfo(t('productEditor.gallery.success.add'));
573
+ await refresh();
574
+ }
575
+ catch (err) {
576
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.gallery.error.add'));
577
+ }
578
+ finally {
579
+ setBusy(false);
580
+ }
581
+ }, [productId, refresh, t]);
582
+ const handleDelete = useCallback(async (itemId) => {
583
+ if (!confirm(t('productEditor.gallery.removeConfirm')))
584
+ return;
585
+ setBusy(true);
586
+ try {
587
+ await apiClient.delete(`/api/v1/admin/catalog/products/${productId}/gallery/${itemId}`);
588
+ setInfo(t('productEditor.gallery.success.remove'));
589
+ await refresh();
590
+ }
591
+ catch (err) {
592
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.gallery.error.delete'));
593
+ }
594
+ finally {
595
+ setBusy(false);
596
+ }
597
+ }, [productId, refresh, t]);
598
+ // --- Page-Save hook: flush staged label + order edits. Labels are patched
599
+ // with `replace=true` so reassigning a label moves it atomically. ---
600
+ const flushGallery = useCallback(async () => {
601
+ const baseline = JSON.parse(baselineRef.current || '[]');
602
+ const baseLabels = new Map(baseline.map((b) => [b.id, [...b.labels].sort().join(',')]));
603
+ const changed = items.filter((it) => baseLabels.get(it.id) !== [...it.labels].sort().join(','));
604
+ for (const it of changed) {
605
+ await apiClient.patch(`/api/v1/admin/catalog/products/${productId}/gallery/${it.id}?replace=true`, { labels: it.labels });
606
+ }
607
+ const baseOrder = baseline.map((b) => b.id).join(',');
608
+ const curOrder = items.map((it) => it.id).join(',');
609
+ if (items.length > 0 && curOrder !== baseOrder) {
610
+ await apiClient.put(`/api/v1/admin/catalog/products/${productId}/gallery/order`, {
611
+ orderedGalleryItemIds: items.map((it) => it.id),
612
+ });
613
+ }
614
+ if (changed.length > 0 || curOrder !== baseOrder) {
615
+ await refresh();
616
+ }
617
+ }, [items, productId, refresh]);
618
+ useImperativeHandle(ref, () => ({ flushGallery }), [flushGallery]);
619
+ const disabled = busy || loading;
620
+ return (_jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('productEditor.gallery.title') }) }), _jsxs(CardContent, { className: "space-y-4", children: [error ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) })) : null, info ? (_jsx(Alert, { children: _jsx(AlertDescription, { children: info }) })) : null, !loading && images.length > 0 ? (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-sm font-medium", children: t('productEditor.gallery.productImages.title') }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('productEditor.gallery.productImages.hint') }), _jsx("div", { className: "flex flex-wrap gap-2", children: images.map((img) => img.kind === 'video' ? (_jsx("video", { src: toAbsoluteAssetUrl(img.url), className: "h-20 w-20 rounded border object-cover", muted: true }, img.id)) : (_jsx("img", { src: toAbsoluteAssetUrl(img.url), alt: img.altText ?? '', className: "h-20 w-20 rounded border object-cover" }, img.id))) })] })) : null, loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.loading') })) : items.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.gallery.empty') })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-xs text-muted-foreground", children: t('productEditor.gallery.dragHint') }), _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-8" }), _jsx(TableHead, { children: t('productEditor.gallery.column.preview') }), GALLERY_LABELS.map((label) => (_jsx(TableHead, { className: "text-center", children: t(`productEditor.gallery.label.${label}`) }, label))), _jsx(TableHead, { children: t('productEditor.gallery.column.assetId') }), _jsx(TableHead, { className: "w-[1%] whitespace-nowrap text-right", children: t('productEditor.gallery.column.actions') })] }) }), _jsx(TableBody, { children: items.map((item, index) => (_jsx(GalleryRow, { item: item, index: index, total: items.length, disabled: disabled, onAssignLabel: assignLabel, onClearLabel: clearLabel, onMove: moveItem, onDragStart: (id) => {
621
+ dragIdRef.current = id;
622
+ }, onDragOver: (e) => e.preventDefault(), onDrop: handleDrop, onDragEnd: () => {
623
+ dragIdRef.current = null;
624
+ }, onPreview: setPreviewUrl, onDelete: handleDelete }, item.id))) })] })] })), _jsx(CreateGalleryItemInline, { disabled: disabled, onCreate: handleCreate })] }), previewUrl ? (_jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6", role: "dialog", "aria-modal": "true", "aria-label": t('productEditor.gallery.preview.title'), onClick: () => setPreviewUrl(null), children: [_jsx("img", { src: previewUrl, alt: t('productEditor.gallery.preview.title'), className: "max-h-full max-w-full rounded shadow-lg", onClick: (e) => e.stopPropagation() }), _jsx("button", { type: "button", className: "b2b-btn b2b-btn--sm", style: { position: 'fixed', top: 16, right: 16 }, onClick: () => setPreviewUrl(null), children: t('productEditor.gallery.preview.close') })] })) : null] }));
625
+ });
626
+ function GalleryRow({ item, index, total, disabled, onAssignLabel, onClearLabel, onMove, onDragStart, onDragOver, onDrop, onDragEnd, onPreview, onDelete, }) {
627
+ const t = useTranslation('catalog');
628
+ const url = item.asset?.url ? toAbsoluteAssetUrl(item.asset.url) : null;
629
+ return (_jsxs(TableRow, { draggable: !disabled, onDragStart: () => onDragStart(item.id), onDragOver: onDragOver, onDrop: () => onDrop(index), onDragEnd: onDragEnd, children: [_jsx(TableCell, { className: "w-8", children: _jsx("span", { className: "inline-flex cursor-grab items-center text-muted-foreground", title: t('productEditor.gallery.dragHint'), "aria-hidden": true, children: _jsx(GripVertical, { className: "h-4 w-4" }) }) }), _jsx(TableCell, { children: url ? (item.asset?.kind === 'video' ? (_jsx("video", { src: url, className: "h-12 w-12 rounded border object-cover", muted: true })) : (_jsx("button", { type: "button", className: "block rounded border p-0", title: t('productEditor.gallery.preview.open'), onClick: () => onPreview(url), children: _jsx("img", { src: url, alt: "", className: "h-12 w-12 rounded object-cover" }) }))) : (_jsx("span", { className: "text-muted-foreground text-xs", children: "\u2014" })) }), GALLERY_LABELS.map((label) => {
630
+ const checked = item.labels.includes(label);
631
+ return (_jsx(TableCell, { className: "text-center", children: _jsx("input", { type: "radio", name: `gallery-label-${label}`, checked: checked, disabled: disabled, "aria-label": t(`productEditor.gallery.label.${label}`), onChange: () => { }, onClick: () => checked ? onClearLabel(label) : onAssignLabel(label, item.id) }) }, label));
632
+ }), _jsxs(TableCell, { className: "font-mono text-xs", children: [item.assetId.slice(0, 8), "\u2026"] }), _jsxs(TableCell, { className: "space-x-2 whitespace-nowrap text-right", children: [_jsx(TouchReorderButtons, { disabled: disabled, onMoveUp: () => onMove(index, index - 1), onMoveDown: () => onMove(index, index + 1), disableUp: index === 0, disableDown: index === total - 1 }), _jsx(Button, { type: "button", size: "sm", variant: "destructive", disabled: disabled, onClick: () => void onDelete(item.id), children: t('productEditor.gallery.action.remove') })] })] }));
633
+ }
634
+ function CreateGalleryItemInline({ disabled, onCreate, }) {
635
+ const t = useTranslation('catalog');
636
+ const [assetId, setAssetId] = useState('');
637
+ const [pickerOpen, setPickerOpen] = useState(false);
638
+ const [pickedFilename, setPickedFilename] = useState(null);
639
+ const onPick = (a) => {
640
+ setAssetId(a.id);
641
+ setPickedFilename(a.filename);
642
+ setPickerOpen(false);
643
+ };
644
+ return (_jsxs("div", { className: "space-y-3 border-t pt-4", role: "group", "aria-label": t('productEditor.gallery.addTitle'), children: [pickerOpen ? (_jsx(AssetPicker, { acceptMimePrefix: "image/", allowUpload: true, onSelect: onPick, onClose: () => setPickerOpen(false) })) : null, _jsxs("div", { className: "grid gap-3 md:grid-cols-4", children: [_jsxs("div", { className: "space-y-1 md:col-span-3", children: [_jsx(Label, { htmlFor: "gasset", children: t('productEditor.gallery.field.asset') }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: "gasset", value: pickedFilename ?? assetId, readOnly: true, placeholder: t('productEditor.gallery.field.noAsset') }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setPickerOpen(true), children: t('productEditor.gallery.action.pickOrUpload') })] })] }), _jsx("div", { className: "flex items-end", children: _jsx(Button, { type: "button", disabled: disabled || !assetId, onClick: () => {
645
+ if (!assetId)
646
+ return;
647
+ void onCreate(assetId).then(() => {
648
+ setAssetId('');
649
+ setPickedFilename(null);
650
+ });
651
+ }, children: t('productEditor.gallery.action.add') }) })] })] }));
652
+ }
653
+ function AttachmentsSection({ productId }) {
654
+ const t = useTranslation('catalog');
655
+ const [attachments, setAttachments] = useState([]);
656
+ const [types, setTypes] = useState([]);
657
+ const [loading, setLoading] = useState(true);
658
+ const [error, setError] = useState(null);
659
+ const [info, setInfo] = useState(null);
660
+ const refresh = useCallback(async () => {
661
+ setLoading(true);
662
+ setError(null);
663
+ try {
664
+ const [detail, typesRes] = await Promise.all([
665
+ apiClient.get(`/api/v1/catalog/products/${productId}`),
666
+ apiClient.get('/api/v1/admin/catalog/attachment-types'),
667
+ ]);
668
+ const detailAttachments = detail.data.attachments ?? [];
669
+ setAttachments(detailAttachments.map((a) => ({
670
+ id: a.id,
671
+ productId,
672
+ assetId: a.asset.id,
673
+ attachmentTypeId: a.type.id,
674
+ name: a.name,
675
+ description: a.description,
676
+ position: a.position,
677
+ asset: {
678
+ url: a.asset.url,
679
+ filename: a.asset.filename,
680
+ mimeType: a.asset.mimeType,
681
+ },
682
+ type: { code: a.type.code, name: a.type.name },
683
+ })));
684
+ setTypes(typesRes.data);
685
+ }
686
+ catch (err) {
687
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.attachments.error.load'));
688
+ }
689
+ finally {
690
+ setLoading(false);
691
+ }
692
+ }, [productId, t]);
693
+ useEffect(() => {
694
+ void refresh();
695
+ }, [refresh]);
696
+ const handleCreate = useCallback(async (input) => {
697
+ try {
698
+ await apiClient.post(`/api/v1/admin/catalog/products/${productId}/attachments`, {
699
+ assetId: input.assetId,
700
+ attachmentTypeId: input.attachmentTypeId,
701
+ name: input.name,
702
+ ...(input.description ? { description: input.description } : {}),
703
+ });
704
+ setInfo(t('productEditor.attachments.success.add'));
705
+ await refresh();
706
+ }
707
+ catch (err) {
708
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.attachments.error.add'));
709
+ }
710
+ }, [productId, refresh, t]);
711
+ const handleDelete = useCallback(async (attachmentId, name) => {
712
+ if (!confirm(t('productEditor.attachments.deleteConfirm', { name })))
713
+ return;
714
+ try {
715
+ await apiClient.delete(`/api/v1/admin/catalog/products/${productId}/attachments/${attachmentId}`);
716
+ setInfo(t('productEditor.attachments.success.delete', { name }));
717
+ await refresh();
718
+ }
719
+ catch (err) {
720
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.attachments.error.delete'));
721
+ }
722
+ }, [productId, refresh, t]);
723
+ return (_jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('productEditor.attachments.title') }) }), _jsxs(CardContent, { className: "space-y-4", children: [error ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) })) : null, info ? (_jsx(Alert, { children: _jsx(AlertDescription, { children: info }) })) : null, loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.loading') })) : attachments.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.attachments.empty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('productEditor.attachments.column.type') }), _jsx(TableHead, { children: t('productEditor.attachments.column.name') }), _jsx(TableHead, { children: t('productEditor.attachments.column.description') }), _jsx(TableHead, { children: t('productEditor.attachments.column.file') }), _jsx(TableHead, { className: "w-[1%] whitespace-nowrap", children: t('productEditor.attachments.column.actions') })] }) }), _jsx(TableBody, { children: attachments.map((att) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: att.type?.name['en-US'] ?? att.type?.code ?? '—' }), _jsx(TableCell, { className: "font-medium", children: att.name }), _jsx(TableCell, { className: "text-muted-foreground", children: att.description ?? '—' }), _jsx(TableCell, { children: att.asset?.url ? (_jsx("a", { href: att.asset.url, target: "_blank", rel: "noopener", className: "text-xs underline", children: att.asset.filename })) : ('—') }), _jsx(TableCell, { children: _jsx(Button, { type: "button", size: "sm", variant: "destructive", onClick: () => void handleDelete(att.id, att.name), children: t('productEditor.attachments.action.delete') }) })] }, att.id))) })] })), _jsx(CreateAttachmentInline, { types: types, onCreate: handleCreate })] })] }));
724
+ }
725
+ function CreateAttachmentInline({ types, onCreate, }) {
726
+ const [assetId, setAssetId] = useState('');
727
+ const [attachmentTypeId, setAttachmentTypeId] = useState('');
728
+ const [name, setName] = useState('');
729
+ const [description, setDescription] = useState('');
730
+ const [pickerOpen, setPickerOpen] = useState(false);
731
+ const [pickedFilename, setPickedFilename] = useState(null);
732
+ const t = useTranslation('catalog');
733
+ return (_jsxs("div", { className: "space-y-3 border-t pt-4", role: "group", "aria-label": t('productEditor.attachments.addTitle'), children: [pickerOpen ? (_jsx(AssetPicker, { allowUpload: true, onSelect: (a) => {
734
+ setAssetId(a.id);
735
+ setPickedFilename(a.filename);
736
+ setPickerOpen(false);
737
+ }, onClose: () => setPickerOpen(false) })) : null, _jsxs("div", { className: "grid gap-3 md:grid-cols-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "aasset", children: t('productEditor.attachments.field.asset') }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: "aasset", value: pickedFilename ?? assetId, readOnly: true, placeholder: t('productEditor.attachments.field.noAsset') }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setPickerOpen(true), children: t('productEditor.attachments.action.pickOrUpload') })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "atype", children: t('productEditor.attachments.field.type') }), _jsxs(Select, { id: "atype", value: attachmentTypeId, onChange: (e) => setAttachmentTypeId(e.target.value), children: [_jsx("option", { value: "", children: "\u2014" }), types.map((typ) => (_jsx("option", { value: typ.id, children: typ.name['en-US'] ?? typ.code }, typ.id)))] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "aname", children: t('productEditor.attachments.field.name') }), _jsx(Input, { id: "aname", value: name, onChange: (e) => setName(e.target.value), placeholder: t('productEditor.attachments.field.namePlaceholder') })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "adesc", children: t('productEditor.attachments.field.description') }), _jsx(Input, { id: "adesc", value: description, onChange: (e) => setDescription(e.target.value), placeholder: t('productEditor.attachments.field.descriptionPlaceholder') })] }), _jsx("div", { className: "md:col-span-4", children: _jsx(Button, { type: "button", onClick: () => {
738
+ if (!assetId || !attachmentTypeId || !name)
739
+ return;
740
+ void onCreate({ assetId, attachmentTypeId, name, description }).then(() => {
741
+ setAssetId('');
742
+ setPickedFilename(null);
743
+ setAttachmentTypeId('');
744
+ setName('');
745
+ setDescription('');
746
+ });
747
+ }, children: t('productEditor.attachments.action.add') }) })] })] }));
748
+ }
749
+ // ---------------------------------------------------------------------------
750
+ // Product Links section (T109, US4) — Related / Up-sell / Cross-sell.
751
+ // Three sub-lists, one product picker (paste id) + kind selector +
752
+ // "Add" button per kind. Backend bulkCreate is all-or-nothing and
753
+ // surfaces SELF_LINK_NOT_ALLOWED / LINK_ALREADY_EXISTS / TARGET_NOT_FOUND.
754
+ // ---------------------------------------------------------------------------
755
+ const LINK_KINDS = ['related', 'up_sell', 'cross_sell'];
756
+ function pickProductName(name, fallback) {
757
+ if (!name)
758
+ return fallback;
759
+ return name['en-US'] ?? Object.values(name)[0] ?? fallback;
760
+ }
761
+ const TARGETS_BATCH_PAGE_SIZE = 200;
762
+ function ProductLinksSection({ productId }) {
763
+ const t = useTranslation('catalog');
764
+ const [links, setLinks] = useState([]);
765
+ const [targetsById, setTargetsById] = useState(() => new Map());
766
+ const [loading, setLoading] = useState(true);
767
+ const [error, setError] = useState(null);
768
+ const [info, setInfo] = useState(null);
769
+ const refresh = useCallback(async () => {
770
+ setLoading(true);
771
+ setError(null);
772
+ try {
773
+ const res = await apiClient.get(`/api/v1/admin/catalog/products/${productId}/links`);
774
+ setLinks(res.data);
775
+ // Fetch target product details across all three kinds in one shot.
776
+ // Paginates with TARGETS_BATCH_PAGE_SIZE so a product with hundreds
777
+ // of links streams across multiple requests without ballooning the
778
+ // body. In practice a single page is enough.
779
+ const uniqueTargetIds = Array.from(new Set(res.data.map((l) => l.targetProductId)));
780
+ const nextById = new Map();
781
+ if (uniqueTargetIds.length > 0) {
782
+ let page = 0;
783
+ // Bounded loop guards against an unexpected backend response that
784
+ // never sets `hasMore: false`.
785
+ for (;;) {
786
+ const batch = await apiClient.post('/api/v1/admin/catalog/products/batch-by-id', {
787
+ ids: uniqueTargetIds,
788
+ page,
789
+ pageSize: TARGETS_BATCH_PAGE_SIZE,
790
+ });
791
+ for (const p of batch.data)
792
+ nextById.set(p.id, p);
793
+ if (!batch.pagination.hasMore)
794
+ break;
795
+ page += 1;
796
+ if (page > 50)
797
+ break;
798
+ }
799
+ }
800
+ setTargetsById(nextById);
801
+ }
802
+ catch (err) {
803
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.links.error.load'));
804
+ }
805
+ finally {
806
+ setLoading(false);
807
+ }
808
+ }, [productId, t]);
809
+ useEffect(() => {
810
+ void refresh();
811
+ }, [refresh]);
812
+ const handleCreate = useCallback(async (input) => {
813
+ try {
814
+ await apiClient.post(`/api/v1/admin/catalog/products/${productId}/links`, {
815
+ links: [{ targetProductId: input.targetProductId, kind: input.kind }],
816
+ });
817
+ setInfo(t('productEditor.links.success.add', { kind: input.kind }));
818
+ await refresh();
819
+ }
820
+ catch (err) {
821
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.links.error.add'));
822
+ }
823
+ }, [productId, refresh, t]);
824
+ const handleDelete = useCallback(async (linkId) => {
825
+ if (!confirm(t('productEditor.links.removeConfirm')))
826
+ return;
827
+ try {
828
+ await apiClient.delete(`/api/v1/admin/catalog/products/${productId}/links/${linkId}`);
829
+ setInfo(t('productEditor.links.success.remove'));
830
+ await refresh();
831
+ }
832
+ catch (err) {
833
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.links.error.delete'));
834
+ }
835
+ }, [productId, refresh, t]);
836
+ const byKind = useMemo(() => {
837
+ const result = {
838
+ related: [],
839
+ up_sell: [],
840
+ cross_sell: [],
841
+ };
842
+ for (const link of links)
843
+ result[link.kind].push(link);
844
+ for (const kind of LINK_KINDS) {
845
+ result[kind].sort((a, b) => a.position - b.position);
846
+ }
847
+ return result;
848
+ }, [links]);
849
+ return (_jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('productEditor.links.title') }) }), _jsxs(CardContent, { className: "space-y-4", children: [error ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) })) : null, info ? (_jsx(Alert, { children: _jsx(AlertDescription, { children: info }) })) : null, loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.loading') })) : (_jsx("div", { className: "space-y-6", children: LINK_KINDS.map((kind) => (_jsxs("div", { className: "space-y-2", children: [_jsxs("h3", { className: "text-sm font-medium uppercase tracking-wide", children: [t(`productEditor.links.kind.${kind}`), " (", byKind[kind].length, ")"] }), byKind[kind].length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: t('productEditor.links.empty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('productEditor.links.column.target') }), _jsx(TableHead, { children: t('productEditor.links.column.sku') }), _jsx(TableHead, { children: t('productEditor.links.column.position') }), _jsx(TableHead, { className: "w-[1%] whitespace-nowrap", children: t('productEditor.links.column.actions') })] }) }), _jsx(TableBody, { children: byKind[kind].map((link) => {
850
+ const target = targetsById.get(link.targetProductId);
851
+ const isMissing = !target;
852
+ const isInactive = target?.status === 'inactive';
853
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: isMissing || isInactive ? 'text-muted-foreground' : '', children: target
854
+ ? pickProductName(target.name, target.slug)
855
+ : `${link.targetProductId.slice(0, 8)}…` }), isMissing ? (_jsx("span", { className: "rounded border px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground", children: t('productEditor.links.badge.missing') })) : null, isInactive ? (_jsx("span", { className: "rounded border px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground", children: t('productEditor.links.badge.inactive') })) : null] }) }), _jsx(TableCell, { className: "font-mono text-xs", children: target?.sku ?? '—' }), _jsx(TableCell, { children: link.position }), _jsx(TableCell, { children: _jsx(Button, { type: "button", size: "sm", variant: "destructive", onClick: () => void handleDelete(link.id), children: t('productEditor.links.action.remove') }) })] }, link.id));
856
+ }) })] }))] }, kind))) })), _jsx(CreateProductLinkInline, { onCreate: handleCreate, sourceProductId: productId })] })] }));
857
+ }
858
+ function CreateProductLinkInline({ onCreate, sourceProductId, }) {
859
+ const t = useTranslation('catalog');
860
+ const [targetProductId, setTargetProductId] = useState(null);
861
+ const [kind, setKind] = useState('related');
862
+ return (_jsxs("div", { className: "grid gap-3 md:grid-cols-3 border-t pt-4", role: "group", "aria-label": t('productEditor.links.addTitle'), children: [_jsxs("div", { className: "space-y-1 md:col-span-2", children: [_jsx(Label, { htmlFor: "ltarget", children: t('productEditor.links.column.target') }), _jsx(ProductPicker, { id: "ltarget", mode: "select", value: targetProductId, onChange: setTargetProductId, excludeIds: [sourceProductId], placeholder: t('productEditor.links.field.targetPlaceholder'), ariaLabel: t('productEditor.links.column.target') })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "lkind", children: t('productEditor.links.field.kind') }), _jsx(Select, { id: "lkind", value: kind, onChange: (e) => setKind(e.target.value), children: LINK_KINDS.map((k) => (_jsx("option", { value: k, children: t(`productEditor.links.kind.${k}`) }, k))) })] }), _jsx("div", { className: "md:col-span-3", children: _jsx(Button, { type: "button", disabled: targetProductId === null, onClick: () => {
863
+ if (targetProductId === null)
864
+ return;
865
+ void onCreate({ targetProductId, kind }).then(() => {
866
+ setTargetProductId(null);
867
+ });
868
+ }, children: t('productEditor.links.action.add') }) })] }));
869
+ }
870
+ function GroupedItemsSection({ productId }) {
871
+ const t = useTranslation('catalog');
872
+ const [items, setItems] = useState([]);
873
+ const [loading, setLoading] = useState(true);
874
+ const [error, setError] = useState(null);
875
+ const [info, setInfo] = useState(null);
876
+ const refresh = useCallback(async () => {
877
+ setLoading(true);
878
+ setError(null);
879
+ try {
880
+ const res = await apiClient.get(`/api/v1/admin/catalog/products/${productId}/grouped-items`);
881
+ setItems(res.data);
882
+ }
883
+ catch (err) {
884
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.grouped.error.load'));
885
+ }
886
+ finally {
887
+ setLoading(false);
888
+ }
889
+ }, [productId, t]);
890
+ useEffect(() => {
891
+ void refresh();
892
+ }, [refresh]);
893
+ const handleCreate = useCallback(async (input) => {
894
+ const qty = Number(input.quantity);
895
+ if (!Number.isFinite(qty) || qty <= 0) {
896
+ setError(t('productEditor.grouped.error.invalidQuantity'));
897
+ return;
898
+ }
899
+ try {
900
+ await apiClient.post(`/api/v1/admin/catalog/products/${productId}/grouped-items`, { childProductId: input.childProductId, quantity: qty });
901
+ setInfo(t('productEditor.grouped.success.add'));
902
+ await refresh();
903
+ }
904
+ catch (err) {
905
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.grouped.error.add'));
906
+ }
907
+ }, [productId, refresh, t]);
908
+ const handleDelete = useCallback(async (itemId) => {
909
+ if (!confirm(t('productEditor.grouped.removeConfirm')))
910
+ return;
911
+ try {
912
+ await apiClient.delete(`/api/v1/admin/catalog/products/${productId}/grouped-items/${itemId}`);
913
+ setInfo(t('productEditor.grouped.success.remove'));
914
+ await refresh();
915
+ }
916
+ catch (err) {
917
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.grouped.error.delete'));
918
+ }
919
+ }, [productId, refresh, t]);
920
+ return (_jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('productEditor.grouped.title') }) }), _jsxs(CardContent, { className: "space-y-4", children: [error ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) })) : null, info ? (_jsx(Alert, { children: _jsx(AlertDescription, { children: info }) })) : null, loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.loading') })) : items.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.grouped.empty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('productEditor.grouped.column.child') }), _jsx(TableHead, { children: t('productEditor.grouped.column.quantity') }), _jsx(TableHead, { children: t('productEditor.grouped.column.position') }), _jsx(TableHead, { className: "w-[1%] whitespace-nowrap", children: t('productEditor.grouped.column.actions') })] }) }), _jsx(TableBody, { children: items.map((item) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: item.childProductId }), _jsx(TableCell, { children: item.quantity }), _jsx(TableCell, { children: item.position }), _jsx(TableCell, { children: _jsx(Button, { type: "button", size: "sm", variant: "destructive", onClick: () => void handleDelete(item.id), children: t('productEditor.grouped.action.remove') }) })] }, item.id))) })] })), _jsx(CreateGroupedItemInline, { onCreate: handleCreate })] })] }));
921
+ }
922
+ function CreateGroupedItemInline({ onCreate, }) {
923
+ const t = useTranslation('catalog');
924
+ const [childProductId, setChildProductId] = useState('');
925
+ const [quantity, setQuantity] = useState('1');
926
+ return (_jsxs("div", { className: "grid gap-3 md:grid-cols-3 border-t pt-4", role: "group", "aria-label": t('productEditor.grouped.addTitle'), children: [_jsxs("div", { className: "space-y-1 md:col-span-2", children: [_jsx(Label, { htmlFor: "gchild", children: t('productEditor.grouped.column.child') }), _jsx(ProductPicker, { id: "gchild", mode: "select", value: childProductId || null, onChange: (v) => setChildProductId(v ?? '') })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "gquantity", children: t('productEditor.grouped.column.quantity') }), _jsx(Input, { id: "gquantity", type: "number", min: "1", value: quantity, onChange: (e) => setQuantity(e.target.value) })] }), _jsx("div", { className: "md:col-span-3", children: _jsx(Button, { type: "button", onClick: () => {
927
+ if (!childProductId)
928
+ return;
929
+ void onCreate({ childProductId, quantity }).then(() => {
930
+ setChildProductId('');
931
+ setQuantity('1');
932
+ });
933
+ }, children: t('productEditor.grouped.action.add') }) })] }));
934
+ }
935
+ function BundleSlotsSection({ productId }) {
936
+ const t = useTranslation('catalog');
937
+ const [slots, setSlots] = useState([]);
938
+ const [loading, setLoading] = useState(true);
939
+ const [error, setError] = useState(null);
940
+ const [info, setInfo] = useState(null);
941
+ const refresh = useCallback(async () => {
942
+ setLoading(true);
943
+ setError(null);
944
+ try {
945
+ const res = await apiClient.get(`/api/v1/admin/catalog/products/${productId}/bundle-slots`);
946
+ setSlots(res.data);
947
+ }
948
+ catch (err) {
949
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.bundle.error.load'));
950
+ }
951
+ finally {
952
+ setLoading(false);
953
+ }
954
+ }, [productId, t]);
955
+ useEffect(() => {
956
+ void refresh();
957
+ }, [refresh]);
958
+ const handleCreateSlot = useCallback(async (input) => {
959
+ const min = Number(input.minQuantity);
960
+ const max = Number(input.maxQuantity);
961
+ if (!Number.isFinite(max) || max <= 0) {
962
+ setError(t('productEditor.bundle.error.invalidMax'));
963
+ return;
964
+ }
965
+ try {
966
+ await apiClient.post(`/api/v1/admin/catalog/products/${productId}/bundle-slots`, {
967
+ name: { 'en-US': input.name },
968
+ ...(Number.isFinite(min) ? { minQuantity: min } : {}),
969
+ maxQuantity: max,
970
+ });
971
+ setInfo(t('productEditor.bundle.success.addSlot', { name: input.name }));
972
+ await refresh();
973
+ }
974
+ catch (err) {
975
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.bundle.error.addSlot'));
976
+ }
977
+ }, [productId, refresh, t]);
978
+ const handleDeleteSlot = useCallback(async (slotId) => {
979
+ if (!confirm(t('productEditor.bundle.removeSlotConfirm')))
980
+ return;
981
+ try {
982
+ await apiClient.delete(`/api/v1/admin/catalog/products/${productId}/bundle-slots/${slotId}`);
983
+ setInfo(t('productEditor.bundle.success.removeSlot'));
984
+ await refresh();
985
+ }
986
+ catch (err) {
987
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.bundle.error.deleteSlot'));
988
+ }
989
+ }, [productId, refresh, t]);
990
+ const handleAddOption = useCallback(async (slotId, optionProductId) => {
991
+ if (!optionProductId)
992
+ return;
993
+ try {
994
+ await apiClient.post(`/api/v1/admin/catalog/products/${productId}/bundle-slots/${slotId}/options`, { optionProductId });
995
+ setInfo(t('productEditor.bundle.success.addOption'));
996
+ await refresh();
997
+ }
998
+ catch (err) {
999
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.bundle.error.addOption'));
1000
+ }
1001
+ }, [productId, refresh, t]);
1002
+ const handleRemoveOption = useCallback(async (slotId, optionId) => {
1003
+ if (!confirm(t('productEditor.bundle.removeOptionConfirm')))
1004
+ return;
1005
+ try {
1006
+ await apiClient.delete(`/api/v1/admin/catalog/products/${productId}/bundle-slots/${slotId}/options/${optionId}`);
1007
+ setInfo(t('productEditor.bundle.success.removeOption'));
1008
+ await refresh();
1009
+ }
1010
+ catch (err) {
1011
+ setError(err instanceof ApiError ? err.envelope.error.message : t('productEditor.bundle.error.deleteOption'));
1012
+ }
1013
+ }, [productId, refresh, t]);
1014
+ return (_jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: t('productEditor.bundle.title') }) }), _jsxs(CardContent, { className: "space-y-4", children: [error ? (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) })) : null, info ? (_jsx(Alert, { children: _jsx(AlertDescription, { children: info }) })) : null, loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.loading') })) : slots.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('productEditor.bundle.empty') })) : (_jsx("div", { className: "space-y-4", children: slots.map((slot) => (_jsx(BundleSlotCard, { slot: slot, onAddOption: handleAddOption, onRemoveOption: handleRemoveOption, onDeleteSlot: handleDeleteSlot }, slot.id))) })), _jsx(CreateBundleSlotInline, { onCreate: handleCreateSlot })] })] }));
1015
+ }
1016
+ function BundleSlotCard({ slot, onAddOption, onRemoveOption, onDeleteSlot, }) {
1017
+ const t = useTranslation('catalog');
1018
+ const [optionId, setOptionId] = useState('');
1019
+ return (_jsxs("div", { className: "rounded border p-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("h3", { className: "font-medium", children: [slot.name['en-US'] ?? Object.values(slot.name)[0] ?? '—', ' ', _jsx("span", { className: "text-xs text-muted-foreground", children: t('productEditor.bundle.slotMeta', {
1020
+ min: slot.minQuantity,
1021
+ max: slot.maxQuantity,
1022
+ position: slot.position,
1023
+ }) })] }), _jsx(Button, { type: "button", size: "sm", variant: "destructive", onClick: () => void onDeleteSlot(slot.id), children: t('productEditor.bundle.action.deleteSlot') })] }), _jsx("div", { className: "mt-2", children: slot.options.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: t('productEditor.bundle.optionsEmpty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('productEditor.bundle.column.optionProductId') }), _jsx(TableHead, { children: t('productEditor.bundle.column.defaultQuantity') }), _jsx(TableHead, { children: t('productEditor.bundle.column.position') }), _jsx(TableHead, { className: "w-[1%] whitespace-nowrap", children: t('productEditor.bundle.column.actions') })] }) }), _jsx(TableBody, { children: slot.options.map((opt) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: opt.optionProductId }), _jsx(TableCell, { children: opt.defaultQuantity }), _jsx(TableCell, { children: opt.position }), _jsx(TableCell, { children: _jsx(Button, { type: "button", size: "sm", variant: "destructive", onClick: () => void onRemoveOption(slot.id, opt.id), children: t('productEditor.bundle.action.removeOption') }) })] }, opt.id))) })] })) }), _jsxs("div", { className: "mt-3 flex items-end gap-2", children: [_jsxs("div", { className: "flex-1 space-y-1", children: [_jsx(Label, { htmlFor: `opt-${slot.id}`, children: t('productEditor.bundle.column.optionProductId') }), _jsx(ProductPicker, { id: `opt-${slot.id}`, mode: "select", value: optionId || null, onChange: (v) => setOptionId(v ?? '') })] }), _jsx(Button, { type: "button", onClick: () => {
1024
+ void onAddOption(slot.id, optionId).then(() => {
1025
+ setOptionId('');
1026
+ });
1027
+ }, children: t('productEditor.bundle.action.addOption') })] })] }));
1028
+ }
1029
+ function CreateBundleSlotInline({ onCreate, }) {
1030
+ const t = useTranslation('catalog');
1031
+ const [name, setName] = useState('');
1032
+ const [minQuantity, setMinQuantity] = useState('0');
1033
+ const [maxQuantity, setMaxQuantity] = useState('1');
1034
+ return (_jsxs("div", { className: "grid gap-3 md:grid-cols-4 border-t pt-4", role: "group", "aria-label": t('productEditor.bundle.addTitle'), children: [_jsxs("div", { className: "space-y-1 md:col-span-2", children: [_jsx(Label, { htmlFor: "bsname", children: t('productEditor.bundle.field.slotName') }), _jsx(Input, { id: "bsname", value: name, onChange: (e) => setName(e.target.value), placeholder: t('productEditor.bundle.field.slotNamePlaceholder') })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "bsmin", children: t('productEditor.bundle.field.min') }), _jsx(Input, { id: "bsmin", type: "number", min: "0", value: minQuantity, onChange: (e) => setMinQuantity(e.target.value) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "bsmax", children: t('productEditor.bundle.field.max') }), _jsx(Input, { id: "bsmax", type: "number", min: "1", value: maxQuantity, onChange: (e) => setMaxQuantity(e.target.value) })] }), _jsx("div", { className: "md:col-span-4", children: _jsx(Button, { type: "button", onClick: () => {
1035
+ if (!name)
1036
+ return;
1037
+ void onCreate({ name, minQuantity, maxQuantity }).then(() => {
1038
+ setName('');
1039
+ setMinQuantity('0');
1040
+ setMaxQuantity('1');
1041
+ });
1042
+ }, children: t('productEditor.bundle.action.add') }) })] }));
1043
+ }
1044
+ /**
1045
+ * The default export a route declaration's dynamic-import factory resolves
1046
+ * (feature 091, R6). The named export stays: it is the spelling this module's
1047
+ * own siblings and tests use.
1048
+ */
1049
+ export default ProductEditor;
1050
+ //# sourceMappingURL=ProductEditor.js.map