@open-mercato/core 0.6.7-develop.6669.1.40b669666b → 0.6.7-develop.6671.1.4b925c5096

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