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