@open-mercato/core 0.6.7-develop.6669.1.40b669666b → 0.6.7-develop.6670.1.4efa7961c6

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 (473) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/inventory_balance/index.js +21 -0
  3. package/dist/generated/entities/inventory_balance/index.js.map +7 -0
  4. package/dist/generated/entities/inventory_lot/index.js +19 -0
  5. package/dist/generated/entities/inventory_lot/index.js.map +7 -0
  6. package/dist/generated/entities/inventory_movement/index.js +35 -0
  7. package/dist/generated/entities/inventory_movement/index.js.map +7 -0
  8. package/dist/generated/entities/inventory_reservation/index.js +23 -0
  9. package/dist/generated/entities/inventory_reservation/index.js.map +7 -0
  10. package/dist/generated/entities/product_inventory_profile/index.js +21 -0
  11. package/dist/generated/entities/product_inventory_profile/index.js.map +7 -0
  12. package/dist/generated/entities/sales_order_warehouse_assignment/index.js +11 -0
  13. package/dist/generated/entities/sales_order_warehouse_assignment/index.js.map +7 -0
  14. package/dist/generated/entities/warehouse/index.js +25 -0
  15. package/dist/generated/entities/warehouse/index.js.map +7 -0
  16. package/dist/generated/entities/warehouse_location/index.js +21 -0
  17. package/dist/generated/entities/warehouse_location/index.js.map +7 -0
  18. package/dist/generated/entities/warehouse_zone/index.js +11 -0
  19. package/dist/generated/entities/warehouse_zone/index.js.map +7 -0
  20. package/dist/generated/entities.ids.generated.js +12 -0
  21. package/dist/generated/entities.ids.generated.js.map +2 -2
  22. package/dist/generated/entity-fields-registry.js +98 -0
  23. package/dist/generated/entity-fields-registry.js.map +2 -2
  24. package/dist/modules/auth/backend/roles/page.js +12 -4
  25. package/dist/modules/auth/backend/roles/page.js.map +2 -2
  26. package/dist/modules/auth/lib/backendChrome.js +1 -0
  27. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  28. package/dist/modules/auth/services/rbacService.js +16 -17
  29. package/dist/modules/auth/services/rbacService.js.map +2 -2
  30. package/dist/modules/catalog/api/products/route.js +3 -0
  31. package/dist/modules/catalog/api/products/route.js.map +2 -2
  32. package/dist/modules/catalog/api/variants/route.js +3 -0
  33. package/dist/modules/catalog/api/variants/route.js.map +2 -2
  34. package/dist/modules/query_index/lib/engine.js +6 -6
  35. package/dist/modules/query_index/lib/engine.js.map +2 -2
  36. package/dist/modules/sales/api/documents/factory.js +1 -0
  37. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  38. package/dist/modules/sales/commands/documents.js +53 -0
  39. package/dist/modules/sales/commands/documents.js.map +2 -2
  40. package/dist/modules/sales/components/documents/ItemsSection.js +51 -2
  41. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  42. package/dist/modules/sales/events.js +2 -0
  43. package/dist/modules/sales/events.js.map +2 -2
  44. package/dist/modules/sales/widgets/injection/order-items-context.js +11 -0
  45. package/dist/modules/sales/widgets/injection/order-items-context.js.map +7 -0
  46. package/dist/modules/wms/acl.js +18 -0
  47. package/dist/modules/wms/acl.js.map +7 -0
  48. package/dist/modules/wms/analytics.js +11 -0
  49. package/dist/modules/wms/analytics.js.map +7 -0
  50. package/dist/modules/wms/api/dashboard/operational/route.js +80 -0
  51. package/dist/modules/wms/api/dashboard/operational/route.js.map +7 -0
  52. package/dist/modules/wms/api/interceptors.js +90 -0
  53. package/dist/modules/wms/api/interceptors.js.map +7 -0
  54. package/dist/modules/wms/api/inventory/adjust/route.js +51 -0
  55. package/dist/modules/wms/api/inventory/adjust/route.js.map +7 -0
  56. package/dist/modules/wms/api/inventory/allocate/route.js +51 -0
  57. package/dist/modules/wms/api/inventory/allocate/route.js.map +7 -0
  58. package/dist/modules/wms/api/inventory/balances/route.js +172 -0
  59. package/dist/modules/wms/api/inventory/balances/route.js.map +7 -0
  60. package/dist/modules/wms/api/inventory/cycle-count/route.js +52 -0
  61. package/dist/modules/wms/api/inventory/cycle-count/route.js.map +7 -0
  62. package/dist/modules/wms/api/inventory/helpers.js +113 -0
  63. package/dist/modules/wms/api/inventory/helpers.js.map +7 -0
  64. package/dist/modules/wms/api/inventory/import/apply/route.js +46 -0
  65. package/dist/modules/wms/api/inventory/import/apply/route.js.map +7 -0
  66. package/dist/modules/wms/api/inventory/import/helpers.js +195 -0
  67. package/dist/modules/wms/api/inventory/import/helpers.js.map +7 -0
  68. package/dist/modules/wms/api/inventory/import/template/route.js +32 -0
  69. package/dist/modules/wms/api/inventory/import/template/route.js.map +7 -0
  70. package/dist/modules/wms/api/inventory/import/validate/route.js +54 -0
  71. package/dist/modules/wms/api/inventory/import/validate/route.js.map +7 -0
  72. package/dist/modules/wms/api/inventory/move/route.js +51 -0
  73. package/dist/modules/wms/api/inventory/move/route.js.map +7 -0
  74. package/dist/modules/wms/api/inventory/movements/route.js +164 -0
  75. package/dist/modules/wms/api/inventory/movements/route.js.map +7 -0
  76. package/dist/modules/wms/api/inventory/receive/route.js +50 -0
  77. package/dist/modules/wms/api/inventory/receive/route.js.map +7 -0
  78. package/dist/modules/wms/api/inventory/release/route.js +44 -0
  79. package/dist/modules/wms/api/inventory/release/route.js.map +7 -0
  80. package/dist/modules/wms/api/inventory/reservations/route.js +128 -0
  81. package/dist/modules/wms/api/inventory/reservations/route.js.map +7 -0
  82. package/dist/modules/wms/api/inventory/reserve/route.js +59 -0
  83. package/dist/modules/wms/api/inventory/reserve/route.js.map +7 -0
  84. package/dist/modules/wms/api/inventory-profiles/route.js +178 -0
  85. package/dist/modules/wms/api/inventory-profiles/route.js.map +7 -0
  86. package/dist/modules/wms/api/listEnrichers.js +181 -0
  87. package/dist/modules/wms/api/listEnrichers.js.map +7 -0
  88. package/dist/modules/wms/api/listSearch.js +77 -0
  89. package/dist/modules/wms/api/listSearch.js.map +7 -0
  90. package/dist/modules/wms/api/locations/route.js +172 -0
  91. package/dist/modules/wms/api/locations/route.js.map +7 -0
  92. package/dist/modules/wms/api/lots/route.js +204 -0
  93. package/dist/modules/wms/api/lots/route.js.map +7 -0
  94. package/dist/modules/wms/api/openapi.js +27 -0
  95. package/dist/modules/wms/api/openapi.js.map +7 -0
  96. package/dist/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.js +54 -0
  97. package/dist/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.js.map +7 -0
  98. package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js +279 -0
  99. package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js.map +7 -0
  100. package/dist/modules/wms/api/sales-orders/assign-warehouse/route.js +56 -0
  101. package/dist/modules/wms/api/sales-orders/assign-warehouse/route.js.map +7 -0
  102. package/dist/modules/wms/api/sales-orders/unassign-warehouse/route.js +43 -0
  103. package/dist/modules/wms/api/sales-orders/unassign-warehouse/route.js.map +7 -0
  104. package/dist/modules/wms/api/warehouses/route.js +181 -0
  105. package/dist/modules/wms/api/warehouses/route.js.map +7 -0
  106. package/dist/modules/wms/api/zones/route.js +159 -0
  107. package/dist/modules/wms/api/zones/route.js.map +7 -0
  108. package/dist/modules/wms/backend/config/wms/page.js +9 -0
  109. package/dist/modules/wms/backend/config/wms/page.js.map +7 -0
  110. package/dist/modules/wms/backend/config/wms/page.meta.js +18 -0
  111. package/dist/modules/wms/backend/config/wms/page.meta.js.map +7 -0
  112. package/dist/modules/wms/backend/page.js +9 -0
  113. package/dist/modules/wms/backend/page.js.map +7 -0
  114. package/dist/modules/wms/backend/page.meta.js +16 -0
  115. package/dist/modules/wms/backend/page.meta.js.map +7 -0
  116. package/dist/modules/wms/backend/wms/inventory/page.js +9 -0
  117. package/dist/modules/wms/backend/wms/inventory/page.js.map +7 -0
  118. package/dist/modules/wms/backend/wms/inventory/page.meta.js +18 -0
  119. package/dist/modules/wms/backend/wms/inventory/page.meta.js.map +7 -0
  120. package/dist/modules/wms/backend/wms/location/[id]/page.js +9 -0
  121. package/dist/modules/wms/backend/wms/location/[id]/page.js.map +7 -0
  122. package/dist/modules/wms/backend/wms/location/[id]/page.meta.js +18 -0
  123. package/dist/modules/wms/backend/wms/location/[id]/page.meta.js.map +7 -0
  124. package/dist/modules/wms/backend/wms/locations/page.js +10 -0
  125. package/dist/modules/wms/backend/wms/locations/page.js.map +7 -0
  126. package/dist/modules/wms/backend/wms/locations/page.meta.js +18 -0
  127. package/dist/modules/wms/backend/wms/locations/page.meta.js.map +7 -0
  128. package/dist/modules/wms/backend/wms/lot/[id]/page.js +9 -0
  129. package/dist/modules/wms/backend/wms/lot/[id]/page.js.map +7 -0
  130. package/dist/modules/wms/backend/wms/lot/[id]/page.meta.js +18 -0
  131. package/dist/modules/wms/backend/wms/lot/[id]/page.meta.js.map +7 -0
  132. package/dist/modules/wms/backend/wms/lots/page.js +9 -0
  133. package/dist/modules/wms/backend/wms/lots/page.js.map +7 -0
  134. package/dist/modules/wms/backend/wms/lots/page.meta.js +18 -0
  135. package/dist/modules/wms/backend/wms/lots/page.meta.js.map +7 -0
  136. package/dist/modules/wms/backend/wms/movements/page.js +22 -0
  137. package/dist/modules/wms/backend/wms/movements/page.js.map +7 -0
  138. package/dist/modules/wms/backend/wms/movements/page.meta.js +18 -0
  139. package/dist/modules/wms/backend/wms/movements/page.meta.js.map +7 -0
  140. package/dist/modules/wms/backend/wms/reservations/page.js +23 -0
  141. package/dist/modules/wms/backend/wms/reservations/page.js.map +7 -0
  142. package/dist/modules/wms/backend/wms/reservations/page.meta.js +18 -0
  143. package/dist/modules/wms/backend/wms/reservations/page.meta.js.map +7 -0
  144. package/dist/modules/wms/backend/wms/sku/[id]/page.js +9 -0
  145. package/dist/modules/wms/backend/wms/sku/[id]/page.js.map +7 -0
  146. package/dist/modules/wms/backend/wms/sku/[id]/page.meta.js +18 -0
  147. package/dist/modules/wms/backend/wms/sku/[id]/page.meta.js.map +7 -0
  148. package/dist/modules/wms/backend/wms/warehouses/page.js +10 -0
  149. package/dist/modules/wms/backend/wms/warehouses/page.js.map +7 -0
  150. package/dist/modules/wms/backend/wms/warehouses/page.meta.js +18 -0
  151. package/dist/modules/wms/backend/wms/warehouses/page.meta.js.map +7 -0
  152. package/dist/modules/wms/backend/wms/zones/page.js +10 -0
  153. package/dist/modules/wms/backend/wms/zones/page.js.map +7 -0
  154. package/dist/modules/wms/backend/wms/zones/page.meta.js +18 -0
  155. package/dist/modules/wms/backend/wms/zones/page.meta.js.map +7 -0
  156. package/dist/modules/wms/ce.js +80 -0
  157. package/dist/modules/wms/ce.js.map +7 -0
  158. package/dist/modules/wms/cli.js +83 -0
  159. package/dist/modules/wms/cli.js.map +7 -0
  160. package/dist/modules/wms/commands/configuration.js +1519 -0
  161. package/dist/modules/wms/commands/configuration.js.map +7 -0
  162. package/dist/modules/wms/commands/index.js +4 -0
  163. package/dist/modules/wms/commands/index.js.map +7 -0
  164. package/dist/modules/wms/commands/inventory-actions.js +1539 -0
  165. package/dist/modules/wms/commands/inventory-actions.js.map +7 -0
  166. package/dist/modules/wms/commands/sales-order-assignment.js +277 -0
  167. package/dist/modules/wms/commands/sales-order-assignment.js.map +7 -0
  168. package/dist/modules/wms/commands/shared.js +82 -0
  169. package/dist/modules/wms/commands/shared.js.map +7 -0
  170. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js +755 -0
  171. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js.map +7 -0
  172. package/dist/modules/wms/components/backend/ChangeLotStatusDialog.js +269 -0
  173. package/dist/modules/wms/components/backend/ChangeLotStatusDialog.js.map +7 -0
  174. package/dist/modules/wms/components/backend/CycleCountWizardDialog.js +1783 -0
  175. package/dist/modules/wms/components/backend/CycleCountWizardDialog.js.map +7 -0
  176. package/dist/modules/wms/components/backend/ImportInventoryDialog.js +766 -0
  177. package/dist/modules/wms/components/backend/ImportInventoryDialog.js.map +7 -0
  178. package/dist/modules/wms/components/backend/InventoryOperationsSection.js +119 -0
  179. package/dist/modules/wms/components/backend/InventoryOperationsSection.js.map +7 -0
  180. package/dist/modules/wms/components/backend/LocationEditDialog.js +140 -0
  181. package/dist/modules/wms/components/backend/LocationEditDialog.js.map +7 -0
  182. package/dist/modules/wms/components/backend/MoveInventoryDialog.js +791 -0
  183. package/dist/modules/wms/components/backend/MoveInventoryDialog.js.map +7 -0
  184. package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js +656 -0
  185. package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js.map +7 -0
  186. package/dist/modules/wms/components/backend/ReleaseReservationDialog.js +344 -0
  187. package/dist/modules/wms/components/backend/ReleaseReservationDialog.js.map +7 -0
  188. package/dist/modules/wms/components/backend/ReserveInventoryDialog.js +475 -0
  189. package/dist/modules/wms/components/backend/ReserveInventoryDialog.js.map +7 -0
  190. package/dist/modules/wms/components/backend/WmsConfigurationPage.js +1112 -0
  191. package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +7 -0
  192. package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js +908 -0
  193. package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +7 -0
  194. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +1015 -0
  195. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +7 -0
  196. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +1049 -0
  197. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +7 -0
  198. package/dist/modules/wms/components/backend/WmsLotsListPage.js +297 -0
  199. package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +7 -0
  200. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js +757 -0
  201. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +7 -0
  202. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +975 -0
  203. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +7 -0
  204. package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +710 -0
  205. package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +7 -0
  206. package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js +79 -0
  207. package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js.map +7 -0
  208. package/dist/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.js +24 -0
  209. package/dist/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.js.map +7 -0
  210. package/dist/modules/wms/components/backend/wmsLookupLoaders.js +55 -0
  211. package/dist/modules/wms/components/backend/wmsLookupLoaders.js.map +7 -0
  212. package/dist/modules/wms/data/enrichers.js +501 -0
  213. package/dist/modules/wms/data/enrichers.js.map +7 -0
  214. package/dist/modules/wms/data/entities.js +467 -0
  215. package/dist/modules/wms/data/entities.js.map +7 -0
  216. package/dist/modules/wms/data/validators.js +399 -0
  217. package/dist/modules/wms/data/validators.js.map +7 -0
  218. package/dist/modules/wms/di.js +27 -0
  219. package/dist/modules/wms/di.js.map +7 -0
  220. package/dist/modules/wms/events.js +42 -0
  221. package/dist/modules/wms/events.js.map +7 -0
  222. package/dist/modules/wms/index.js +17 -0
  223. package/dist/modules/wms/index.js.map +7 -0
  224. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +54 -0
  225. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +7 -0
  226. package/dist/modules/wms/lib/catalogInventoryProfileSync.js +144 -0
  227. package/dist/modules/wms/lib/catalogInventoryProfileSync.js.map +7 -0
  228. package/dist/modules/wms/lib/expiry.js +29 -0
  229. package/dist/modules/wms/lib/expiry.js.map +7 -0
  230. package/dist/modules/wms/lib/flashMutationError.js +51 -0
  231. package/dist/modules/wms/lib/flashMutationError.js.map +7 -0
  232. package/dist/modules/wms/lib/inventoryDisplayUi.js +188 -0
  233. package/dist/modules/wms/lib/inventoryDisplayUi.js.map +7 -0
  234. package/dist/modules/wms/lib/inventoryIdempotency.js +37 -0
  235. package/dist/modules/wms/lib/inventoryIdempotency.js.map +7 -0
  236. package/dist/modules/wms/lib/inventoryImportCsv.js +173 -0
  237. package/dist/modules/wms/lib/inventoryImportCsv.js.map +7 -0
  238. package/dist/modules/wms/lib/inventoryImportService.js +468 -0
  239. package/dist/modules/wms/lib/inventoryImportService.js.map +7 -0
  240. package/dist/modules/wms/lib/inventoryMutationUi.js +25 -0
  241. package/dist/modules/wms/lib/inventoryMutationUi.js.map +7 -0
  242. package/dist/modules/wms/lib/inventoryPolicy.js +61 -0
  243. package/dist/modules/wms/lib/inventoryPolicy.js.map +7 -0
  244. package/dist/modules/wms/lib/inventoryReconciliation.js +193 -0
  245. package/dist/modules/wms/lib/inventoryReconciliation.js.map +7 -0
  246. package/dist/modules/wms/lib/inventoryTrackingValidation.js +13 -0
  247. package/dist/modules/wms/lib/inventoryTrackingValidation.js.map +7 -0
  248. package/dist/modules/wms/lib/loadOperationalDashboard.js +580 -0
  249. package/dist/modules/wms/lib/loadOperationalDashboard.js.map +7 -0
  250. package/dist/modules/wms/lib/lowStockBalanceFilter.js +49 -0
  251. package/dist/modules/wms/lib/lowStockBalanceFilter.js.map +7 -0
  252. package/dist/modules/wms/lib/primaryWarehousePolicy.js +32 -0
  253. package/dist/modules/wms/lib/primaryWarehousePolicy.js.map +7 -0
  254. package/dist/modules/wms/lib/roleFeatures.js +30 -0
  255. package/dist/modules/wms/lib/roleFeatures.js.map +7 -0
  256. package/dist/modules/wms/lib/salesOrderInventoryAutomation.js +291 -0
  257. package/dist/modules/wms/lib/salesOrderInventoryAutomation.js.map +7 -0
  258. package/dist/modules/wms/lib/salesOrderWarehouseAssignment.js +54 -0
  259. package/dist/modules/wms/lib/salesOrderWarehouseAssignment.js.map +7 -0
  260. package/dist/modules/wms/lib/wmsIntegrationToggles.js +71 -0
  261. package/dist/modules/wms/lib/wmsIntegrationToggles.js.map +7 -0
  262. package/dist/modules/wms/migrations/Migration20260428110546.js +78 -0
  263. package/dist/modules/wms/migrations/Migration20260428110546.js.map +7 -0
  264. package/dist/modules/wms/migrations/Migration20260527120000.js +15 -0
  265. package/dist/modules/wms/migrations/Migration20260527120000.js.map +7 -0
  266. package/dist/modules/wms/migrations/Migration20260527140000.js +37 -0
  267. package/dist/modules/wms/migrations/Migration20260527140000.js.map +7 -0
  268. package/dist/modules/wms/migrations/Migration20260613120000.js +23 -0
  269. package/dist/modules/wms/migrations/Migration20260613120000.js.map +7 -0
  270. package/dist/modules/wms/migrations/Migration20260616090000.js +13 -0
  271. package/dist/modules/wms/migrations/Migration20260616090000.js.map +7 -0
  272. package/dist/modules/wms/migrations/Migration20260707180000.js +15 -0
  273. package/dist/modules/wms/migrations/Migration20260707180000.js.map +7 -0
  274. package/dist/modules/wms/notifications.client.js +18 -0
  275. package/dist/modules/wms/notifications.client.js.map +7 -0
  276. package/dist/modules/wms/notifications.js +53 -0
  277. package/dist/modules/wms/notifications.js.map +7 -0
  278. package/dist/modules/wms/search.js +196 -0
  279. package/dist/modules/wms/search.js.map +7 -0
  280. package/dist/modules/wms/setup.js +30 -0
  281. package/dist/modules/wms/setup.js.map +7 -0
  282. package/dist/modules/wms/subscribers/low-stock-notification.js +40 -0
  283. package/dist/modules/wms/subscribers/low-stock-notification.js.map +7 -0
  284. package/dist/modules/wms/subscribers/reservation-shortfall-notification.js +69 -0
  285. package/dist/modules/wms/subscribers/reservation-shortfall-notification.js.map +7 -0
  286. package/dist/modules/wms/subscribers/sales-order-cancelled-release.js +14 -0
  287. package/dist/modules/wms/subscribers/sales-order-cancelled-release.js.map +7 -0
  288. package/dist/modules/wms/subscribers/sales-order-confirmed-reserve.js +14 -0
  289. package/dist/modules/wms/subscribers/sales-order-confirmed-reserve.js.map +7 -0
  290. package/dist/modules/wms/translations.js +10 -0
  291. package/dist/modules/wms/translations.js.map +7 -0
  292. package/dist/modules/wms/vector.js +9 -0
  293. package/dist/modules/wms/vector.js.map +7 -0
  294. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +200 -0
  295. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +7 -0
  296. package/dist/modules/wms/widgets/injection/order-items-stock-column/widget.js +104 -0
  297. package/dist/modules/wms/widgets/injection/order-items-stock-column/widget.js.map +7 -0
  298. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js +389 -0
  299. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js.map +7 -0
  300. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.js +16 -0
  301. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.js.map +7 -0
  302. package/dist/modules/wms/widgets/injection-table.js +36 -0
  303. package/dist/modules/wms/widgets/injection-table.js.map +7 -0
  304. package/dist/modules/wms/widgets/notifications/WmsLowStockRenderer.js +150 -0
  305. package/dist/modules/wms/widgets/notifications/WmsLowStockRenderer.js.map +7 -0
  306. package/dist/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.js +186 -0
  307. package/dist/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.js.map +7 -0
  308. package/dist/modules/wms/widgets/notifications/index.js +7 -0
  309. package/dist/modules/wms/widgets/notifications/index.js.map +7 -0
  310. package/generated/entities/inventory_balance/index.ts +9 -0
  311. package/generated/entities/inventory_lot/index.ts +8 -0
  312. package/generated/entities/inventory_movement/index.ts +16 -0
  313. package/generated/entities/inventory_reservation/index.ts +10 -0
  314. package/generated/entities/product_inventory_profile/index.ts +9 -0
  315. package/generated/entities/sales_order_warehouse_assignment/index.ts +4 -0
  316. package/generated/entities/warehouse/index.ts +11 -0
  317. package/generated/entities/warehouse_location/index.ts +9 -0
  318. package/generated/entities/warehouse_zone/index.ts +4 -0
  319. package/generated/entities.ids.generated.ts +12 -0
  320. package/generated/entity-fields-registry.ts +98 -0
  321. package/package.json +7 -7
  322. package/src/modules/auth/backend/roles/page.tsx +13 -4
  323. package/src/modules/auth/i18n/de.json +2 -0
  324. package/src/modules/auth/i18n/en.json +2 -0
  325. package/src/modules/auth/i18n/es.json +2 -0
  326. package/src/modules/auth/i18n/pl.json +2 -0
  327. package/src/modules/auth/lib/backendChrome.tsx +1 -0
  328. package/src/modules/auth/services/rbacService.ts +25 -21
  329. package/src/modules/catalog/api/products/route.ts +3 -0
  330. package/src/modules/catalog/api/variants/route.ts +3 -0
  331. package/src/modules/query_index/lib/engine.ts +18 -6
  332. package/src/modules/sales/api/documents/factory.ts +1 -0
  333. package/src/modules/sales/commands/documents.ts +71 -0
  334. package/src/modules/sales/components/documents/ItemsSection.tsx +58 -0
  335. package/src/modules/sales/events.ts +2 -0
  336. package/src/modules/sales/widgets/injection/order-items-context.ts +15 -0
  337. package/src/modules/wms/acl.ts +14 -0
  338. package/src/modules/wms/analytics.ts +9 -0
  339. package/src/modules/wms/api/dashboard/operational/route.ts +83 -0
  340. package/src/modules/wms/api/interceptors.ts +108 -0
  341. package/src/modules/wms/api/inventory/adjust/route.ts +50 -0
  342. package/src/modules/wms/api/inventory/allocate/route.ts +50 -0
  343. package/src/modules/wms/api/inventory/balances/route.ts +178 -0
  344. package/src/modules/wms/api/inventory/cycle-count/route.ts +51 -0
  345. package/src/modules/wms/api/inventory/helpers.ts +128 -0
  346. package/src/modules/wms/api/inventory/import/apply/route.ts +47 -0
  347. package/src/modules/wms/api/inventory/import/helpers.ts +223 -0
  348. package/src/modules/wms/api/inventory/import/template/route.ts +31 -0
  349. package/src/modules/wms/api/inventory/import/validate/route.ts +56 -0
  350. package/src/modules/wms/api/inventory/move/route.ts +50 -0
  351. package/src/modules/wms/api/inventory/movements/route.ts +164 -0
  352. package/src/modules/wms/api/inventory/receive/route.ts +49 -0
  353. package/src/modules/wms/api/inventory/release/route.ts +43 -0
  354. package/src/modules/wms/api/inventory/reservations/route.ts +128 -0
  355. package/src/modules/wms/api/inventory/reserve/route.ts +59 -0
  356. package/src/modules/wms/api/inventory-profiles/route.ts +177 -0
  357. package/src/modules/wms/api/listEnrichers.ts +312 -0
  358. package/src/modules/wms/api/listSearch.ts +121 -0
  359. package/src/modules/wms/api/locations/route.ts +171 -0
  360. package/src/modules/wms/api/lots/route.ts +205 -0
  361. package/src/modules/wms/api/openapi.ts +28 -0
  362. package/src/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.ts +58 -0
  363. package/src/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.ts +306 -0
  364. package/src/modules/wms/api/sales-orders/assign-warehouse/route.ts +57 -0
  365. package/src/modules/wms/api/sales-orders/unassign-warehouse/route.ts +43 -0
  366. package/src/modules/wms/api/warehouses/route.ts +181 -0
  367. package/src/modules/wms/api/zones/route.ts +158 -0
  368. package/src/modules/wms/backend/config/wms/page.meta.ts +14 -0
  369. package/src/modules/wms/backend/config/wms/page.tsx +5 -0
  370. package/src/modules/wms/backend/page.meta.ts +12 -0
  371. package/src/modules/wms/backend/page.tsx +5 -0
  372. package/src/modules/wms/backend/wms/inventory/page.meta.ts +14 -0
  373. package/src/modules/wms/backend/wms/inventory/page.tsx +5 -0
  374. package/src/modules/wms/backend/wms/location/[id]/page.meta.ts +14 -0
  375. package/src/modules/wms/backend/wms/location/[id]/page.tsx +5 -0
  376. package/src/modules/wms/backend/wms/locations/page.meta.ts +14 -0
  377. package/src/modules/wms/backend/wms/locations/page.tsx +12 -0
  378. package/src/modules/wms/backend/wms/lot/[id]/page.meta.ts +14 -0
  379. package/src/modules/wms/backend/wms/lot/[id]/page.tsx +5 -0
  380. package/src/modules/wms/backend/wms/lots/page.meta.ts +14 -0
  381. package/src/modules/wms/backend/wms/lots/page.tsx +5 -0
  382. package/src/modules/wms/backend/wms/movements/page.meta.ts +14 -0
  383. package/src/modules/wms/backend/wms/movements/page.tsx +23 -0
  384. package/src/modules/wms/backend/wms/reservations/page.meta.ts +14 -0
  385. package/src/modules/wms/backend/wms/reservations/page.tsx +24 -0
  386. package/src/modules/wms/backend/wms/sku/[id]/page.meta.ts +14 -0
  387. package/src/modules/wms/backend/wms/sku/[id]/page.tsx +5 -0
  388. package/src/modules/wms/backend/wms/warehouses/page.meta.ts +14 -0
  389. package/src/modules/wms/backend/wms/warehouses/page.tsx +12 -0
  390. package/src/modules/wms/backend/wms/zones/page.meta.ts +14 -0
  391. package/src/modules/wms/backend/wms/zones/page.tsx +12 -0
  392. package/src/modules/wms/ce.ts +76 -0
  393. package/src/modules/wms/cli.ts +96 -0
  394. package/src/modules/wms/commands/configuration.ts +1844 -0
  395. package/src/modules/wms/commands/index.ts +3 -0
  396. package/src/modules/wms/commands/inventory-actions.ts +1866 -0
  397. package/src/modules/wms/commands/sales-order-assignment.ts +346 -0
  398. package/src/modules/wms/commands/shared.ts +90 -0
  399. package/src/modules/wms/components/backend/AdjustInventoryDialog.tsx +820 -0
  400. package/src/modules/wms/components/backend/ChangeLotStatusDialog.tsx +312 -0
  401. package/src/modules/wms/components/backend/CycleCountWizardDialog.tsx +2007 -0
  402. package/src/modules/wms/components/backend/ImportInventoryDialog.tsx +1018 -0
  403. package/src/modules/wms/components/backend/InventoryOperationsSection.tsx +159 -0
  404. package/src/modules/wms/components/backend/LocationEditDialog.tsx +185 -0
  405. package/src/modules/wms/components/backend/MoveInventoryDialog.tsx +907 -0
  406. package/src/modules/wms/components/backend/ReceiveInventoryDialog.tsx +695 -0
  407. package/src/modules/wms/components/backend/ReleaseReservationDialog.tsx +424 -0
  408. package/src/modules/wms/components/backend/ReserveInventoryDialog.tsx +511 -0
  409. package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +1340 -0
  410. package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +1132 -0
  411. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +1310 -0
  412. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +1355 -0
  413. package/src/modules/wms/components/backend/WmsLotsListPage.tsx +359 -0
  414. package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +958 -0
  415. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +1245 -0
  416. package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +960 -0
  417. package/src/modules/wms/components/backend/useWmsInventoryMutationAccess.ts +89 -0
  418. package/src/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.ts +32 -0
  419. package/src/modules/wms/components/backend/wmsLookupLoaders.ts +78 -0
  420. package/src/modules/wms/data/enrichers.ts +718 -0
  421. package/src/modules/wms/data/entities.ts +436 -0
  422. package/src/modules/wms/data/validators.ts +463 -0
  423. package/src/modules/wms/di.ts +25 -0
  424. package/src/modules/wms/events.ts +41 -0
  425. package/src/modules/wms/i18n/de.json +1051 -0
  426. package/src/modules/wms/i18n/en.json +1051 -0
  427. package/src/modules/wms/i18n/es.json +1051 -0
  428. package/src/modules/wms/i18n/pl.json +1051 -0
  429. package/src/modules/wms/index.ts +15 -0
  430. package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +65 -0
  431. package/src/modules/wms/lib/catalogInventoryProfileSync.ts +199 -0
  432. package/src/modules/wms/lib/expiry.ts +30 -0
  433. package/src/modules/wms/lib/flashMutationError.ts +74 -0
  434. package/src/modules/wms/lib/inventoryDisplayUi.ts +249 -0
  435. package/src/modules/wms/lib/inventoryIdempotency.ts +54 -0
  436. package/src/modules/wms/lib/inventoryImportCsv.ts +212 -0
  437. package/src/modules/wms/lib/inventoryImportService.ts +629 -0
  438. package/src/modules/wms/lib/inventoryMutationUi.ts +21 -0
  439. package/src/modules/wms/lib/inventoryPolicy.ts +111 -0
  440. package/src/modules/wms/lib/inventoryReconciliation.ts +263 -0
  441. package/src/modules/wms/lib/inventoryTrackingValidation.ts +23 -0
  442. package/src/modules/wms/lib/loadOperationalDashboard.ts +776 -0
  443. package/src/modules/wms/lib/lowStockBalanceFilter.ts +74 -0
  444. package/src/modules/wms/lib/primaryWarehousePolicy.ts +46 -0
  445. package/src/modules/wms/lib/roleFeatures.ts +32 -0
  446. package/src/modules/wms/lib/salesOrderInventoryAutomation.ts +390 -0
  447. package/src/modules/wms/lib/salesOrderWarehouseAssignment.ts +78 -0
  448. package/src/modules/wms/lib/wmsIntegrationToggles.ts +81 -0
  449. package/src/modules/wms/migrations/.snapshot-open-mercato.json +2880 -0
  450. package/src/modules/wms/migrations/Migration20260428110546.ts +91 -0
  451. package/src/modules/wms/migrations/Migration20260527120000.ts +15 -0
  452. package/src/modules/wms/migrations/Migration20260527140000.ts +35 -0
  453. package/src/modules/wms/migrations/Migration20260613120000.ts +21 -0
  454. package/src/modules/wms/migrations/Migration20260616090000.ts +11 -0
  455. package/src/modules/wms/migrations/Migration20260707180000.ts +13 -0
  456. package/src/modules/wms/notifications.client.ts +18 -0
  457. package/src/modules/wms/notifications.ts +51 -0
  458. package/src/modules/wms/search.ts +230 -0
  459. package/src/modules/wms/setup.ts +29 -0
  460. package/src/modules/wms/subscribers/low-stock-notification.ts +54 -0
  461. package/src/modules/wms/subscribers/reservation-shortfall-notification.ts +98 -0
  462. package/src/modules/wms/subscribers/sales-order-cancelled-release.ts +21 -0
  463. package/src/modules/wms/subscribers/sales-order-confirmed-reserve.ts +21 -0
  464. package/src/modules/wms/translations.ts +6 -0
  465. package/src/modules/wms/vector.ts +7 -0
  466. package/src/modules/wms/widgets/injection/catalog-inventory-profile/widget.ts +238 -0
  467. package/src/modules/wms/widgets/injection/order-items-stock-column/widget.tsx +153 -0
  468. package/src/modules/wms/widgets/injection/sales-order-stock-context/widget.client.tsx +476 -0
  469. package/src/modules/wms/widgets/injection/sales-order-stock-context/widget.ts +15 -0
  470. package/src/modules/wms/widgets/injection-table.ts +34 -0
  471. package/src/modules/wms/widgets/notifications/WmsLowStockRenderer.tsx +153 -0
  472. package/src/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.tsx +194 -0
  473. package/src/modules/wms/widgets/notifications/index.ts +2 -0
@@ -0,0 +1,1245 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+ import { z } from 'zod'
7
+ import { useQuery } from '@tanstack/react-query'
8
+ import type { ColumnDef } from '@tanstack/react-table'
9
+ import {
10
+ ArrowDown,
11
+ ArrowLeft,
12
+ ArrowLeftRight,
13
+ ClipboardList,
14
+ Download,
15
+ ExternalLink,
16
+ SlidersHorizontal,
17
+ Warehouse,
18
+ } from 'lucide-react'
19
+ import { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'
20
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
21
+ import { EmptyState } from '@open-mercato/ui/backend/EmptyState'
22
+ import { ErrorMessage, LoadingMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
23
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
24
+ import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
25
+ import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
26
+ import { Button } from '@open-mercato/ui/primitives/button'
27
+ import { LinkButton } from '@open-mercato/ui/primitives/link-button'
28
+ import {
29
+ Select,
30
+ SelectContent,
31
+ SelectItem,
32
+ SelectTrigger,
33
+ SelectValue,
34
+ } from '@open-mercato/ui/primitives/select'
35
+ import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
36
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
37
+ import { cn } from '@open-mercato/shared/lib/utils'
38
+ import { E } from '#generated/entities.ids.generated'
39
+ import {
40
+ inventoryMovementReasonLabel,
41
+ type InventoryDisplayTranslator,
42
+ } from '../../lib/inventoryDisplayUi'
43
+ import { AdjustInventoryDialog } from './AdjustInventoryDialog'
44
+ import { CycleCountWizardDialog } from './CycleCountWizardDialog'
45
+ import { ReceiveInventoryDialog } from './ReceiveInventoryDialog'
46
+ import { useWmsInventoryMutationAccess } from './useWmsInventoryMutationAccess'
47
+
48
+ const variantIdSchema = z.string().uuid()
49
+
50
+ type PagedResponse<T> = {
51
+ items: T[]
52
+ total: number
53
+ totalPages: number
54
+ }
55
+
56
+ type CatalogVariantRow = {
57
+ id: string
58
+ sku?: string | null
59
+ name?: string | null
60
+ is_active?: boolean | null
61
+ created_at?: string | null
62
+ }
63
+
64
+ type InventoryProfileRow = {
65
+ reorder_point?: string | number | null
66
+ safety_stock?: string | number | null
67
+ }
68
+
69
+ type InventoryBalanceRow = {
70
+ id: string
71
+ warehouse_id?: string | null
72
+ warehouse_name?: string | null
73
+ warehouse_code?: string | null
74
+ location_id?: string | null
75
+ location_code?: string | null
76
+ location_type?: string | null
77
+ lot_id?: string | null
78
+ quantity_on_hand?: string | number | null
79
+ quantity_reserved?: string | number | null
80
+ quantity_allocated?: string | number | null
81
+ quantity_available?: number | null
82
+ }
83
+
84
+ type InventoryLotRow = {
85
+ id: string
86
+ lot_number?: string | null
87
+ expires_at?: string | null
88
+ status?: string | null
89
+ }
90
+
91
+ type InventoryMovementRow = {
92
+ id: string
93
+ warehouse_id?: string | null
94
+ warehouse_name?: string | null
95
+ warehouse_code?: string | null
96
+ location_from_id?: string | null
97
+ location_from_code?: string | null
98
+ location_to_id?: string | null
99
+ location_to_code?: string | null
100
+ catalog_variant_id?: string | null
101
+ variant_sku?: string | null
102
+ quantity?: string | number | null
103
+ type?: string | null
104
+ reference_type?: string | null
105
+ reference_id?: string | null
106
+ reason?: string | null
107
+ reason_code?: string | null
108
+ performed_at?: string | null
109
+ received_at?: string | null
110
+ }
111
+
112
+ type InventoryMutationPreset = {
113
+ warehouseId?: string
114
+ locationId?: string
115
+ lotId?: string
116
+ }
117
+
118
+ function escapeCsvCell(value: string | number): string {
119
+ const str = String(value ?? '')
120
+ if (/[",\n]/.test(str)) return `"${str.replace(/"/g, '""')}"`
121
+ return str
122
+ }
123
+
124
+ function downloadCsvFile(filename: string, rows: string[][]) {
125
+ const csv = rows.map((row) => row.map(escapeCsvCell).join(',')).join('\n')
126
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
127
+ const url = URL.createObjectURL(blob)
128
+ const anchor = document.createElement('a')
129
+ anchor.href = url
130
+ anchor.download = filename
131
+ anchor.click()
132
+ URL.revokeObjectURL(url)
133
+ }
134
+ type WarehouseOption = {
135
+ id: string
136
+ name?: string | null
137
+ code?: string | null
138
+ }
139
+
140
+ type DistributionFilter = 'all' | 'sellable' | 'picking' | 'nearExpiry'
141
+
142
+ const NON_SELLABLE_LOCATION_TYPES = new Set(['staging', 'dock'])
143
+ const PICKING_LOCATION_TYPES = new Set(['staging', 'bin', 'slot'])
144
+ const NEAR_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000
145
+ const USAGE_WINDOW_DAYS = 14
146
+
147
+ function toNumber(value: string | number | null | undefined): number {
148
+ const parsed = Number(value ?? 0)
149
+ return Number.isFinite(parsed) ? parsed : 0
150
+ }
151
+
152
+ function formatWarehouseLabel(row: {
153
+ warehouse_name?: string | null
154
+ warehouse_code?: string | null
155
+ warehouse_id?: string | null
156
+ }): string {
157
+ const code = (row.warehouse_code ?? '').trim()
158
+ const name = (row.warehouse_name ?? '').trim()
159
+ if (code && name) return `${code} · ${name}`
160
+ return name || code || row.warehouse_id || '—'
161
+ }
162
+
163
+ function formatLocationLabel(code: string | null | undefined, id: string | null | undefined): string {
164
+ const trimmed = (code ?? '').trim()
165
+ if (trimmed) return trimmed
166
+ return id || '—'
167
+ }
168
+
169
+ function formatLotLabel(
170
+ lot: InventoryLotRow | undefined,
171
+ locale: string,
172
+ ): string {
173
+ if (!lot) return '—'
174
+ const number = (lot.lot_number ?? '').trim() || lot.id
175
+ if (!lot.expires_at) return number
176
+ const expires = new Date(lot.expires_at)
177
+ if (Number.isNaN(expires.getTime())) return number
178
+ const expLabel = new Intl.DateTimeFormat(locale, { month: '2-digit', year: '2-digit' }).format(expires)
179
+ return `${number} · exp ${expLabel}`
180
+ }
181
+
182
+ function isNearExpiry(expiresAt: string | null | undefined, nowMs: number): boolean {
183
+ if (!expiresAt) return false
184
+ const expires = new Date(expiresAt).getTime()
185
+ if (Number.isNaN(expires)) return false
186
+ return expires > nowMs && expires - nowMs <= NEAR_EXPIRY_MS
187
+ }
188
+
189
+ function isExpired(expiresAt: string | null | undefined, nowMs: number): boolean {
190
+ if (!expiresAt) return false
191
+ const expires = new Date(expiresAt).getTime()
192
+ return !Number.isNaN(expires) && expires <= nowMs
193
+ }
194
+
195
+ function resolveBalanceStatus(
196
+ row: InventoryBalanceRow,
197
+ lot: InventoryLotRow | undefined,
198
+ reorderPoint: number,
199
+ nowMs: number,
200
+ ): { variant: StatusBadgeVariant; labelKey: string; labelFallback: string } {
201
+ if (lot?.status === 'expired' || isExpired(lot?.expires_at, nowMs)) {
202
+ return {
203
+ variant: 'error',
204
+ labelKey: 'wms.backend.sku.distribution.status.expired',
205
+ labelFallback: 'Expired',
206
+ }
207
+ }
208
+ if (isNearExpiry(lot?.expires_at, nowMs)) {
209
+ return {
210
+ variant: 'warning',
211
+ labelKey: 'wms.backend.sku.distribution.status.nearExpiry',
212
+ labelFallback: 'Near expiry',
213
+ }
214
+ }
215
+ const available = row.quantity_available ?? 0
216
+ if (reorderPoint > 0 && available <= reorderPoint) {
217
+ return {
218
+ variant: 'warning',
219
+ labelKey: 'wms.backend.sku.distribution.status.lowStock',
220
+ labelFallback: 'Low stock',
221
+ }
222
+ }
223
+ const reserved = toNumber(row.quantity_reserved)
224
+ const onHand = toNumber(row.quantity_on_hand)
225
+ if (reserved > 0 && onHand > 0 && reserved >= onHand) {
226
+ return {
227
+ variant: 'info',
228
+ labelKey: 'wms.backend.sku.distribution.status.reserved',
229
+ labelFallback: 'Reserved',
230
+ }
231
+ }
232
+ return {
233
+ variant: 'success',
234
+ labelKey: 'wms.backend.sku.distribution.status.available',
235
+ labelFallback: 'Available',
236
+ }
237
+ }
238
+
239
+ function matchesDistributionFilter(
240
+ row: InventoryBalanceRow,
241
+ lot: InventoryLotRow | undefined,
242
+ filter: DistributionFilter,
243
+ nowMs: number,
244
+ ): boolean {
245
+ const locationType = (row.location_type ?? '').trim().toLowerCase()
246
+ const locationCode = (row.location_code ?? '').trim().toLowerCase()
247
+ switch (filter) {
248
+ case 'all':
249
+ return true
250
+ case 'sellable':
251
+ return !NON_SELLABLE_LOCATION_TYPES.has(locationType)
252
+ case 'picking':
253
+ return (
254
+ PICKING_LOCATION_TYPES.has(locationType) ||
255
+ locationCode.includes('pick') ||
256
+ locationCode.includes('staging')
257
+ )
258
+ case 'nearExpiry':
259
+ return isNearExpiry(lot?.expires_at, nowMs) || lot?.status === 'expired'
260
+ default:
261
+ return true
262
+ }
263
+ }
264
+
265
+ function movementTypeLabel(type: string, t: ReturnType<typeof useT>): string {
266
+ const key = `wms.backend.sku.activity.types.${type}`
267
+ const fallbacks: Record<string, string> = {
268
+ receipt: 'Receive',
269
+ return_receive: 'Receive',
270
+ adjust: 'Adjust',
271
+ transfer: 'Move',
272
+ pick: 'Allocate',
273
+ pack: 'Allocate',
274
+ cycle_count: 'Reconcile',
275
+ putaway: 'Putaway',
276
+ ship: 'Ship',
277
+ }
278
+ return t(key, fallbacks[type] ?? type)
279
+ }
280
+
281
+ const movementStatusMap: Record<string, StatusBadgeVariant> = {
282
+ receipt: 'success',
283
+ return_receive: 'success',
284
+ adjust: 'warning',
285
+ transfer: 'info',
286
+ pick: 'info',
287
+ pack: 'info',
288
+ cycle_count: 'neutral',
289
+ putaway: 'info',
290
+ ship: 'success',
291
+ }
292
+
293
+ function formatMovementTitle(
294
+ row: InventoryMovementRow,
295
+ skuLabel: string,
296
+ t: ReturnType<typeof useT>,
297
+ ): string {
298
+ const quantity = Math.abs(toNumber(row.quantity))
299
+ const signedQuantity = toNumber(row.quantity)
300
+ switch (row.type) {
301
+ case 'receipt':
302
+ case 'return_receive':
303
+ return t('wms.backend.dashboard.activity.titles.received', 'Received {quantity}× {sku}', {
304
+ quantity,
305
+ sku: skuLabel,
306
+ })
307
+ case 'adjust':
308
+ return t('wms.backend.dashboard.activity.titles.adjusted', 'Adjusted {quantity}× {sku}', {
309
+ quantity: `${signedQuantity >= 0 ? '+' : ''}${signedQuantity}`,
310
+ sku: skuLabel,
311
+ })
312
+ case 'transfer':
313
+ return t('wms.backend.dashboard.activity.titles.moved', 'Moved {quantity}× {sku}', {
314
+ quantity,
315
+ sku: skuLabel,
316
+ })
317
+ case 'pick':
318
+ case 'pack':
319
+ return t('wms.backend.dashboard.activity.titles.allocated', 'Allocated {quantity}× {sku}', {
320
+ quantity,
321
+ sku: skuLabel,
322
+ })
323
+ case 'cycle_count':
324
+ return t('wms.backend.dashboard.activity.titles.reconciled', 'Inventory reconciled — {sku}', {
325
+ sku: skuLabel,
326
+ })
327
+ default:
328
+ return t('wms.backend.dashboard.activity.titles.generic', '{type} {quantity}× {sku}', {
329
+ type: row.type ?? 'movement',
330
+ quantity,
331
+ sku: skuLabel,
332
+ })
333
+ }
334
+ }
335
+
336
+ function formatMovementSubtitle(
337
+ row: InventoryMovementRow,
338
+ t: InventoryDisplayTranslator,
339
+ ): string | null {
340
+ const reasonLabel = inventoryMovementReasonLabel(
341
+ {
342
+ reasonCode: row.reason_code,
343
+ reason: row.reason,
344
+ movementType: row.type,
345
+ },
346
+ t,
347
+ )
348
+ if (reasonLabel) return reasonLabel
349
+ if (row.reference_type && row.reference_id) return `${row.reference_type} · ${row.reference_id}`
350
+ return null
351
+ }
352
+
353
+ function formatMovementLocation(row: InventoryMovementRow): string {
354
+ const warehouse = formatWarehouseLabel(row)
355
+ const from = formatLocationLabel(row.location_from_code, row.location_from_id)
356
+ const to = formatLocationLabel(row.location_to_code, row.location_to_id)
357
+ if (row.type === 'transfer' && from !== '—' && to !== '—') return `${from} → ${to}`
358
+ const location = to !== '—' ? to : from
359
+ if (location !== '—') return `${warehouse} · ${location}`
360
+ return warehouse
361
+ }
362
+
363
+ type SkuKpiCardProps = {
364
+ title: string
365
+ caption: string
366
+ value: string
367
+ badgeLabel: string | null
368
+ badgeVariant: StatusBadgeVariant
369
+ ctaLabel: string
370
+ ctaHref: string
371
+ }
372
+
373
+ function SkuKpiCard({
374
+ title,
375
+ caption,
376
+ value,
377
+ badgeLabel,
378
+ badgeVariant,
379
+ ctaLabel,
380
+ ctaHref,
381
+ }: SkuKpiCardProps) {
382
+ return (
383
+ <section className="flex min-h-52 flex-col rounded-lg border bg-card p-5 text-card-foreground shadow-sm">
384
+ <p className="text-sm font-medium">{title}</p>
385
+ <p className="mt-3 text-xs text-muted-foreground">{caption}</p>
386
+ <div className="mt-2 flex items-end gap-3">
387
+ <p className="text-3xl font-semibold tracking-tight">{value}</p>
388
+ {badgeLabel ? (
389
+ <StatusBadge variant={badgeVariant} dot>
390
+ {badgeLabel}
391
+ </StatusBadge>
392
+ ) : null}
393
+ </div>
394
+ <LinkButton asChild variant="primary" size="sm" className="mt-auto pt-4 w-fit">
395
+ <Link href={ctaHref}>{ctaLabel}</Link>
396
+ </LinkButton>
397
+ </section>
398
+ )
399
+ }
400
+
401
+ type WmsSkuDetailPageProps = {
402
+ variantId: string
403
+ }
404
+
405
+ export default function WmsSkuDetailPage({ variantId }: WmsSkuDetailPageProps) {
406
+ const t = useT()
407
+ const locale = useLocale()
408
+ const router = useRouter()
409
+ const access = useWmsInventoryMutationAccess()
410
+ const parsedVariantId = variantIdSchema.safeParse(variantId.trim())
411
+ const catalogVariantId = parsedVariantId.success ? parsedVariantId.data : null
412
+
413
+ const [warehouseId, setWarehouseId] = React.useState<string>('all')
414
+ const [distributionFilter, setDistributionFilter] = React.useState<DistributionFilter>('all')
415
+ const [distributionPage, setDistributionPage] = React.useState(1)
416
+ const [selectedBalanceIds, setSelectedBalanceIds] = React.useState<Set<string>>(() => new Set())
417
+ const [adjustOpen, setAdjustOpen] = React.useState(false)
418
+ const [adjustPreset, setAdjustPreset] = React.useState<InventoryMutationPreset>({})
419
+ const [receiveOpen, setReceiveOpen] = React.useState(false)
420
+ const [cycleOpen, setCycleOpen] = React.useState(false)
421
+ const [cyclePreset, setCyclePreset] = React.useState<Pick<InventoryMutationPreset, 'warehouseId' | 'locationId'>>({})
422
+
423
+ const distributionPageSize = 20
424
+
425
+ const activityTimeFormatter = React.useMemo(
426
+ () =>
427
+ new Intl.DateTimeFormat(locale, {
428
+ month: '2-digit',
429
+ day: '2-digit',
430
+ hour: '2-digit',
431
+ minute: '2-digit',
432
+ }),
433
+ [locale],
434
+ )
435
+
436
+ const variantQuery = useQuery({
437
+ queryKey: ['wms-sku-detail', 'variant', catalogVariantId],
438
+ enabled: Boolean(catalogVariantId),
439
+ queryFn: async () => {
440
+ const params = new URLSearchParams({
441
+ id: catalogVariantId!,
442
+ page: '1',
443
+ pageSize: '1',
444
+ })
445
+ const call = await apiCall<PagedResponse<CatalogVariantRow>>(
446
+ `/api/catalog/variants?${params.toString()}`,
447
+ )
448
+ if (!call.ok) {
449
+ await raiseCrudError(call.response, t('wms.backend.sku.errors.variant', 'Failed to load SKU.'))
450
+ }
451
+ return call.result?.items?.[0] ?? null
452
+ },
453
+ })
454
+
455
+ const profileQuery = useQuery({
456
+ queryKey: ['wms-sku-detail', 'profile', catalogVariantId],
457
+ enabled: Boolean(catalogVariantId),
458
+ queryFn: async () => {
459
+ const params = new URLSearchParams({
460
+ catalogVariantId: catalogVariantId!,
461
+ page: '1',
462
+ pageSize: '1',
463
+ })
464
+ const call = await apiCall<PagedResponse<InventoryProfileRow>>(
465
+ `/api/wms/inventory-profiles?${params.toString()}`,
466
+ )
467
+ if (!call.ok) {
468
+ await raiseCrudError(call.response, t('wms.backend.sku.errors.profile', 'Failed to load inventory profile.'))
469
+ }
470
+ return call.result?.items?.[0] ?? null
471
+ },
472
+ })
473
+
474
+ const warehousesQuery = useQuery({
475
+ queryKey: ['wms-sku-detail', 'warehouses'],
476
+ queryFn: async () => {
477
+ const params = new URLSearchParams({ page: '1', pageSize: '100', sortField: 'name', sortDir: 'asc' })
478
+ const call = await apiCall<PagedResponse<WarehouseOption>>(`/api/wms/warehouses?${params.toString()}`)
479
+ if (!call.ok) {
480
+ await raiseCrudError(call.response, t('wms.backend.sku.errors.warehouses', 'Failed to load warehouses.'))
481
+ }
482
+ return call.result?.items ?? []
483
+ },
484
+ })
485
+
486
+ const balancesQuery = useQuery({
487
+ queryKey: ['wms-sku-detail', 'balances', catalogVariantId, warehouseId],
488
+ enabled: Boolean(catalogVariantId),
489
+ queryFn: async () => {
490
+ const params = new URLSearchParams({
491
+ catalogVariantId: catalogVariantId!,
492
+ page: '1',
493
+ pageSize: '100',
494
+ sortField: 'updatedAt',
495
+ sortDir: 'desc',
496
+ })
497
+ if (warehouseId !== 'all') params.set('warehouseId', warehouseId)
498
+ const call = await apiCall<PagedResponse<InventoryBalanceRow>>(
499
+ `/api/wms/inventory/balances?${params.toString()}`,
500
+ )
501
+ if (!call.ok) {
502
+ await raiseCrudError(call.response, t('wms.backend.sku.errors.balances', 'Failed to load stock distribution.'))
503
+ }
504
+ return call.result ?? { items: [], total: 0, totalPages: 1 }
505
+ },
506
+ })
507
+
508
+ const lotsQuery = useQuery({
509
+ queryKey: ['wms-sku-detail', 'lots', catalogVariantId],
510
+ enabled: Boolean(catalogVariantId),
511
+ queryFn: async () => {
512
+ const params = new URLSearchParams({
513
+ catalogVariantId: catalogVariantId!,
514
+ page: '1',
515
+ pageSize: '100',
516
+ })
517
+ const call = await apiCall<PagedResponse<InventoryLotRow>>(`/api/wms/lots?${params.toString()}`)
518
+ if (!call.ok) {
519
+ await raiseCrudError(call.response, t('wms.backend.sku.errors.lots', 'Failed to load lots.'))
520
+ }
521
+ return call.result?.items ?? []
522
+ },
523
+ })
524
+
525
+ const movementsQuery = useQuery({
526
+ queryKey: ['wms-sku-detail', 'movements', catalogVariantId, warehouseId],
527
+ enabled: Boolean(catalogVariantId),
528
+ queryFn: async () => {
529
+ const params = new URLSearchParams({
530
+ catalogVariantId: catalogVariantId!,
531
+ page: '1',
532
+ pageSize: '25',
533
+ sortField: 'performedAt',
534
+ sortDir: 'desc',
535
+ })
536
+ if (warehouseId !== 'all') params.set('warehouseId', warehouseId)
537
+ const call = await apiCall<PagedResponse<InventoryMovementRow>>(
538
+ `/api/wms/inventory/movements?${params.toString()}`,
539
+ )
540
+ if (!call.ok) {
541
+ await raiseCrudError(call.response, t('wms.backend.sku.errors.movements', 'Failed to load recent activity.'))
542
+ }
543
+ return call.result?.items ?? []
544
+ },
545
+ })
546
+
547
+ const lotById = React.useMemo(() => {
548
+ const map = new Map<string, InventoryLotRow>()
549
+ for (const lot of lotsQuery.data ?? []) {
550
+ if (lot.id) map.set(lot.id, lot)
551
+ }
552
+ return map
553
+ }, [lotsQuery.data])
554
+
555
+ const reorderPoint = toNumber(profileQuery.data?.reorder_point)
556
+ const safetyStock = toNumber(profileQuery.data?.safety_stock)
557
+ const skuLabel =
558
+ (variantQuery.data?.sku ?? '').trim() ||
559
+ (variantQuery.data?.name ?? '').trim() ||
560
+ catalogVariantId ||
561
+ '—'
562
+ const variantName = (variantQuery.data?.name ?? '').trim()
563
+ const pageTitle = (variantQuery.data?.sku ?? '').trim() || skuLabel
564
+
565
+ const totals = React.useMemo(() => {
566
+ const items = balancesQuery.data?.items ?? []
567
+ let onHand = 0
568
+ let reserved = 0
569
+ let available = 0
570
+ for (const row of items) {
571
+ onHand += toNumber(row.quantity_on_hand)
572
+ reserved += toNumber(row.quantity_reserved)
573
+ available += row.quantity_available ?? 0
574
+ }
575
+ return { onHand, reserved, available }
576
+ }, [balancesQuery.data?.items])
577
+
578
+ const daysOfSupply = React.useMemo(() => {
579
+ const movements = movementsQuery.data ?? []
580
+ const windowStart = Date.now() - USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1000
581
+ let outbound = 0
582
+ for (const row of movements) {
583
+ const at = row.performed_at ?? row.received_at
584
+ if (!at) continue
585
+ const ts = new Date(at).getTime()
586
+ if (Number.isNaN(ts) || ts < windowStart) continue
587
+ if (row.type === 'pick' || row.type === 'pack' || row.type === 'ship') {
588
+ outbound += Math.abs(toNumber(row.quantity))
589
+ }
590
+ }
591
+ const daily = outbound / USAGE_WINDOW_DAYS
592
+ if (daily <= 0 || totals.available <= 0) return null
593
+ return Math.round(totals.available / daily)
594
+ }, [movementsQuery.data, totals.available])
595
+
596
+ const nowMs = React.useMemo(() => Date.now(), [])
597
+
598
+ const filteredBalances = React.useMemo(() => {
599
+ const items = balancesQuery.data?.items ?? []
600
+ return items.filter((row) =>
601
+ matchesDistributionFilter(row, row.lot_id ? lotById.get(row.lot_id) : undefined, distributionFilter, nowMs),
602
+ )
603
+ }, [balancesQuery.data?.items, distributionFilter, lotById, nowMs])
604
+
605
+ const distributionTotal = filteredBalances.length
606
+ const distributionTotalPages = Math.max(1, Math.ceil(distributionTotal / distributionPageSize))
607
+ const pagedBalances = React.useMemo(() => {
608
+ const start = (distributionPage - 1) * distributionPageSize
609
+ return filteredBalances.slice(start, start + distributionPageSize)
610
+ }, [distributionPage, distributionPageSize, filteredBalances])
611
+
612
+ React.useEffect(() => {
613
+ setDistributionPage(1)
614
+ setSelectedBalanceIds(new Set())
615
+ }, [distributionFilter, warehouseId])
616
+
617
+ const selectedBalances = React.useMemo(
618
+ () => filteredBalances.filter((row) => selectedBalanceIds.has(row.id)),
619
+ [filteredBalances, selectedBalanceIds],
620
+ )
621
+
622
+ const movementsHref = React.useMemo(() => {
623
+ const params = new URLSearchParams()
624
+ if (catalogVariantId) params.set('catalogVariantId', catalogVariantId)
625
+ if (warehouseId !== 'all') params.set('warehouseId', warehouseId)
626
+ const query = params.toString()
627
+ return query ? `/backend/wms/movements?${query}` : '/backend/wms/movements'
628
+ }, [catalogVariantId, warehouseId])
629
+
630
+ const inventoryConsoleHref = React.useMemo(() => {
631
+ if (!catalogVariantId) return '/backend/wms/inventory'
632
+ return `/backend/wms/inventory?catalogVariantId=${encodeURIComponent(catalogVariantId)}`
633
+ }, [catalogVariantId])
634
+
635
+ const openAdjustDialog = React.useCallback((preset: InventoryMutationPreset = {}) => {
636
+ setAdjustPreset(preset)
637
+ setAdjustOpen(true)
638
+ }, [])
639
+
640
+ const openCycleCountDialog = React.useCallback(
641
+ (preset: Pick<InventoryMutationPreset, 'warehouseId' | 'locationId'> = {}) => {
642
+ setCyclePreset(preset)
643
+ setCycleOpen(true)
644
+ },
645
+ [],
646
+ )
647
+
648
+ const resolveMutationContext = React.useCallback((): InventoryMutationPreset => {
649
+ const firstSelected = selectedBalances[0]
650
+ const scopedWarehouseId =
651
+ firstSelected?.warehouse_id?.trim() ||
652
+ (warehouseId !== 'all' ? warehouseId : undefined)
653
+ const locationId =
654
+ selectedBalances.length === 1 ? firstSelected?.location_id?.trim() || undefined : undefined
655
+ const lotId =
656
+ selectedBalances.length === 1 ? firstSelected?.lot_id?.trim() || undefined : undefined
657
+ return {
658
+ warehouseId: scopedWarehouseId,
659
+ locationId,
660
+ lotId,
661
+ }
662
+ }, [selectedBalances, warehouseId])
663
+
664
+ const toggleBalanceSelection = React.useCallback((balanceId: string, selected: boolean) => {
665
+ setSelectedBalanceIds((current) => {
666
+ const next = new Set(current)
667
+ if (selected) next.add(balanceId)
668
+ else next.delete(balanceId)
669
+ return next
670
+ })
671
+ }, [])
672
+
673
+ const togglePageSelection = React.useCallback((selected: boolean) => {
674
+ setSelectedBalanceIds((current) => {
675
+ const next = new Set(current)
676
+ for (const row of pagedBalances) {
677
+ if (selected) next.add(row.id)
678
+ else next.delete(row.id)
679
+ }
680
+ return next
681
+ })
682
+ }, [pagedBalances])
683
+
684
+ const pageSelectionState = React.useMemo(() => {
685
+ if (pagedBalances.length === 0) {
686
+ return { checked: false, indeterminate: false }
687
+ }
688
+ const selectedOnPage = pagedBalances.filter((row) => selectedBalanceIds.has(row.id)).length
689
+ return {
690
+ checked: selectedOnPage === pagedBalances.length,
691
+ indeterminate: selectedOnPage > 0 && selectedOnPage < pagedBalances.length,
692
+ }
693
+ }, [pagedBalances, selectedBalanceIds])
694
+
695
+ const handleExportDistributionCsv = React.useCallback(() => {
696
+ const headers = [
697
+ t('wms.backend.sku.distribution.columns.warehouse', 'Warehouse'),
698
+ t('wms.backend.sku.distribution.columns.location', 'Location'),
699
+ t('wms.backend.sku.distribution.columns.lot', 'Lot'),
700
+ t('wms.backend.sku.distribution.columns.onHand', 'On hand'),
701
+ t('wms.backend.sku.distribution.columns.reserved', 'Reserved'),
702
+ t('wms.backend.sku.distribution.columns.status', 'Status'),
703
+ ]
704
+ const rows = filteredBalances.map((row) => {
705
+ const lot = row.lot_id ? lotById.get(row.lot_id) : undefined
706
+ const status = resolveBalanceStatus(row, lot, reorderPoint, nowMs)
707
+ return [
708
+ formatWarehouseLabel(row),
709
+ formatLocationLabel(row.location_code, row.location_id),
710
+ formatLotLabel(lot, locale),
711
+ String(toNumber(row.quantity_on_hand)),
712
+ String(toNumber(row.quantity_reserved)),
713
+ t(status.labelKey, status.labelFallback),
714
+ ]
715
+ })
716
+ const safeSku = pageTitle.replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '') || 'sku'
717
+ downloadCsvFile(`${safeSku}-distribution.csv`, [headers, ...rows])
718
+ }, [filteredBalances, locale, lotById, nowMs, pageTitle, reorderPoint, t])
719
+
720
+ const inventoryHref = '/backend/wms/inventory'
721
+
722
+ const selectedWarehouse = warehousesQuery.data?.find((warehouse) => warehouse.id === warehouseId)
723
+ const warehouseLabel =
724
+ warehouseId === 'all'
725
+ ? t('wms.backend.sku.filters.allWarehouses', 'All warehouses')
726
+ : selectedWarehouse?.name || selectedWarehouse?.code || warehouseId
727
+
728
+ const subtitleParts = [
729
+ variantName || null,
730
+ variantQuery.data?.is_active === false
731
+ ? t('wms.backend.sku.header.inactive', 'Inactive')
732
+ : null,
733
+ ].filter(Boolean)
734
+
735
+ const distributionColumns = React.useMemo<ColumnDef<InventoryBalanceRow>[]>(
736
+ () => [
737
+ {
738
+ id: 'select',
739
+ header: () => (
740
+ <Checkbox
741
+ aria-label={t('wms.backend.sku.distribution.columns.select', 'Select')}
742
+ checked={pageSelectionState.indeterminate ? 'indeterminate' : pageSelectionState.checked}
743
+ onCheckedChange={(checked) => togglePageSelection(checked === true)}
744
+ />
745
+ ),
746
+ cell: ({ row }) => (
747
+ <Checkbox
748
+ aria-label={t('wms.backend.sku.distribution.columns.select', 'Select')}
749
+ checked={selectedBalanceIds.has(row.original.id)}
750
+ onCheckedChange={(checked) => toggleBalanceSelection(row.original.id, checked === true)}
751
+ />
752
+ ),
753
+ meta: { maxWidth: '2.75rem' },
754
+ },
755
+ {
756
+ accessorKey: 'warehouse_id',
757
+ header: t('wms.backend.sku.distribution.columns.warehouse', 'Warehouse'),
758
+ cell: ({ row }) => formatWarehouseLabel(row.original),
759
+ },
760
+ {
761
+ accessorKey: 'location_id',
762
+ header: t('wms.backend.sku.distribution.columns.location', 'Location'),
763
+ cell: ({ row }) => {
764
+ const locationId = row.original.location_id?.trim()
765
+ const label = formatLocationLabel(row.original.location_code, row.original.location_id)
766
+ if (!locationId || label === '—') return label
767
+ return (
768
+ <Link
769
+ href={`/backend/wms/location/${encodeURIComponent(locationId)}`}
770
+ className="font-medium text-primary hover:underline"
771
+ >
772
+ {label}
773
+ </Link>
774
+ )
775
+ },
776
+ },
777
+ {
778
+ accessorKey: 'lot_id',
779
+ header: t('wms.backend.sku.distribution.columns.lot', 'Lot'),
780
+ cell: ({ row }) => {
781
+ const lotId = row.original.lot_id?.trim()
782
+ const lot = lotId ? lotById.get(lotId) : undefined
783
+ const label = formatLotLabel(lot, locale)
784
+ if (!lotId || label === '—') return label
785
+ return (
786
+ <Link
787
+ href={`/backend/wms/lot/${encodeURIComponent(lotId)}`}
788
+ className="font-medium text-primary hover:underline"
789
+ >
790
+ {label}
791
+ </Link>
792
+ )
793
+ },
794
+ },
795
+ {
796
+ accessorKey: 'quantity_on_hand',
797
+ header: t('wms.backend.sku.distribution.columns.onHand', 'On hand'),
798
+ cell: ({ row }) => String(toNumber(row.original.quantity_on_hand)),
799
+ },
800
+ {
801
+ accessorKey: 'quantity_reserved',
802
+ header: t('wms.backend.sku.distribution.columns.reserved', 'Reserved'),
803
+ cell: ({ row }) => String(toNumber(row.original.quantity_reserved)),
804
+ },
805
+ {
806
+ id: 'status',
807
+ header: t('wms.backend.sku.distribution.columns.status', 'Status'),
808
+ cell: ({ row }) => {
809
+ const lot = row.original.lot_id ? lotById.get(row.original.lot_id) : undefined
810
+ const status = resolveBalanceStatus(row.original, lot, reorderPoint, nowMs)
811
+ return (
812
+ <StatusBadge variant={status.variant} dot>
813
+ {t(status.labelKey, status.labelFallback)}
814
+ </StatusBadge>
815
+ )
816
+ },
817
+ },
818
+ ],
819
+ [
820
+ locale,
821
+ lotById,
822
+ nowMs,
823
+ pageSelectionState.checked,
824
+ pageSelectionState.indeterminate,
825
+ reorderPoint,
826
+ selectedBalanceIds,
827
+ t,
828
+ toggleBalanceSelection,
829
+ togglePageSelection,
830
+ ],
831
+ )
832
+
833
+ const activityColumns = React.useMemo<ColumnDef<InventoryMovementRow>[]>(
834
+ () => [
835
+ {
836
+ accessorKey: 'type',
837
+ header: t('wms.backend.sku.activity.columns.event', 'Event'),
838
+ cell: ({ row }) => {
839
+ const type = row.original.type ?? 'movement'
840
+ return (
841
+ <StatusBadge variant={movementStatusMap[type] ?? 'neutral'}>
842
+ {movementTypeLabel(type, t)}
843
+ </StatusBadge>
844
+ )
845
+ },
846
+ },
847
+ {
848
+ id: 'details',
849
+ header: t('wms.backend.sku.activity.columns.details', 'Details'),
850
+ cell: ({ row }) => {
851
+ const subtitle = formatMovementSubtitle(row.original, t)
852
+ return (
853
+ <div className="space-y-0.5">
854
+ <p className="text-sm font-medium">{formatMovementTitle(row.original, skuLabel, t)}</p>
855
+ {subtitle ? <p className="text-xs text-muted-foreground">{subtitle}</p> : null}
856
+ </div>
857
+ )
858
+ },
859
+ },
860
+ {
861
+ id: 'location',
862
+ header: t('wms.backend.sku.activity.columns.location', 'Location'),
863
+ cell: ({ row }) => (
864
+ <span className="text-sm text-muted-foreground">{formatMovementLocation(row.original)}</span>
865
+ ),
866
+ },
867
+ {
868
+ id: 'time',
869
+ header: t('wms.backend.sku.activity.columns.time', 'Time'),
870
+ cell: ({ row }) => {
871
+ const raw = row.original.performed_at ?? row.original.received_at
872
+ if (!raw) return '—'
873
+ const date = new Date(raw)
874
+ if (Number.isNaN(date.getTime())) return raw
875
+ return activityTimeFormatter.format(date)
876
+ },
877
+ meta: { maxWidth: '6rem' },
878
+ },
879
+ ],
880
+ [activityTimeFormatter, skuLabel, t],
881
+ )
882
+
883
+ const filterChips: Array<{ id: DistributionFilter; label: string }> = [
884
+ { id: 'all', label: t('wms.backend.sku.distribution.filters.all', 'All bins') },
885
+ { id: 'sellable', label: t('wms.backend.sku.distribution.filters.sellable', 'Sellable') },
886
+ { id: 'picking', label: t('wms.backend.sku.distribution.filters.picking', 'Picking') },
887
+ { id: 'nearExpiry', label: t('wms.backend.sku.distribution.filters.nearExpiry', 'Near expiry') },
888
+ ]
889
+
890
+ const isLoading =
891
+ variantQuery.isLoading ||
892
+ profileQuery.isLoading ||
893
+ balancesQuery.isLoading ||
894
+ lotsQuery.isLoading ||
895
+ movementsQuery.isLoading
896
+
897
+ const hasError =
898
+ variantQuery.isError ||
899
+ profileQuery.isError ||
900
+ balancesQuery.isError ||
901
+ lotsQuery.isError ||
902
+ movementsQuery.isError
903
+
904
+ if (!catalogVariantId) {
905
+ return (
906
+ <Page>
907
+ <PageBody>
908
+ <ErrorMessage
909
+ label={t('wms.backend.sku.errors.invalidId', 'Invalid SKU identifier.')}
910
+ action={(
911
+ <Button type="button" variant="outline" size="sm" onClick={() => router.push(inventoryHref)}>
912
+ {t('wms.backend.sku.actions.backToInventory', 'Back to inventory')}
913
+ </Button>
914
+ )}
915
+ />
916
+ </PageBody>
917
+ </Page>
918
+ )
919
+ }
920
+
921
+ return (
922
+ <Page>
923
+ <PageBody className="space-y-6">
924
+ <LinkButton asChild variant="gray" size="sm" className="w-fit px-0 text-muted-foreground hover:text-foreground">
925
+ <Link href={inventoryHref}>
926
+ <ArrowLeft className="size-4" />
927
+ {t('wms.backend.sku.actions.backToInventory', 'Back to inventory')}
928
+ </Link>
929
+ </LinkButton>
930
+
931
+ <PageHeader
932
+ title={pageTitle}
933
+ description={subtitleParts.join(' · ')}
934
+ actions={(
935
+ <>
936
+ <Select value={warehouseId} onValueChange={setWarehouseId}>
937
+ <SelectTrigger className="w-full max-w-xs sm:w-56">
938
+ <Warehouse className="mr-2 size-4 shrink-0 text-muted-foreground" />
939
+ <SelectValue placeholder={warehouseLabel} />
940
+ </SelectTrigger>
941
+ <SelectContent>
942
+ <SelectItem value="all">
943
+ {t('wms.backend.sku.filters.allWarehouses', 'All warehouses')}
944
+ </SelectItem>
945
+ {(warehousesQuery.data ?? []).map((warehouse) => (
946
+ <SelectItem key={warehouse.id} value={warehouse.id}>
947
+ {warehouse.name || warehouse.code || warehouse.id}
948
+ </SelectItem>
949
+ ))}
950
+ </SelectContent>
951
+ </Select>
952
+ {access.canAdjust ? (
953
+ <Button type="button" variant="outline" onClick={() => openAdjustDialog()}>
954
+ <SlidersHorizontal className="size-4" />
955
+ {t('wms.backend.sku.actions.adjust', 'Adjust stock')}
956
+ </Button>
957
+ ) : null}
958
+ </>
959
+ )}
960
+ />
961
+
962
+ {isLoading ? (
963
+ <LoadingMessage label={t('wms.backend.sku.loading', 'Loading SKU view…')} />
964
+ ) : null}
965
+
966
+ {hasError ? (
967
+ <ErrorMessage label={t('wms.backend.sku.errors.load', 'Failed to load SKU view.')} />
968
+ ) : null}
969
+
970
+ {!isLoading && !hasError && variantQuery.data ? (
971
+ <>
972
+ <section className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
973
+ <SkuKpiCard
974
+ title={t('wms.backend.sku.kpis.onHand.title', 'On hand')}
975
+ caption={t('wms.backend.sku.kpis.onHand.caption', 'Total physical quantity')}
976
+ value={String(totals.onHand)}
977
+ badgeLabel={
978
+ reorderPoint > 0 && totals.onHand <= reorderPoint
979
+ ? t('wms.backend.sku.kpis.onHand.badgeLow', 'Below reorder')
980
+ : null
981
+ }
982
+ badgeVariant="warning"
983
+ ctaLabel={t('wms.backend.sku.kpis.onHand.cta', 'View distribution')}
984
+ ctaHref="#stock-distribution"
985
+ />
986
+ <SkuKpiCard
987
+ title={t('wms.backend.sku.kpis.reserved.title', 'Reserved')}
988
+ caption={t('wms.backend.sku.kpis.reserved.caption', 'Committed to orders')}
989
+ value={String(totals.reserved)}
990
+ badgeLabel={
991
+ totals.reserved > 0
992
+ ? t('wms.backend.sku.kpis.reserved.badge', '{count} units held', { count: totals.reserved })
993
+ : null
994
+ }
995
+ badgeVariant="info"
996
+ ctaLabel={t('wms.backend.sku.kpis.reserved.cta', 'View reservations')}
997
+ ctaHref="/backend/wms/reservations"
998
+ />
999
+ <SkuKpiCard
1000
+ title={t('wms.backend.sku.kpis.available.title', 'Available')}
1001
+ caption={t('wms.backend.sku.kpis.available.caption', 'Ready to allocate')}
1002
+ value={String(totals.available)}
1003
+ badgeLabel={
1004
+ totals.available > 0
1005
+ ? t('wms.backend.sku.kpis.available.badge', 'Sellable')
1006
+ : t('wms.backend.sku.kpis.available.badgeEmpty', 'None')
1007
+ }
1008
+ badgeVariant={totals.available > 0 ? 'success' : 'neutral'}
1009
+ ctaLabel={t('wms.backend.sku.kpis.available.cta', 'Adjust stock')}
1010
+ ctaHref={access.canAdjust ? '#stock-distribution' : inventoryHref}
1011
+ />
1012
+ <SkuKpiCard
1013
+ title={t('wms.backend.sku.kpis.reorderPoint.title', 'Reorder point')}
1014
+ caption={t('wms.backend.sku.kpis.reorderPoint.caption', 'From inventory profile')}
1015
+ value={String(reorderPoint)}
1016
+ badgeLabel={
1017
+ safetyStock > 0
1018
+ ? t('wms.backend.sku.kpis.reorderPoint.badgeSafety', 'Safety {count}', { count: safetyStock })
1019
+ : null
1020
+ }
1021
+ badgeVariant={totals.available <= reorderPoint && reorderPoint > 0 ? 'error' : 'neutral'}
1022
+ ctaLabel={t('wms.backend.sku.kpis.reorderPoint.cta', 'Edit profile')}
1023
+ ctaHref="/backend/config/wms"
1024
+ />
1025
+ <SkuKpiCard
1026
+ title={t('wms.backend.sku.kpis.daysOfSupply.title', 'Days of supply')}
1027
+ caption={t('wms.backend.sku.kpis.daysOfSupply.caption', 'Based on last {days} days outbound', {
1028
+ days: USAGE_WINDOW_DAYS,
1029
+ })}
1030
+ value={daysOfSupply === null ? '—' : String(daysOfSupply)}
1031
+ badgeLabel={
1032
+ daysOfSupply !== null && daysOfSupply <= 7
1033
+ ? t('wms.backend.sku.kpis.daysOfSupply.badgeShort', 'Short runway')
1034
+ : null
1035
+ }
1036
+ badgeVariant="warning"
1037
+ ctaLabel={t('wms.backend.sku.kpis.daysOfSupply.cta', 'View movements')}
1038
+ ctaHref={movementsHref}
1039
+ />
1040
+ </section>
1041
+
1042
+ <section
1043
+ id="stock-distribution"
1044
+ className="rounded-lg border bg-card text-card-foreground shadow-sm"
1045
+ >
1046
+ <div className="border-b px-5 py-4">
1047
+ <h2 className="text-base font-semibold">
1048
+ {t('wms.backend.sku.distribution.title', 'Stock distribution')}
1049
+ </h2>
1050
+ </div>
1051
+ <div className="flex flex-col gap-4 border-b px-5 py-3 lg:flex-row lg:items-center lg:justify-between">
1052
+ <div className="flex flex-wrap gap-2">
1053
+ {filterChips.map((chip) => (
1054
+ <Button
1055
+ key={chip.id}
1056
+ type="button"
1057
+ size="sm"
1058
+ variant={distributionFilter === chip.id ? 'default' : 'outline'}
1059
+ className={cn('rounded-full', distributionFilter === chip.id && 'shadow-sm')}
1060
+ onClick={() => setDistributionFilter(chip.id)}
1061
+ >
1062
+ {chip.label}
1063
+ </Button>
1064
+ ))}
1065
+ </div>
1066
+ <div className="flex flex-wrap items-center gap-2">
1067
+ <Button type="button" variant="outline" onClick={handleExportDistributionCsv}>
1068
+ <Download className="size-4" />
1069
+ {t('wms.backend.sku.distribution.actions.exportCsv', 'Export CSV')}
1070
+ </Button>
1071
+ {access.canCycleCount ? (
1072
+ <Button
1073
+ type="button"
1074
+ variant="outline"
1075
+ onClick={() => openCycleCountDialog(resolveMutationContext())}
1076
+ >
1077
+ {t('wms.backend.sku.distribution.actions.cycleCountZone', 'Cycle count zone')}
1078
+ </Button>
1079
+ ) : null}
1080
+ {access.canAdjust && selectedBalances.length > 0 ? (
1081
+ <Button
1082
+ type="button"
1083
+ variant="default"
1084
+ onClick={() => openAdjustDialog(resolveMutationContext())}
1085
+ >
1086
+ {t('wms.backend.sku.distribution.actions.adjustSelected', 'Adjust selected ({count})', { count: selectedBalances.length })}
1087
+ </Button>
1088
+ ) : null}
1089
+ </div>
1090
+ </div>
1091
+ <DataTable<InventoryBalanceRow>
1092
+ embedded
1093
+ columns={distributionColumns}
1094
+ data={pagedBalances}
1095
+ disableRowClick
1096
+ entityId={E.wms.inventory_balance}
1097
+ perspective={{ tableId: 'wms.sku.distribution' }}
1098
+ pagination={{
1099
+ page: distributionPage,
1100
+ pageSize: distributionPageSize,
1101
+ total: distributionTotal,
1102
+ totalPages: distributionTotalPages,
1103
+ onPageChange: setDistributionPage,
1104
+ }}
1105
+ emptyState={(
1106
+ <EmptyState
1107
+ title={t('wms.backend.sku.distribution.empty.title', 'No locations in this view')}
1108
+ description={t(
1109
+ 'wms.backend.sku.distribution.empty.description',
1110
+ 'Try another filter or post inventory through receipts or adjustments.',
1111
+ )}
1112
+ />
1113
+ )}
1114
+ />
1115
+ {distributionTotal > 0 ? (
1116
+ <div className="flex flex-col gap-2 border-t px-5 py-3 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
1117
+ <span>
1118
+ {t('wms.backend.sku.distribution.footer.showing', 'Showing {shown} of {total} locations', {
1119
+ shown: pagedBalances.length,
1120
+ total: distributionTotal,
1121
+ })}
1122
+ </span>
1123
+ {balancesQuery.data && balancesQuery.data.total > distributionTotal ? (
1124
+ <LinkButton asChild variant="gray" size="sm" className="h-auto px-0">
1125
+ <Link href={inventoryHref}>
1126
+ {t('wms.backend.sku.distribution.footer.viewAll', 'View all locations →')}
1127
+ </Link>
1128
+ </LinkButton>
1129
+ ) : null}
1130
+ </div>
1131
+ ) : null}
1132
+ </section>
1133
+
1134
+ <DataTable<InventoryMovementRow>
1135
+ title={t('wms.backend.sku.activity.title', 'Recent activity')}
1136
+ columns={activityColumns}
1137
+ data={movementsQuery.data ?? []}
1138
+ disableRowClick
1139
+ entityId={E.wms.inventory_movement}
1140
+ perspective={{ tableId: 'wms.sku.activity' }}
1141
+ emptyState={t('wms.backend.sku.activity.empty', 'No recent movements for this SKU.')}
1142
+ actions={(
1143
+ <Button asChild type="button" variant="ghost" size="sm">
1144
+ <Link href={movementsHref}>
1145
+ {t('wms.backend.sku.activity.viewAll', 'View all movements →')}
1146
+ </Link>
1147
+ </Button>
1148
+ )}
1149
+ />
1150
+
1151
+ <section className="flex flex-col gap-4 rounded-lg border bg-card px-5 py-4 shadow-sm lg:flex-row lg:items-center lg:justify-between">
1152
+ <div>
1153
+ <h2 className="text-base font-semibold">
1154
+ {t('wms.backend.sku.quickActions.title', 'Quick actions')}
1155
+ </h2>
1156
+ <p className="text-sm text-muted-foreground">
1157
+ {t(
1158
+ 'wms.backend.sku.quickActions.description',
1159
+ 'Run common inventory actions without leaving this SKU view',
1160
+ )}
1161
+ </p>
1162
+ </div>
1163
+ <div className="flex flex-wrap gap-2">
1164
+ {access.canReceive ? (
1165
+ <Button type="button" variant="default" onClick={() => setReceiveOpen(true)}>
1166
+ <ArrowDown className="size-4" />
1167
+ {t('wms.backend.sku.quickActions.receive', 'Receive stock')}
1168
+ </Button>
1169
+ ) : null}
1170
+ {access.canAdjust ? (
1171
+ <Button type="button" variant="outline" onClick={() => openAdjustDialog()}>
1172
+ <SlidersHorizontal className="size-4" />
1173
+ {t('wms.backend.sku.quickActions.adjust', 'Adjust inventory')}
1174
+ </Button>
1175
+ ) : null}
1176
+ {access.canAdjust ? (
1177
+ <Button asChild type="button" variant="outline">
1178
+ <Link href={inventoryConsoleHref}>
1179
+ <ArrowLeftRight className="size-4" />
1180
+ {t('wms.backend.sku.quickActions.move', 'Move stock')}
1181
+ </Link>
1182
+ </Button>
1183
+ ) : null}
1184
+ {access.canCycleCount ? (
1185
+ <Button
1186
+ type="button"
1187
+ variant="outline"
1188
+ onClick={() => openCycleCountDialog(resolveMutationContext())}
1189
+ >
1190
+ <ClipboardList className="size-4" />
1191
+ {t('wms.backend.sku.quickActions.cycleCount', 'Cycle count')}
1192
+ </Button>
1193
+ ) : null}
1194
+ <Button asChild type="button" variant="outline">
1195
+ <Link href={movementsHref}>
1196
+ {t('wms.backend.sku.quickActions.openLedger', 'Open ledger')}
1197
+ <ExternalLink className="size-4" />
1198
+ </Link>
1199
+ </Button>
1200
+ </div>
1201
+ </section>
1202
+ </>
1203
+ ) : null}
1204
+
1205
+ {!isLoading && !hasError && !variantQuery.data ? (
1206
+ <RecordNotFoundState
1207
+ label={t('wms.backend.sku.errors.notFound', 'SKU not found.')}
1208
+ backHref={inventoryHref}
1209
+ backLabel={t('wms.backend.sku.actions.backToInventory', 'Back to inventory')}
1210
+ />
1211
+ ) : null}
1212
+ </PageBody>
1213
+
1214
+ {access.canReceive && catalogVariantId ? (
1215
+ <ReceiveInventoryDialog
1216
+ open={receiveOpen}
1217
+ onOpenChange={setReceiveOpen}
1218
+ access={access}
1219
+ initialCatalogVariantId={catalogVariantId}
1220
+ />
1221
+ ) : null}
1222
+ {access.canAdjust && catalogVariantId ? (
1223
+ <AdjustInventoryDialog
1224
+ open={adjustOpen}
1225
+ onOpenChange={setAdjustOpen}
1226
+ access={access}
1227
+ initialCatalogVariantId={catalogVariantId}
1228
+ initialWarehouseId={adjustPreset.warehouseId}
1229
+ initialLocationId={adjustPreset.locationId}
1230
+ initialLotId={adjustPreset.lotId}
1231
+ />
1232
+ ) : null}
1233
+ {access.canCycleCount ? (
1234
+ <CycleCountWizardDialog
1235
+ open={cycleOpen}
1236
+ onOpenChange={setCycleOpen}
1237
+ access={access}
1238
+ initialCatalogVariantId={catalogVariantId ?? undefined}
1239
+ initialWarehouseId={cyclePreset.warehouseId}
1240
+ initialLocationId={cyclePreset.locationId}
1241
+ />
1242
+ ) : null}
1243
+ </Page>
1244
+ )
1245
+ }