@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,2007 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { z } from 'zod'
5
+ import { useQueryClient } from '@tanstack/react-query'
6
+ import { Check, Minus, PackageSearch, Plus, Warehouse } from 'lucide-react'
7
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
8
+ import { flashMutationError } from '../../lib/flashMutationError'
9
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
10
+ import { ComboboxInput } from '@open-mercato/ui/backend/inputs/ComboboxInput'
11
+ import { DateTimePicker } from '@open-mercato/ui/backend/inputs/DateTimePicker'
12
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
13
+ import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
14
+ import { Button } from '@open-mercato/ui/primitives/button'
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogDescription,
19
+ DialogFooter,
20
+ DialogHeader,
21
+ DialogTitle,
22
+ } from '@open-mercato/ui/primitives/dialog'
23
+ import { FormField } from '@open-mercato/ui/primitives/form-field'
24
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
25
+ import { Input } from '@open-mercato/ui/primitives/input'
26
+ import { KbdShortcut } from '@open-mercato/ui/primitives/kbd'
27
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
28
+ import { Switch } from '@open-mercato/ui/primitives/switch'
29
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
30
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
31
+ import { createLogger } from '@open-mercato/shared/lib/logger'
32
+ import { cn } from '@open-mercato/shared/lib/utils'
33
+ import {
34
+ buildInventoryMutationReferenceId,
35
+ computeCycleCountVariance,
36
+ formatSignedQuantity,
37
+ } from '../../lib/inventoryMutationUi'
38
+ import {
39
+ BalanceLookupError,
40
+ ScopeEstimateError,
41
+ ScopeQueueError,
42
+ buildCycleCountScopeQueue,
43
+ fetchBalanceOnHand,
44
+ fetchCycleCountScopeEstimate,
45
+ filterLocationsByCodeRange,
46
+ formatCycleCountZoneLabel,
47
+ loadAllLocations,
48
+ loadAssigneeOptions,
49
+ loadBinLocationOptions,
50
+ loadCatalogVariantOptions,
51
+ loadLocationOptions,
52
+ loadLotOptionsByIds,
53
+ loadLotOptionsForBalanceLocation,
54
+ loadWarehouseOptions,
55
+ loadZoneOptions,
56
+ mapLocationOptions,
57
+ resolveAssigneeLabel,
58
+ resolveCatalogVariantLabel,
59
+ resolveLocationLabel,
60
+ resolveLotLabel,
61
+ resolveWarehouseLabel,
62
+ resolveZoneLabel,
63
+ } from './inventoryMutationLoaders'
64
+ import type { ScopeQueueItem } from './inventoryMutationLoaders'
65
+ import type { useWmsInventoryMutationAccess } from './useWmsInventoryMutationAccess'
66
+
67
+ const logger = createLogger('wms')
68
+
69
+ type CycleCountWizardDialogProps = {
70
+ open: boolean
71
+ onOpenChange: (open: boolean) => void
72
+ access: ReturnType<typeof useWmsInventoryMutationAccess>
73
+ initialCatalogVariantId?: string
74
+ initialWarehouseId?: string
75
+ initialLocationId?: string
76
+ initialLotId?: string
77
+ }
78
+
79
+ type WizardStep = 1 | 2 | 3
80
+
81
+ type CycleCountFormState = {
82
+ zoneId: string
83
+ warehouseId: string
84
+ fromLocationId: string
85
+ toLocationId: string
86
+ scheduledAt: string
87
+ assigneeId: string
88
+ expectedSkus: number
89
+ locationId: string
90
+ catalogVariantId: string
91
+ lotId: string
92
+ countedQuantity: number
93
+ setupNotes: string
94
+ countNotes: string
95
+ reason: string
96
+ }
97
+
98
+ const EMPTY_FORM: CycleCountFormState = {
99
+ zoneId: '',
100
+ warehouseId: '',
101
+ fromLocationId: '',
102
+ toLocationId: '',
103
+ scheduledAt: '',
104
+ assigneeId: '',
105
+ expectedSkus: 0,
106
+ locationId: '',
107
+ catalogVariantId: '',
108
+ lotId: '',
109
+ countedQuantity: 0,
110
+ setupNotes: '',
111
+ countNotes: '',
112
+ reason: 'cycle_count',
113
+ }
114
+
115
+ function CycleCountStepIndicator({ step }: { step: WizardStep }) {
116
+ const steps = [1, 2, 3] as const
117
+
118
+ return (
119
+ <div
120
+ className="flex items-center gap-1.5 pt-1"
121
+ aria-label={`Step ${step} of 3`}
122
+ >
123
+ {steps.map((stepNumber, index) => {
124
+ const completed = stepNumber < step
125
+ const current = stepNumber === step
126
+
127
+ return (
128
+ <React.Fragment key={stepNumber}>
129
+ {index > 0 ? (
130
+ <div
131
+ className={cn(
132
+ 'h-0.5 w-4 shrink-0',
133
+ stepNumber <= step ? 'bg-foreground' : 'bg-border',
134
+ )}
135
+ aria-hidden="true"
136
+ />
137
+ ) : null}
138
+ <div
139
+ className={cn(
140
+ 'flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-semibold',
141
+ completed || current
142
+ ? 'bg-foreground text-primary-foreground'
143
+ : 'bg-muted text-muted-foreground',
144
+ )}
145
+ aria-current={current ? 'step' : undefined}
146
+ >
147
+ {completed ? <Check className="size-3" aria-hidden="true" /> : stepNumber}
148
+ </div>
149
+ </React.Fragment>
150
+ )
151
+ })}
152
+ </div>
153
+ )
154
+ }
155
+
156
+ function SummaryPanel({
157
+ title,
158
+ children,
159
+ }: {
160
+ title: string
161
+ children: React.ReactNode
162
+ }) {
163
+ return (
164
+ <div className="rounded-lg border bg-muted/40 px-4 py-3.5">
165
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
166
+ {title}
167
+ </p>
168
+ {children}
169
+ </div>
170
+ )
171
+ }
172
+
173
+ export function CycleCountWizardDialog({
174
+ open,
175
+ onOpenChange,
176
+ access,
177
+ initialCatalogVariantId,
178
+ initialWarehouseId,
179
+ initialLocationId,
180
+ initialLotId,
181
+ }: CycleCountWizardDialogProps) {
182
+ const t = useT()
183
+ const queryClient = useQueryClient()
184
+ const formRef = React.useRef<HTMLFormElement>(null)
185
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
186
+ retryLastMutation: () => Promise<boolean>
187
+ }>({
188
+ contextId: 'wms-inventory-cycle-count',
189
+ })
190
+ const mutationContext = React.useMemo(
191
+ () => ({ retryLastMutation }),
192
+ [retryLastMutation],
193
+ )
194
+
195
+ const [step, setStep] = React.useState<WizardStep>(1)
196
+ const [form, setForm] = React.useState<CycleCountFormState>(EMPTY_FORM)
197
+ const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})
198
+ const [systemOnHand, setSystemOnHand] = React.useState(0)
199
+ const [loadingBalance, setLoadingBalance] = React.useState(false)
200
+ const [submitting, setSubmitting] = React.useState(false)
201
+ const [linesPosted, setLinesPosted] = React.useState(0)
202
+ const [autoAdjust, setAutoAdjust] = React.useState(true)
203
+ const [referenceId] = React.useState(() => buildInventoryMutationReferenceId())
204
+ const [optionLabelByValue, setOptionLabelByValue] = React.useState<Record<string, string>>({})
205
+ const [loadingScopeEstimate, setLoadingScopeEstimate] = React.useState(false)
206
+ const [scopeStats, setScopeStats] = React.useState<{ expectedSkus: number; binCount: number } | null>(
207
+ null,
208
+ )
209
+ const [expectedSkusTouched, setExpectedSkusTouched] = React.useState(false)
210
+ const [zoneSuggestions, setZoneSuggestions] = React.useState<Array<{ value: string; label: string }>>([])
211
+ const [lotSuggestions, setLotSuggestions] = React.useState<Array<{ value: string; label: string }>>([])
212
+ const [assigneeCanListUsers, setAssigneeCanListUsers] = React.useState(true)
213
+ const [scopeEstimateError, setScopeEstimateError] = React.useState<string | null>(null)
214
+ const [balanceError, setBalanceError] = React.useState<string | null>(null)
215
+
216
+ const [scopeQueue, setScopeQueue] = React.useState<ScopeQueueItem[]>([])
217
+ const [scopeQueueIndex, setScopeQueueIndex] = React.useState(0)
218
+ const [scopeQueueLoading, setScopeQueueLoading] = React.useState(false)
219
+ const [scopeQueueReady, setScopeQueueReady] = React.useState(false)
220
+ const [scopeQueueError, setScopeQueueError] = React.useState<string | null>(null)
221
+ const [finishConfirmPending, setFinishConfirmPending] = React.useState(false)
222
+
223
+ const scopeQueueRef = React.useRef<ScopeQueueItem[]>([])
224
+ const scopeQueueIndexRef = React.useRef(0)
225
+ scopeQueueRef.current = scopeQueue
226
+ scopeQueueIndexRef.current = scopeQueueIndex
227
+
228
+ const registerOptionLabels = React.useCallback(
229
+ (options: Array<{ value: string; label: string }>) => {
230
+ setOptionLabelByValue((current) => {
231
+ let changed = false
232
+ const next = { ...current }
233
+ for (const option of options) {
234
+ const value = option.value.trim()
235
+ const label = option.label.trim()
236
+ if (!value || !label || next[value] === label) continue
237
+ next[value] = label
238
+ changed = true
239
+ }
240
+ return changed ? next : current
241
+ })
242
+ },
243
+ [],
244
+ )
245
+
246
+ // When a location holds multiple lots, the balance lookup already knows the
247
+ // candidate lot IDs. Seed them as static Combobox suggestions so the Lot
248
+ // field shows options immediately — without waiting on the debounced
249
+ // loadSuggestions path that has historically raced into a stuck
250
+ // "Loading suggestions…" state under balance-lookup disable churn.
251
+ const seedLotSuggestionsFromCandidates = React.useCallback(
252
+ async (candidateLotIds: string[]) => {
253
+ if (candidateLotIds.length === 0) {
254
+ setLotSuggestions([])
255
+ return
256
+ }
257
+ const options = await loadLotOptionsByIds(candidateLotIds)
258
+ registerOptionLabels(options)
259
+ setLotSuggestions(options.map((option) => ({ value: option.value, label: option.label })))
260
+ },
261
+ [registerOptionLabels],
262
+ )
263
+
264
+ const resolveOptionLabel = React.useCallback(
265
+ (value: string) => optionLabelByValue[value] ?? value,
266
+ [optionLabelByValue],
267
+ )
268
+ const optionLabelByValueRef = React.useRef(optionLabelByValue)
269
+ optionLabelByValueRef.current = optionLabelByValue
270
+ const zoneWarehouseByZoneIdRef = React.useRef<Record<string, string>>({})
271
+
272
+ const rememberZoneWarehouseOptions = React.useCallback(
273
+ (options: Array<{ value: string; warehouseId?: string }>) => {
274
+ for (const option of options) {
275
+ const zoneId = option.value.trim()
276
+ const warehouseId = option.warehouseId?.trim()
277
+ if (!zoneId || !warehouseId) continue
278
+ zoneWarehouseByZoneIdRef.current[zoneId] = warehouseId
279
+ }
280
+ },
281
+ [],
282
+ )
283
+
284
+ const variance = computeCycleCountVariance(systemOnHand, form.countedQuantity)
285
+
286
+ const setupSchema = React.useMemo(
287
+ () =>
288
+ z.object({
289
+ warehouseId: z.string().uuid(),
290
+ zoneId: z.string().uuid(),
291
+ fromLocationId: z.string().uuid().optional().or(z.literal('')),
292
+ toLocationId: z.string().uuid().optional().or(z.literal('')),
293
+ assigneeId: z.string().uuid(),
294
+ expectedSkus: z.coerce.number().int().min(1),
295
+ setupNotes: z.string().trim().max(500).optional(),
296
+ }),
297
+ [],
298
+ )
299
+
300
+ const countSchema = React.useMemo(
301
+ () =>
302
+ z.object({
303
+ warehouseId: z.string().uuid(),
304
+ locationId: z.string().uuid(),
305
+ catalogVariantId: z.string().uuid(),
306
+ lotId: z.string().uuid().optional().or(z.literal('')),
307
+ countedQuantity: z.coerce.number().min(0),
308
+ countNotes: z.string().trim().max(500).optional(),
309
+ }),
310
+ [],
311
+ )
312
+
313
+ const commitSchema = React.useMemo(
314
+ () =>
315
+ z.object({
316
+ reason: z.string().trim().min(1).max(500),
317
+ }),
318
+ [],
319
+ )
320
+
321
+ const resetWizard = React.useCallback(() => {
322
+ setStep(1)
323
+ setForm(EMPTY_FORM)
324
+ setFieldErrors({})
325
+ setSystemOnHand(0)
326
+ setLoadingBalance(false)
327
+ setSubmitting(false)
328
+ setAutoAdjust(true)
329
+ setOptionLabelByValue({})
330
+ zoneWarehouseByZoneIdRef.current = {}
331
+ setLoadingScopeEstimate(false)
332
+ setScopeStats(null)
333
+ setExpectedSkusTouched(false)
334
+ setZoneSuggestions([])
335
+ setLotSuggestions([])
336
+ setAssigneeCanListUsers(true)
337
+ setScopeEstimateError(null)
338
+ setBalanceError(null)
339
+ setLinesPosted(0)
340
+ setScopeQueue([])
341
+ setScopeQueueIndex(0)
342
+ setScopeQueueLoading(false)
343
+ setScopeQueueReady(false)
344
+ setScopeQueueError(null)
345
+ setFinishConfirmPending(false)
346
+ }, [])
347
+
348
+ const resetToStep2 = React.useCallback(() => {
349
+ const currentQueue = scopeQueueRef.current
350
+ const currentIndex = scopeQueueIndexRef.current
351
+ const nextIndex = currentIndex + 1
352
+ const nextItem = currentQueue[nextIndex]
353
+
354
+ setStep(2)
355
+ setFieldErrors({})
356
+ setSystemOnHand(0)
357
+ setLoadingBalance(false)
358
+ setBalanceError(null)
359
+ setAutoAdjust(true)
360
+ setFinishConfirmPending(false)
361
+
362
+ if (nextItem) {
363
+ setScopeQueueIndex(nextIndex)
364
+ setForm((current) => ({
365
+ ...current,
366
+ locationId: nextItem.locationId,
367
+ catalogVariantId: nextItem.catalogVariantId,
368
+ lotId: nextItem.lotId ?? '',
369
+ countedQuantity: 0,
370
+ countNotes: '',
371
+ reason: 'cycle_count',
372
+ }))
373
+ } else {
374
+ setForm((current) => ({
375
+ ...current,
376
+ locationId: '',
377
+ catalogVariantId: '',
378
+ lotId: '',
379
+ countedQuantity: 0,
380
+ countNotes: '',
381
+ reason: 'cycle_count',
382
+ }))
383
+ }
384
+ }, [])
385
+
386
+ const closeDialog = React.useCallback(() => {
387
+ onOpenChange(false)
388
+ resetWizard()
389
+ }, [onOpenChange, resetWizard])
390
+
391
+ const patchForm = React.useCallback((patch: Partial<CycleCountFormState>) => {
392
+ setForm((current) => ({ ...current, ...patch }))
393
+ setFieldErrors({})
394
+ }, [])
395
+
396
+ React.useEffect(() => {
397
+ if (!open) return
398
+ const catalogVariantId = initialCatalogVariantId?.trim()
399
+ const warehouseId = initialWarehouseId?.trim()
400
+ const locationId = initialLocationId?.trim()
401
+ const lotId = initialLotId?.trim()
402
+ setForm((current) => ({
403
+ ...current,
404
+ scheduledAt: current.scheduledAt.trim() || new Date().toISOString(),
405
+ assigneeId: current.assigneeId.trim() || access.userId?.trim() || '',
406
+ ...(catalogVariantId ? { catalogVariantId } : {}),
407
+ ...(warehouseId ? { warehouseId } : {}),
408
+ ...(lotId ? { lotId } : {}),
409
+ ...(locationId
410
+ ? {
411
+ locationId,
412
+ fromLocationId: locationId,
413
+ toLocationId: locationId,
414
+ }
415
+ : {}),
416
+ }))
417
+ }, [access.userId, initialCatalogVariantId, initialLocationId, initialLotId, initialWarehouseId, open])
418
+
419
+ const assigneeFallback = React.useMemo(() => {
420
+ const userId = access.userId?.trim()
421
+ if (!userId) return null
422
+ const label = resolveOptionLabel(userId)
423
+ return { userId, label: label === userId ? userId : label }
424
+ }, [access.userId, resolveOptionLabel])
425
+
426
+ React.useEffect(() => {
427
+ if (!open || !assigneeFallback) return
428
+ let cancelled = false
429
+ void loadAssigneeOptions(undefined, assigneeFallback).then((result) => {
430
+ if (cancelled) return
431
+ setAssigneeCanListUsers(result.canListUsers)
432
+ registerOptionLabels(result.options)
433
+ })
434
+ return () => {
435
+ cancelled = true
436
+ }
437
+ }, [assigneeFallback, open, registerOptionLabels])
438
+
439
+ React.useEffect(() => {
440
+ if (!open) return
441
+ const warehouseId = form.warehouseId.trim()
442
+ if (!warehouseId) {
443
+ setZoneSuggestions([])
444
+ return
445
+ }
446
+
447
+ let cancelled = false
448
+ void loadZoneOptions(warehouseId)
449
+ .then((options) => {
450
+ if (cancelled) return
451
+ rememberZoneWarehouseOptions(options)
452
+ registerOptionLabels(options)
453
+ setZoneSuggestions(
454
+ options.map((option) => ({
455
+ value: option.value,
456
+ label: option.label,
457
+ })),
458
+ )
459
+ })
460
+ .catch((error: unknown) => {
461
+ if (cancelled) return
462
+ setZoneSuggestions([])
463
+ logger.error('loadZoneOptions failed', { component: 'CycleCountWizardDialog', err: error })
464
+ })
465
+
466
+ return () => {
467
+ cancelled = true
468
+ }
469
+ }, [form.warehouseId, open, registerOptionLabels, rememberZoneWarehouseOptions])
470
+
471
+ const loadZoneSuggestions = React.useCallback(
472
+ async (query?: string) => {
473
+ const warehouseId = form.warehouseId.trim()
474
+ if (!warehouseId) return []
475
+ const options = await loadZoneOptions(warehouseId, query)
476
+ rememberZoneWarehouseOptions(options)
477
+ registerOptionLabels(options)
478
+ if (!query?.trim()) {
479
+ setZoneSuggestions(
480
+ options.map((option) => ({
481
+ value: option.value,
482
+ label: option.label,
483
+ })),
484
+ )
485
+ }
486
+ return options.map((option) => ({
487
+ value: option.value,
488
+ label: option.label,
489
+ }))
490
+ },
491
+ [form.warehouseId, registerOptionLabels, rememberZoneWarehouseOptions],
492
+ )
493
+
494
+ const scheduledAtValue = React.useMemo(() => {
495
+ const raw = form.scheduledAt.trim()
496
+ if (!raw) return new Date()
497
+ const parsed = new Date(raw)
498
+ return Number.isNaN(parsed.getTime()) ? new Date() : parsed
499
+ }, [form.scheduledAt])
500
+
501
+ const scheduleImmediate = React.useMemo(() => {
502
+ const deltaMs = Math.abs(scheduledAtValue.getTime() - Date.now())
503
+ return deltaMs <= 5 * 60 * 1000
504
+ }, [scheduledAtValue])
505
+
506
+ React.useEffect(() => {
507
+ if (!open) return
508
+ let cancelled = false
509
+
510
+ const ensureLabel = async (value: string, resolve: (id: string) => Promise<string | null>) => {
511
+ const id = value.trim()
512
+ if (!id || optionLabelByValueRef.current[id]) return
513
+ const label = await resolve(id)
514
+ if (cancelled || !label) return
515
+ registerOptionLabels([{ value: id, label }])
516
+ }
517
+
518
+ void Promise.all([
519
+ ensureLabel(form.catalogVariantId, resolveCatalogVariantLabel),
520
+ ensureLabel(form.zoneId, resolveZoneLabel),
521
+ ensureLabel(form.warehouseId, resolveWarehouseLabel),
522
+ ensureLabel(form.fromLocationId, resolveLocationLabel),
523
+ ensureLabel(form.toLocationId, resolveLocationLabel),
524
+ ensureLabel(form.locationId, resolveLocationLabel),
525
+ ensureLabel(form.lotId, resolveLotLabel),
526
+ ensureLabel(form.assigneeId, resolveAssigneeLabel),
527
+ ])
528
+
529
+ return () => {
530
+ cancelled = true
531
+ }
532
+ }, [
533
+ form.assigneeId,
534
+ form.catalogVariantId,
535
+ form.fromLocationId,
536
+ form.locationId,
537
+ form.lotId,
538
+ form.toLocationId,
539
+ form.warehouseId,
540
+ form.zoneId,
541
+ open,
542
+ registerOptionLabels,
543
+ ])
544
+
545
+ React.useEffect(() => {
546
+ if (!open || step !== 1) return
547
+ const warehouseId = form.warehouseId.trim()
548
+ if (!warehouseId) {
549
+ setScopeStats(null)
550
+ setScopeEstimateError(null)
551
+ if (!expectedSkusTouched) patchForm({ expectedSkus: 0 })
552
+ return
553
+ }
554
+
555
+ let cancelled = false
556
+ setLoadingScopeEstimate(true)
557
+ setScopeEstimateError(null)
558
+ void fetchCycleCountScopeEstimate({
559
+ warehouseId,
560
+ fromLocationId: form.fromLocationId.trim() || null,
561
+ toLocationId: form.toLocationId.trim() || null,
562
+ })
563
+ .then((estimate) => {
564
+ if (cancelled) return
565
+ setScopeStats(estimate)
566
+ setScopeEstimateError(null)
567
+ if (!expectedSkusTouched) {
568
+ patchForm({ expectedSkus: Math.max(1, estimate.expectedSkus) })
569
+ }
570
+ })
571
+ .catch((error: unknown) => {
572
+ if (cancelled) return
573
+ setScopeStats(null)
574
+ if (error instanceof ScopeEstimateError) {
575
+ setScopeEstimateError(
576
+ t(
577
+ 'wms.backend.inventory.cycleCount.errors.scopeEstimate',
578
+ 'Failed to estimate count scope.',
579
+ ),
580
+ )
581
+ return
582
+ }
583
+ logger.error('fetchCycleCountScopeEstimate failed', { component: 'CycleCountWizardDialog', err: error })
584
+ setScopeEstimateError(
585
+ t(
586
+ 'wms.backend.inventory.cycleCount.errors.scopeEstimate',
587
+ 'Failed to estimate count scope.',
588
+ ),
589
+ )
590
+ })
591
+ .finally(() => {
592
+ if (!cancelled) setLoadingScopeEstimate(false)
593
+ })
594
+
595
+ return () => {
596
+ cancelled = true
597
+ }
598
+ }, [
599
+ expectedSkusTouched,
600
+ form.fromLocationId,
601
+ form.toLocationId,
602
+ form.warehouseId,
603
+ open,
604
+ patchForm,
605
+ step,
606
+ t,
607
+ ])
608
+
609
+ React.useEffect(() => {
610
+ if (!open || !form.zoneId.trim() || !scopeStats) return
611
+ const baseLabel = optionLabelByValueRef.current[form.zoneId.trim()]
612
+ if (!baseLabel || baseLabel.includes('SKUs')) return
613
+ registerOptionLabels([
614
+ {
615
+ value: form.zoneId.trim(),
616
+ label: formatCycleCountZoneLabel(baseLabel, scopeStats),
617
+ },
618
+ ])
619
+ }, [form.zoneId, open, registerOptionLabels, scopeStats])
620
+
621
+ React.useEffect(() => {
622
+ if (!open || step < 2) return
623
+ const warehouseId = form.warehouseId.trim()
624
+ const locationId = form.locationId.trim()
625
+ const catalogVariantId = form.catalogVariantId.trim()
626
+ if (!warehouseId || !locationId || !catalogVariantId) {
627
+ setSystemOnHand(0)
628
+ setBalanceError(null)
629
+ return
630
+ }
631
+
632
+ let cancelled = false
633
+ setLoadingBalance(true)
634
+ setBalanceError(null)
635
+ void fetchBalanceOnHand({
636
+ warehouseId,
637
+ locationId,
638
+ catalogVariantId,
639
+ lotId: form.lotId.trim() || null,
640
+ })
641
+ .then((value) => {
642
+ if (!cancelled) {
643
+ setSystemOnHand(value)
644
+ setBalanceError(null)
645
+ }
646
+ })
647
+ .catch((error: unknown) => {
648
+ if (cancelled) return
649
+ setSystemOnHand(0)
650
+ if (error instanceof BalanceLookupError) {
651
+ if (error.code === 'LOT_REQUIRED') {
652
+ setBalanceError(
653
+ t(
654
+ 'wms.backend.inventory.cycleCount.errors.ambiguousLot',
655
+ 'Multiple lots found at this location — select a lot to continue.',
656
+ ),
657
+ )
658
+ void seedLotSuggestionsFromCandidates(error.candidateLotIds)
659
+ return
660
+ }
661
+ setBalanceError(
662
+ t('wms.backend.inventory.cycleCount.errors.balance', 'Failed to load system on-hand.'),
663
+ )
664
+ return
665
+ }
666
+ logger.error('fetchBalanceOnHand failed', { component: 'CycleCountWizardDialog', err: error })
667
+ setBalanceError(
668
+ t('wms.backend.inventory.cycleCount.errors.balance', 'Failed to load system on-hand.'),
669
+ )
670
+ })
671
+ .finally(() => {
672
+ if (!cancelled) setLoadingBalance(false)
673
+ })
674
+
675
+ return () => {
676
+ cancelled = true
677
+ }
678
+ }, [
679
+ form.catalogVariantId,
680
+ form.locationId,
681
+ form.lotId,
682
+ form.warehouseId,
683
+ open,
684
+ seedLotSuggestionsFromCandidates,
685
+ step,
686
+ t,
687
+ ])
688
+
689
+ const applyValidationErrors = React.useCallback((issues: z.ZodIssue[]) => {
690
+ const nextErrors: Record<string, string> = {}
691
+ for (const issue of issues) {
692
+ const key = String(issue.path[0] ?? 'form')
693
+ if (!nextErrors[key]) nextErrors[key] = issue.message
694
+ }
695
+ setFieldErrors(nextErrors)
696
+ }, [])
697
+
698
+ const handleSetupContinue = React.useCallback(() => {
699
+ const parsed = setupSchema.safeParse({
700
+ warehouseId: form.warehouseId,
701
+ zoneId: form.zoneId,
702
+ fromLocationId: form.fromLocationId.trim() || undefined,
703
+ toLocationId: form.toLocationId.trim() || undefined,
704
+ assigneeId: form.assigneeId,
705
+ expectedSkus: form.expectedSkus,
706
+ setupNotes: form.setupNotes.trim() || undefined,
707
+ })
708
+ if (!parsed.success) {
709
+ applyValidationErrors(parsed.error.issues)
710
+ return
711
+ }
712
+ const zoneWarehouseId = zoneWarehouseByZoneIdRef.current[parsed.data.zoneId.trim()]
713
+ if (zoneWarehouseId && zoneWarehouseId !== parsed.data.warehouseId) {
714
+ setFieldErrors({
715
+ zoneId: t(
716
+ 'wms.backend.inventory.cycleCount.errors.zoneWarehouse',
717
+ 'Selected zone does not belong to this warehouse.',
718
+ ),
719
+ })
720
+ return
721
+ }
722
+
723
+ const fromLocId = form.fromLocationId.trim() || null
724
+ const toLocId = form.toLocationId.trim() || null
725
+ const hasRange = !!(fromLocId || toLocId)
726
+
727
+ if (!hasRange) {
728
+ const nextLocationId = form.fromLocationId.trim() || form.locationId.trim()
729
+ if (nextLocationId && nextLocationId !== form.locationId.trim()) {
730
+ patchForm({ locationId: nextLocationId })
731
+ }
732
+ }
733
+
734
+ setStep(2)
735
+
736
+ if (hasRange) {
737
+ setScopeQueue([])
738
+ setScopeQueueIndex(0)
739
+ setScopeQueueLoading(true)
740
+ setScopeQueueReady(false)
741
+ setScopeQueueError(null)
742
+
743
+ void buildCycleCountScopeQueue({
744
+ warehouseId: form.warehouseId,
745
+ fromLocationId: fromLocId,
746
+ toLocationId: toLocId,
747
+ })
748
+ .then((queue) => {
749
+ setScopeQueue(queue)
750
+ setScopeQueueIndex(0)
751
+ setScopeQueueReady(true)
752
+
753
+ for (const item of queue) {
754
+ registerOptionLabels([{ value: item.locationId, label: item.locationCode }])
755
+ }
756
+
757
+ if (queue.length > 0) {
758
+ const first = queue[0]
759
+ setForm((current) => ({
760
+ ...current,
761
+ locationId: first.locationId,
762
+ catalogVariantId: first.catalogVariantId,
763
+ lotId: first.lotId ?? '',
764
+ }))
765
+ }
766
+ })
767
+ .catch((error: unknown) => {
768
+ setScopeQueueReady(true)
769
+ if (error instanceof ScopeQueueError) {
770
+ setScopeQueueError(
771
+ t(
772
+ 'wms.backend.inventory.cycleCount.steps.counting.queueError',
773
+ 'Failed to load scope — enter location manually.',
774
+ ),
775
+ )
776
+ } else {
777
+ logger.error('buildCycleCountScopeQueue failed', { component: 'CycleCountWizardDialog', err: error })
778
+ setScopeQueueError(
779
+ t(
780
+ 'wms.backend.inventory.cycleCount.steps.counting.queueError',
781
+ 'Failed to load scope — enter location manually.',
782
+ ),
783
+ )
784
+ }
785
+ })
786
+ .finally(() => {
787
+ setScopeQueueLoading(false)
788
+ })
789
+ } else {
790
+ setScopeQueueReady(true)
791
+ setScopeQueueLoading(false)
792
+ setScopeQueueError(null)
793
+ }
794
+ }, [
795
+ applyValidationErrors,
796
+ form.assigneeId,
797
+ form.expectedSkus,
798
+ form.fromLocationId,
799
+ form.locationId,
800
+ form.setupNotes,
801
+ form.toLocationId,
802
+ form.warehouseId,
803
+ form.zoneId,
804
+ patchForm,
805
+ registerOptionLabels,
806
+ setupSchema,
807
+ t,
808
+ ])
809
+
810
+ const handleCountContinue = React.useCallback(async () => {
811
+ const parsed = countSchema.safeParse({
812
+ ...form,
813
+ lotId: form.lotId.trim() || undefined,
814
+ countNotes: form.countNotes.trim() || undefined,
815
+ })
816
+ if (!parsed.success) {
817
+ applyValidationErrors(parsed.error.issues)
818
+ return
819
+ }
820
+
821
+ setLoadingBalance(true)
822
+ try {
823
+ const onHand = await fetchBalanceOnHand({
824
+ warehouseId: parsed.data.warehouseId,
825
+ locationId: parsed.data.locationId,
826
+ catalogVariantId: parsed.data.catalogVariantId,
827
+ lotId: parsed.data.lotId ?? null,
828
+ })
829
+ setSystemOnHand(onHand)
830
+ setStep(3)
831
+ } catch (error) {
832
+ if (error instanceof BalanceLookupError && error.code === 'LOT_REQUIRED') {
833
+ const message = t(
834
+ 'wms.backend.inventory.cycleCount.errors.ambiguousLot',
835
+ 'Multiple lots found at this location — select a lot to continue.',
836
+ )
837
+ setFieldErrors({ lotId: message })
838
+ flash(message, 'error')
839
+ void seedLotSuggestionsFromCandidates(error.candidateLotIds)
840
+ return
841
+ }
842
+ logger.error('fetchBalanceOnHand failed', { component: 'CycleCountWizardDialog', err: error })
843
+ flash(
844
+ t('wms.backend.inventory.cycleCount.errors.balance', 'Failed to load system on-hand.'),
845
+ 'error',
846
+ )
847
+ } finally {
848
+ setLoadingBalance(false)
849
+ }
850
+ }, [applyValidationErrors, countSchema, form, seedLotSuggestionsFromCandidates, t])
851
+
852
+ const handlePost = React.useCallback(async () => {
853
+ const parsed = commitSchema.safeParse({ reason: form.reason })
854
+ if (!parsed.success) {
855
+ applyValidationErrors(parsed.error.issues)
856
+ return
857
+ }
858
+
859
+ if (!form.warehouseId || !form.locationId || !form.catalogVariantId) {
860
+ flash(
861
+ t('wms.backend.inventory.cycleCount.errors.scope', 'Complete setup and select a count location.'),
862
+ 'error',
863
+ )
864
+ return
865
+ }
866
+
867
+ if (!access.scopeReady || !access.organizationId || !access.tenantId || !access.userId) {
868
+ flash(
869
+ t(
870
+ 'wms.backend.inventory.mutations.errors.scope',
871
+ 'Select an organization and sign in before posting inventory changes.',
872
+ ),
873
+ 'error',
874
+ )
875
+ return
876
+ }
877
+
878
+ if (!autoAdjust && variance !== 0) {
879
+ flash(
880
+ t(
881
+ 'wms.backend.inventory.cycleCount.errors.autoAdjustRequired',
882
+ 'Enable auto-adjust to commit a variance.',
883
+ ),
884
+ 'error',
885
+ )
886
+ return
887
+ }
888
+
889
+ setSubmitting(true)
890
+ try {
891
+ const notes = [form.setupNotes.trim(), form.countNotes.trim()].filter(Boolean).join(' · ')
892
+ const setupMetadata = {
893
+ zoneId: form.zoneId.trim() || null,
894
+ fromLocationId: form.fromLocationId.trim() || null,
895
+ toLocationId: form.toLocationId.trim() || null,
896
+ scheduledAt: form.scheduledAt.trim() || null,
897
+ assigneeId: form.assigneeId.trim() || null,
898
+ expectedSkus: form.expectedSkus,
899
+ }
900
+ const payload: Record<string, unknown> = {
901
+ organizationId: access.organizationId,
902
+ tenantId: access.tenantId,
903
+ warehouseId: form.warehouseId,
904
+ locationId: form.locationId,
905
+ catalogVariantId: form.catalogVariantId,
906
+ countedQuantity: form.countedQuantity,
907
+ autoAdjust,
908
+ reason: parsed.data.reason,
909
+ referenceId,
910
+ performedBy: form.assigneeId.trim() || access.userId,
911
+ }
912
+ if (form.lotId.trim()) payload.lotId = form.lotId.trim()
913
+ if (notes || Object.values(setupMetadata).some((value) => value !== null && value !== 0)) {
914
+ payload.metadata = { notes: notes || undefined, setup: setupMetadata }
915
+ }
916
+
917
+ await runMutation({
918
+ operation: async () => {
919
+ const call = await apiCall<{
920
+ ok?: boolean
921
+ adjustmentDelta?: string
922
+ movementId?: string | null
923
+ }>('/api/wms/inventory/cycle-count', {
924
+ method: 'POST',
925
+ headers: { 'content-type': 'application/json' },
926
+ body: JSON.stringify(payload),
927
+ })
928
+ if (!call.ok) {
929
+ await raiseCrudError(
930
+ call.response,
931
+ t('wms.backend.inventory.cycleCount.errors.submit', 'Failed to post cycle count.'),
932
+ )
933
+ }
934
+ return call.result ?? {}
935
+ },
936
+ context: mutationContext,
937
+ mutationPayload: payload,
938
+ })
939
+
940
+ const deltaLabel = formatSignedQuantity(variance)
941
+ flash(
942
+ t('wms.backend.inventory.cycleCount.flash.success', 'Cycle count posted ({delta})', {
943
+ delta: deltaLabel,
944
+ }),
945
+ 'success',
946
+ )
947
+ await queryClient.invalidateQueries({ queryKey: ['wms-inventory-console'] })
948
+ await queryClient.invalidateQueries({ queryKey: ['wms-sku-detail'] })
949
+ setLinesPosted((n) => n + 1)
950
+ resetToStep2()
951
+ } catch (error) {
952
+ flashMutationError(error, t('wms.backend.inventory.cycleCount.errors.submit', 'Failed to post cycle count.'), t)
953
+ } finally {
954
+ setSubmitting(false)
955
+ }
956
+ }, [
957
+ access,
958
+ applyValidationErrors,
959
+ autoAdjust,
960
+ commitSchema,
961
+ form,
962
+ mutationContext,
963
+ queryClient,
964
+ referenceId,
965
+ resetToStep2,
966
+ runMutation,
967
+ t,
968
+ variance,
969
+ ])
970
+
971
+ const handlePrimaryAction = React.useCallback(() => {
972
+ if (step === 1) {
973
+ handleSetupContinue()
974
+ return
975
+ }
976
+ if (step === 2) {
977
+ void handleCountContinue()
978
+ return
979
+ }
980
+ void handlePost()
981
+ }, [handleCountContinue, handlePost, handleSetupContinue, step])
982
+
983
+ const handleDialogKeyDown = React.useCallback(
984
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
985
+ if (event.key === 'Escape') {
986
+ event.preventDefault()
987
+ closeDialog()
988
+ return
989
+ }
990
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
991
+ event.preventDefault()
992
+ if (submitting || loadingBalance) return
993
+ handlePrimaryAction()
994
+ }
995
+ },
996
+ [closeDialog, handlePrimaryAction, loadingBalance, submitting],
997
+ )
998
+
999
+ const adjustCounted = React.useCallback((delta: -1 | 1) => {
1000
+ setForm((current) => ({
1001
+ ...current,
1002
+ countedQuantity: Math.max(0, current.countedQuantity + delta),
1003
+ }))
1004
+ setFieldErrors({})
1005
+ }, [])
1006
+
1007
+ const adjustExpectedSkus = React.useCallback((delta: -1 | 1) => {
1008
+ setExpectedSkusTouched(true)
1009
+ setForm((current) => ({
1010
+ ...current,
1011
+ expectedSkus: Math.max(1, current.expectedSkus + delta),
1012
+ }))
1013
+ setFieldErrors({})
1014
+ }, [])
1015
+
1016
+ const isQueueActive = scopeQueueReady && scopeQueue.length > 0
1017
+ const isQueueComplete = isQueueActive && scopeQueueIndex >= scopeQueue.length
1018
+ const queueCurrent = scopeQueueIndex + 1
1019
+ const queueTotal = scopeQueue.length
1020
+
1021
+ const stepSubtitle = React.useMemo(() => {
1022
+ if (step === 1) {
1023
+ return t(
1024
+ 'wms.backend.inventory.cycleCount.steps.setup.subtitle',
1025
+ 'Step 1 of 3 · Set scope and expectations',
1026
+ )
1027
+ }
1028
+ if (step === 2) {
1029
+ if (linesPosted > 0) {
1030
+ return t(
1031
+ 'wms.backend.inventory.cycleCount.steps.counting.subtitleWithPosted',
1032
+ 'Step 2 of 3 · {count} line(s) posted — count next SKU',
1033
+ { count: linesPosted },
1034
+ )
1035
+ }
1036
+ return t(
1037
+ 'wms.backend.inventory.cycleCount.steps.counting.subtitle',
1038
+ 'Step 2 of 3 · Scan and tally items',
1039
+ )
1040
+ }
1041
+ return t(
1042
+ 'wms.backend.inventory.cycleCount.steps.review.subtitle',
1043
+ 'Step 3 of 3 · Review variances and commit',
1044
+ )
1045
+ }, [linesPosted, step, t])
1046
+
1047
+ const shortcutHint = React.useMemo(() => {
1048
+ if (step === 1) {
1049
+ return t('wms.backend.inventory.cycleCount.steps.setup.shortcut', 'to start')
1050
+ }
1051
+ if (step === 2) {
1052
+ return t('wms.backend.inventory.cycleCount.steps.counting.shortcut', 'to continue')
1053
+ }
1054
+ return t('wms.backend.inventory.cycleCount.steps.review.shortcut', 'to commit')
1055
+ }, [step, t])
1056
+
1057
+ const variantScanLabel = form.catalogVariantId
1058
+ ? resolveOptionLabel(form.catalogVariantId)
1059
+ : t(
1060
+ 'wms.backend.inventory.cycleCount.form.variantScanPlaceholder',
1061
+ 'Select a variant to begin scanning',
1062
+ )
1063
+
1064
+ const locationLabel = form.locationId ? resolveOptionLabel(form.locationId) : '—'
1065
+ const variantLabel = form.catalogVariantId ? resolveOptionLabel(form.catalogVariantId) : '—'
1066
+ const lotLabel = form.lotId ? resolveOptionLabel(form.lotId) : '—'
1067
+
1068
+ const primaryDisabled =
1069
+ submitting ||
1070
+ (step === 2 && (loadingBalance || scopeQueueLoading || isQueueComplete))
1071
+
1072
+ const loadRangeFilteredLocationOptions = React.useCallback(
1073
+ async (query?: string) => {
1074
+ const warehouseId = form.warehouseId.trim()
1075
+ if (!warehouseId) return []
1076
+
1077
+ const fromLocId = form.fromLocationId.trim()
1078
+ const toLocId = form.toLocationId.trim()
1079
+ if (!fromLocId && !toLocId) {
1080
+ const options = await loadLocationOptions(warehouseId, query)
1081
+ registerOptionLabels(options)
1082
+ return options.map((option) => ({ value: option.value, label: option.label }))
1083
+ }
1084
+
1085
+ try {
1086
+ const allLocations = await loadAllLocations(warehouseId, {
1087
+ search: query?.trim() || undefined,
1088
+ })
1089
+ const fromCode = fromLocId
1090
+ ? (optionLabelByValueRef.current[fromLocId] !== fromLocId
1091
+ ? optionLabelByValueRef.current[fromLocId]
1092
+ : undefined)
1093
+ : undefined
1094
+ const toCode = toLocId
1095
+ ? (optionLabelByValueRef.current[toLocId] !== toLocId
1096
+ ? optionLabelByValueRef.current[toLocId]
1097
+ : undefined)
1098
+ : undefined
1099
+ const filtered = filterLocationsByCodeRange(allLocations, fromCode, toCode)
1100
+ const options = mapLocationOptions(filtered)
1101
+ registerOptionLabels(options)
1102
+ return options.map((option) => ({ value: option.value, label: option.label }))
1103
+ } catch {
1104
+ const options = await loadLocationOptions(warehouseId, query)
1105
+ registerOptionLabels(options)
1106
+ return options.map((option) => ({ value: option.value, label: option.label }))
1107
+ }
1108
+ },
1109
+ [
1110
+ form.fromLocationId,
1111
+ form.toLocationId,
1112
+ form.warehouseId,
1113
+ registerOptionLabels,
1114
+ ],
1115
+ )
1116
+
1117
+ // Memoized so ComboboxInput's async-suggestion effect (keyed on this
1118
+ // callback's identity) doesn't get torn down and rescheduled by every
1119
+ // unrelated re-render of the wizard (balance lookups, label resolution,
1120
+ // etc.) — an inline arrow here would recreate on every render and could
1121
+ // starve the debounced fetch of a chance to ever resolve, leaving the
1122
+ // "Loading suggestions…" state stuck indefinitely.
1123
+ const loadLotSuggestionsForBalanceLocation = React.useCallback(
1124
+ async (query?: string) => {
1125
+ const options = await loadLotOptionsForBalanceLocation({
1126
+ warehouseId: form.warehouseId,
1127
+ locationId: form.locationId,
1128
+ catalogVariantId: form.catalogVariantId,
1129
+ query,
1130
+ })
1131
+ registerOptionLabels(options)
1132
+ return options.map((option) => ({ value: option.value, label: option.label }))
1133
+ },
1134
+ [form.catalogVariantId, form.locationId, form.warehouseId, registerOptionLabels],
1135
+ )
1136
+
1137
+ return (
1138
+ <Dialog open={open} onOpenChange={(next) => (next ? onOpenChange(true) : closeDialog())}>
1139
+ <DialogContent
1140
+ className="max-w-lg gap-0 overflow-hidden p-0"
1141
+ onKeyDown={handleDialogKeyDown}
1142
+ >
1143
+ <div className="border-b px-6 py-4 pr-12">
1144
+ <DialogHeader className="space-y-1 text-left">
1145
+ <DialogTitle>
1146
+ {t('wms.backend.inventory.cycleCount.dialog.title', 'Cycle count')}
1147
+ </DialogTitle>
1148
+ <DialogDescription>{stepSubtitle}</DialogDescription>
1149
+ <CycleCountStepIndicator step={step} />
1150
+ </DialogHeader>
1151
+ </div>
1152
+
1153
+ <form
1154
+ ref={formRef}
1155
+ onSubmit={(event) => {
1156
+ event.preventDefault()
1157
+ handlePrimaryAction()
1158
+ }}
1159
+ className="flex min-h-0 flex-1 flex-col"
1160
+ >
1161
+ <div className="flex max-h-[min(70vh,640px)] flex-col gap-5 overflow-y-auto px-6 py-6">
1162
+ {step === 1 ? (
1163
+ <>
1164
+ <FormField
1165
+ label={t('wms.backend.inventory.cycleCount.form.warehouse', 'Warehouse')}
1166
+ required
1167
+ error={fieldErrors.warehouseId}
1168
+ >
1169
+ <ComboboxInput
1170
+ value={form.warehouseId}
1171
+ onChange={(next) => {
1172
+ patchForm({
1173
+ warehouseId: next.trim(),
1174
+ zoneId: '',
1175
+ fromLocationId: '',
1176
+ toLocationId: '',
1177
+ locationId: '',
1178
+ })
1179
+ setExpectedSkusTouched(false)
1180
+ setScopeStats(null)
1181
+ setZoneSuggestions([])
1182
+ }}
1183
+ loadSuggestions={async (query) => {
1184
+ const options = await loadWarehouseOptions(query)
1185
+ registerOptionLabels(options)
1186
+ return options.map((option) => ({
1187
+ value: option.value,
1188
+ label: option.label,
1189
+ }))
1190
+ }}
1191
+ resolveLabel={resolveOptionLabel}
1192
+ placeholder={t(
1193
+ 'wms.backend.inventory.cycleCount.form.warehousePlaceholder',
1194
+ 'Select warehouse',
1195
+ )}
1196
+ allowCustomValues={false}
1197
+ disabled={loadingBalance || submitting}
1198
+ />
1199
+ </FormField>
1200
+
1201
+ <FormField
1202
+ label={t('wms.backend.inventory.cycleCount.form.zone', 'Zone')}
1203
+ required
1204
+ error={fieldErrors.zoneId}
1205
+ >
1206
+ <div className="relative [&_input]:pl-9">
1207
+ <Warehouse
1208
+ className="pointer-events-none absolute left-3 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground"
1209
+ aria-hidden="true"
1210
+ />
1211
+ <ComboboxInput
1212
+ key={form.warehouseId || 'cycle-count-zone'}
1213
+ value={form.zoneId}
1214
+ onChange={(next) => {
1215
+ const zoneId = next.trim()
1216
+ patchForm({
1217
+ zoneId,
1218
+ fromLocationId: '',
1219
+ toLocationId: '',
1220
+ locationId: '',
1221
+ })
1222
+ setExpectedSkusTouched(false)
1223
+ setScopeStats(null)
1224
+ }}
1225
+ suggestions={zoneSuggestions}
1226
+ loadSuggestions={loadZoneSuggestions}
1227
+ resolveLabel={(value) => {
1228
+ const label = resolveOptionLabel(value)
1229
+ if (value === form.zoneId.trim() && scopeStats) {
1230
+ const base = label.includes(' · ') ? label.split(' · ')[0] ?? label : label
1231
+ return formatCycleCountZoneLabel(base, scopeStats)
1232
+ }
1233
+ return label
1234
+ }}
1235
+ placeholder={
1236
+ form.warehouseId.trim()
1237
+ ? t(
1238
+ 'wms.backend.inventory.cycleCount.form.zonePlaceholder',
1239
+ 'Select zone',
1240
+ )
1241
+ : t(
1242
+ 'wms.backend.inventory.cycleCount.form.zonePlaceholderDisabled',
1243
+ 'Select warehouse first',
1244
+ )
1245
+ }
1246
+ allowCustomValues={false}
1247
+ disabled={loadingBalance || submitting || !form.warehouseId.trim()}
1248
+ />
1249
+ </div>
1250
+ {form.warehouseId.trim() ? (
1251
+ <p className="mt-1 text-xs text-muted-foreground">
1252
+ {form.zoneId.trim() && scopeStats
1253
+ ? t(
1254
+ 'wms.backend.inventory.cycleCount.form.zoneScopeHint',
1255
+ '{warehouse} · {expectedSkus} SKUs · {binCount} bins in scope',
1256
+ {
1257
+ warehouse: resolveOptionLabel(form.warehouseId),
1258
+ expectedSkus: scopeStats.expectedSkus,
1259
+ binCount: scopeStats.binCount,
1260
+ },
1261
+ )
1262
+ : t(
1263
+ 'wms.backend.inventory.cycleCount.form.zoneWarehouseHint',
1264
+ 'Functional zone within {warehouse}',
1265
+ { warehouse: resolveOptionLabel(form.warehouseId) },
1266
+ )}
1267
+ </p>
1268
+ ) : (
1269
+ <p className="mt-1 text-xs text-muted-foreground">
1270
+ {t(
1271
+ 'wms.backend.inventory.cycleCount.form.zoneSelectWarehouse',
1272
+ 'Select a warehouse to choose a zone.',
1273
+ )}
1274
+ </p>
1275
+ )}
1276
+ </FormField>
1277
+
1278
+ <div className="grid gap-5 sm:grid-cols-2">
1279
+ <FormField
1280
+ label={t('wms.backend.inventory.cycleCount.form.fromBin', 'From bin')}
1281
+ error={fieldErrors.fromLocationId}
1282
+ >
1283
+ <ComboboxInput
1284
+ value={form.fromLocationId}
1285
+ onChange={(next) => {
1286
+ patchForm({ fromLocationId: next.trim() })
1287
+ setExpectedSkusTouched(false)
1288
+ }}
1289
+ loadSuggestions={async (query) => {
1290
+ const options = await loadBinLocationOptions(form.warehouseId, query)
1291
+ registerOptionLabels(options)
1292
+ return options.map((option) => ({
1293
+ value: option.value,
1294
+ label: option.label,
1295
+ }))
1296
+ }}
1297
+ resolveLabel={resolveOptionLabel}
1298
+ placeholder={t(
1299
+ 'wms.backend.inventory.cycleCount.form.fromBinPlaceholder',
1300
+ 'Start bin',
1301
+ )}
1302
+ allowCustomValues={false}
1303
+ disabled={loadingBalance || submitting || !form.warehouseId.trim()}
1304
+ />
1305
+ </FormField>
1306
+
1307
+ <FormField
1308
+ label={t('wms.backend.inventory.cycleCount.form.toBin', 'To bin')}
1309
+ error={fieldErrors.toLocationId}
1310
+ >
1311
+ <ComboboxInput
1312
+ value={form.toLocationId}
1313
+ onChange={(next) => {
1314
+ patchForm({ toLocationId: next.trim() })
1315
+ setExpectedSkusTouched(false)
1316
+ }}
1317
+ loadSuggestions={async (query) => {
1318
+ const options = await loadBinLocationOptions(form.warehouseId, query)
1319
+ registerOptionLabels(options)
1320
+ return options.map((option) => ({
1321
+ value: option.value,
1322
+ label: option.label,
1323
+ }))
1324
+ }}
1325
+ resolveLabel={resolveOptionLabel}
1326
+ placeholder={t(
1327
+ 'wms.backend.inventory.cycleCount.form.toBinPlaceholder',
1328
+ 'End bin',
1329
+ )}
1330
+ allowCustomValues={false}
1331
+ disabled={loadingBalance || submitting || !form.warehouseId.trim()}
1332
+ />
1333
+ </FormField>
1334
+ </div>
1335
+
1336
+ <FormField
1337
+ label={t('wms.backend.inventory.cycleCount.form.schedule', 'Schedule')}
1338
+ error={fieldErrors.scheduledAt}
1339
+ >
1340
+ <DateTimePicker
1341
+ value={scheduledAtValue}
1342
+ onChange={(next) => {
1343
+ patchForm({ scheduledAt: next ? next.toISOString() : '' })
1344
+ }}
1345
+ placeholder={t(
1346
+ 'wms.backend.inventory.cycleCount.form.schedulePlaceholder',
1347
+ 'Pick date and time',
1348
+ )}
1349
+ disabled={loadingBalance || submitting}
1350
+ />
1351
+ {scheduleImmediate ? (
1352
+ <p className="mt-1 text-xs text-muted-foreground">
1353
+ {t(
1354
+ 'wms.backend.inventory.cycleCount.form.scheduleImmediate',
1355
+ 'Immediate start',
1356
+ )}
1357
+ </p>
1358
+ ) : null}
1359
+ </FormField>
1360
+
1361
+ <FormField
1362
+ label={t('wms.backend.inventory.cycleCount.form.expectedSkus', 'Expected SKUs')}
1363
+ required
1364
+ error={fieldErrors.expectedSkus}
1365
+ >
1366
+ <div className="flex w-32 items-center gap-2 rounded-md border bg-background p-2 shadow-xs">
1367
+ <IconButton
1368
+ type="button"
1369
+ variant="ghost"
1370
+ size="sm"
1371
+ aria-label={t(
1372
+ 'wms.backend.inventory.cycleCount.form.decreaseExpectedSkus',
1373
+ 'Decrease expected SKUs',
1374
+ )}
1375
+ onClick={() => adjustExpectedSkus(-1)}
1376
+ disabled={loadingBalance || submitting || loadingScopeEstimate}
1377
+ >
1378
+ <Minus className="size-4" />
1379
+ </IconButton>
1380
+ <Input
1381
+ type="number"
1382
+ inputMode="numeric"
1383
+ min={1}
1384
+ value={String(form.expectedSkus)}
1385
+ onChange={(event) => {
1386
+ const parsed = Number(event.target.value)
1387
+ if (!Number.isFinite(parsed) || parsed < 1) return
1388
+ setExpectedSkusTouched(true)
1389
+ patchForm({ expectedSkus: Math.floor(parsed) })
1390
+ }}
1391
+ className="h-8 w-auto min-w-0 flex-1 border-0 bg-transparent p-0 shadow-none"
1392
+ inputClassName="text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
1393
+ disabled={loadingBalance || submitting || loadingScopeEstimate}
1394
+ />
1395
+ <IconButton
1396
+ type="button"
1397
+ variant="ghost"
1398
+ size="sm"
1399
+ aria-label={t(
1400
+ 'wms.backend.inventory.cycleCount.form.increaseExpectedSkus',
1401
+ 'Increase expected SKUs',
1402
+ )}
1403
+ onClick={() => adjustExpectedSkus(1)}
1404
+ disabled={loadingBalance || submitting || loadingScopeEstimate}
1405
+ >
1406
+ <Plus className="size-4" />
1407
+ </IconButton>
1408
+ </div>
1409
+ {loadingScopeEstimate ? (
1410
+ <p className="mt-1 text-xs text-muted-foreground">
1411
+ {t(
1412
+ 'wms.backend.inventory.cycleCount.form.expectedSkusLoading',
1413
+ 'Estimating scope…',
1414
+ )}
1415
+ </p>
1416
+ ) : null}
1417
+ {scopeEstimateError ? (
1418
+ <p className="mt-1 text-xs text-status-warning-fg">{scopeEstimateError}</p>
1419
+ ) : null}
1420
+ </FormField>
1421
+
1422
+ <FormField
1423
+ label={t('wms.backend.inventory.cycleCount.form.assignee', 'Assignee')}
1424
+ required
1425
+ error={fieldErrors.assigneeId}
1426
+ >
1427
+ {assigneeCanListUsers ? (
1428
+ <ComboboxInput
1429
+ value={form.assigneeId}
1430
+ onChange={(next) => patchForm({ assigneeId: next.trim() })}
1431
+ loadSuggestions={async (query) => {
1432
+ const result = await loadAssigneeOptions(query, assigneeFallback ?? undefined)
1433
+ setAssigneeCanListUsers(result.canListUsers)
1434
+ registerOptionLabels(result.options)
1435
+ return result.options.map((option) => ({
1436
+ value: option.value,
1437
+ label: option.label,
1438
+ }))
1439
+ }}
1440
+ resolveLabel={resolveOptionLabel}
1441
+ placeholder={t(
1442
+ 'wms.backend.inventory.cycleCount.form.assigneePlaceholder',
1443
+ 'Select assignee',
1444
+ )}
1445
+ allowCustomValues={false}
1446
+ disabled={loadingBalance || submitting}
1447
+ />
1448
+ ) : (
1449
+ <div className="rounded-md border bg-muted/40 px-3 py-2.5 text-sm text-foreground">
1450
+ {resolveOptionLabel(form.assigneeId) || form.assigneeId}
1451
+ </div>
1452
+ )}
1453
+ {!assigneeCanListUsers ? (
1454
+ <p className="mt-1 text-xs text-muted-foreground">
1455
+ {t(
1456
+ 'wms.backend.inventory.cycleCount.form.assigneeLocked',
1457
+ 'Only the current user can be assigned without user-directory access.',
1458
+ )}
1459
+ </p>
1460
+ ) : null}
1461
+ </FormField>
1462
+
1463
+ <FormField label={t('wms.backend.inventory.cycleCount.form.setupNotes', 'Notes')}>
1464
+ <Textarea
1465
+ value={form.setupNotes}
1466
+ onChange={(event) => patchForm({ setupNotes: event.target.value })}
1467
+ placeholder={t(
1468
+ 'wms.backend.inventory.cycleCount.form.setupNotesPlaceholder',
1469
+ 'Optional — pre-count instructions',
1470
+ )}
1471
+ rows={3}
1472
+ disabled={loadingBalance || submitting}
1473
+ />
1474
+ </FormField>
1475
+ </>
1476
+ ) : null}
1477
+
1478
+ {step === 2 ? (
1479
+ <>
1480
+ {scopeQueueLoading ? (
1481
+ <p className="text-xs text-muted-foreground">
1482
+ {t(
1483
+ 'wms.backend.inventory.cycleCount.steps.counting.queueLoading',
1484
+ 'Loading count scope…',
1485
+ )}
1486
+ </p>
1487
+ ) : null}
1488
+
1489
+ {!scopeQueueLoading && scopeQueueError ? (
1490
+ <p className="text-xs text-status-warning-fg">{scopeQueueError}</p>
1491
+ ) : null}
1492
+
1493
+ {!scopeQueueLoading && scopeQueueReady && isQueueActive && !isQueueComplete ? (
1494
+ <div className="rounded-lg border bg-muted/40 px-4 py-3">
1495
+ <div className="flex items-center justify-between gap-3">
1496
+ <p className="text-xs font-semibold text-foreground">
1497
+ {t(
1498
+ 'wms.backend.inventory.cycleCount.steps.counting.queueItem',
1499
+ 'Item {current} of {total}',
1500
+ { current: queueCurrent, total: queueTotal },
1501
+ )}
1502
+ </p>
1503
+ <p className="text-xs text-muted-foreground">
1504
+ {resolveOptionLabel(form.locationId || '')}
1505
+ </p>
1506
+ </div>
1507
+ {form.expectedSkus > 0 ? (
1508
+ <p className="mt-1 text-xs text-muted-foreground">
1509
+ {t(
1510
+ 'wms.backend.inventory.cycleCount.steps.counting.progressSkus',
1511
+ '{posted} / {expected} SKUs counted',
1512
+ { posted: linesPosted, expected: form.expectedSkus },
1513
+ )}
1514
+ </p>
1515
+ ) : null}
1516
+ </div>
1517
+ ) : null}
1518
+
1519
+ {!scopeQueueLoading && scopeQueueReady && isQueueActive && !isQueueComplete && scopeQueueError === null && scopeQueue.length === 0 ? (
1520
+ <p className="text-xs text-muted-foreground">
1521
+ {t(
1522
+ 'wms.backend.inventory.cycleCount.steps.counting.queueEmpty',
1523
+ 'No items found in range — enter location manually.',
1524
+ )}
1525
+ </p>
1526
+ ) : null}
1527
+
1528
+ {isQueueComplete ? (
1529
+ <div className="rounded-lg border border-status-success-border bg-status-success-bg px-4 py-3">
1530
+ <p className="text-sm font-semibold text-status-success-fg">
1531
+ {t(
1532
+ 'wms.backend.inventory.cycleCount.steps.counting.queueComplete',
1533
+ 'All {total} items counted — session complete',
1534
+ { total: queueTotal },
1535
+ )}
1536
+ </p>
1537
+ </div>
1538
+ ) : null}
1539
+
1540
+ {!isQueueComplete ? (
1541
+ <>
1542
+ {!isQueueActive && form.expectedSkus > 0 ? (
1543
+ <p className="text-xs text-muted-foreground">
1544
+ {t(
1545
+ 'wms.backend.inventory.cycleCount.steps.counting.progressSkus',
1546
+ '{posted} / {expected} SKUs counted',
1547
+ { posted: linesPosted, expected: form.expectedSkus },
1548
+ )}
1549
+ </p>
1550
+ ) : null}
1551
+
1552
+ <FormField
1553
+ label={t(
1554
+ 'wms.backend.inventory.cycleCount.form.currentlyScanning',
1555
+ 'Currently scanning',
1556
+ )}
1557
+ >
1558
+ <div className="relative rounded-md bg-muted/50 px-3 py-2.5 pl-9 text-sm text-muted-foreground">
1559
+ <PackageSearch
1560
+ className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
1561
+ aria-hidden="true"
1562
+ />
1563
+ {variantScanLabel}
1564
+ </div>
1565
+ </FormField>
1566
+
1567
+ <FormField
1568
+ label={t('wms.backend.inventory.cycleCount.form.variant', 'Variant / SKU')}
1569
+ required
1570
+ error={fieldErrors.catalogVariantId}
1571
+ >
1572
+ <ComboboxInput
1573
+ value={form.catalogVariantId}
1574
+ onChange={(next) => {
1575
+ patchForm({
1576
+ catalogVariantId: next.trim(),
1577
+ lotId: '',
1578
+ })
1579
+ setLotSuggestions([])
1580
+ }}
1581
+ loadSuggestions={async (query) => {
1582
+ const options = await loadCatalogVariantOptions(query)
1583
+ registerOptionLabels(options)
1584
+ return options.map((option) => ({
1585
+ value: option.value,
1586
+ label: option.label,
1587
+ description: option.description,
1588
+ }))
1589
+ }}
1590
+ resolveLabel={resolveOptionLabel}
1591
+ placeholder={t(
1592
+ 'wms.backend.inventory.cycleCount.form.variantPlaceholder',
1593
+ 'Search variant or SKU',
1594
+ )}
1595
+ allowCustomValues={false}
1596
+ disabled={loadingBalance || submitting || scopeQueueLoading}
1597
+ />
1598
+ </FormField>
1599
+
1600
+ <div className="grid gap-5 sm:grid-cols-2">
1601
+ <FormField
1602
+ label={t('wms.backend.inventory.cycleCount.form.location', 'Location')}
1603
+ required
1604
+ error={fieldErrors.locationId}
1605
+ >
1606
+ <ComboboxInput
1607
+ value={form.locationId}
1608
+ onChange={(next) => {
1609
+ patchForm({ locationId: next.trim(), lotId: '' })
1610
+ setLotSuggestions([])
1611
+ }}
1612
+ loadSuggestions={loadRangeFilteredLocationOptions}
1613
+ resolveLabel={resolveOptionLabel}
1614
+ placeholder={t(
1615
+ 'wms.backend.inventory.cycleCount.form.locationPlaceholder',
1616
+ 'Select location',
1617
+ )}
1618
+ allowCustomValues={false}
1619
+ disabled={loadingBalance || submitting || !form.warehouseId || scopeQueueLoading}
1620
+ />
1621
+ </FormField>
1622
+
1623
+ <FormField
1624
+ label={t('wms.backend.inventory.cycleCount.form.lot', 'Lot')}
1625
+ error={fieldErrors.lotId}
1626
+ >
1627
+ <ComboboxInput
1628
+ value={form.lotId}
1629
+ onChange={(next) => patchForm({ lotId: next.trim() })}
1630
+ suggestions={lotSuggestions}
1631
+ loadSuggestions={loadLotSuggestionsForBalanceLocation}
1632
+ resolveLabel={resolveOptionLabel}
1633
+ placeholder={t(
1634
+ 'wms.backend.inventory.cycleCount.form.lotPlaceholder',
1635
+ 'Select lot (optional)',
1636
+ )}
1637
+ allowCustomValues={false}
1638
+ disabled={
1639
+ loadingBalance
1640
+ || submitting
1641
+ || !form.catalogVariantId
1642
+ || !form.locationId
1643
+ }
1644
+ />
1645
+ </FormField>
1646
+ </div>
1647
+
1648
+ <FormField
1649
+ label={t('wms.backend.inventory.cycleCount.form.counted', 'Counted')}
1650
+ required
1651
+ error={fieldErrors.countedQuantity}
1652
+ >
1653
+ <div className="flex w-32 items-center gap-2 rounded-md border bg-background p-2 shadow-xs">
1654
+ <IconButton
1655
+ type="button"
1656
+ variant="ghost"
1657
+ size="sm"
1658
+ aria-label={t(
1659
+ 'wms.backend.inventory.cycleCount.form.decrease',
1660
+ 'Decrease quantity',
1661
+ )}
1662
+ onClick={() => adjustCounted(-1)}
1663
+ disabled={loadingBalance || submitting}
1664
+ >
1665
+ <Minus className="size-4" />
1666
+ </IconButton>
1667
+ <Input
1668
+ type="number"
1669
+ inputMode="numeric"
1670
+ min={0}
1671
+ value={String(form.countedQuantity)}
1672
+ onChange={(event) => {
1673
+ const parsed = Number(event.target.value)
1674
+ if (!Number.isFinite(parsed) || parsed < 0) return
1675
+ patchForm({ countedQuantity: parsed })
1676
+ }}
1677
+ className="h-8 w-auto min-w-0 flex-1 border-0 bg-transparent p-0 shadow-none"
1678
+ inputClassName="text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
1679
+ disabled={loadingBalance || submitting}
1680
+ />
1681
+ <IconButton
1682
+ type="button"
1683
+ variant="ghost"
1684
+ size="sm"
1685
+ aria-label={t(
1686
+ 'wms.backend.inventory.cycleCount.form.increase',
1687
+ 'Increase quantity',
1688
+ )}
1689
+ onClick={() => adjustCounted(1)}
1690
+ disabled={loadingBalance || submitting}
1691
+ >
1692
+ <Plus className="size-4" />
1693
+ </IconButton>
1694
+ </div>
1695
+ </FormField>
1696
+
1697
+ <FormField label={t('wms.backend.inventory.cycleCount.form.countNotes', 'Notes')}>
1698
+ <Textarea
1699
+ value={form.countNotes}
1700
+ onChange={(event) => patchForm({ countNotes: event.target.value })}
1701
+ placeholder={t(
1702
+ 'wms.backend.inventory.cycleCount.form.countNotesPlaceholder',
1703
+ 'Optional — defects, packaging notes',
1704
+ )}
1705
+ rows={3}
1706
+ disabled={loadingBalance || submitting}
1707
+ />
1708
+ </FormField>
1709
+
1710
+ {form.catalogVariantId && form.locationId ? (
1711
+ <SummaryPanel
1712
+ title={t('wms.backend.inventory.cycleCount.review.progress.title', 'Progress')}
1713
+ >
1714
+ <div className="mt-2 flex items-center justify-between gap-3">
1715
+ <p className="text-sm font-semibold tabular-nums text-foreground">
1716
+ {t(
1717
+ 'wms.backend.inventory.cycleCount.review.progress.summary',
1718
+ 'System {system} · Counted {counted}',
1719
+ {
1720
+ system: systemOnHand,
1721
+ counted: form.countedQuantity,
1722
+ },
1723
+ )}
1724
+ </p>
1725
+ {variance === 0 ? (
1726
+ <StatusBadge variant="success">
1727
+ {t('wms.backend.inventory.cycleCount.review.progress.match', 'Match')}
1728
+ </StatusBadge>
1729
+ ) : (
1730
+ <StatusBadge variant="info">
1731
+ {formatSignedQuantity(variance)}
1732
+ </StatusBadge>
1733
+ )}
1734
+ </div>
1735
+ {loadingBalance ? (
1736
+ <p className="mt-1 text-xs text-muted-foreground">
1737
+ {t(
1738
+ 'wms.backend.inventory.cycleCount.review.progress.loading',
1739
+ 'Refreshing balance…',
1740
+ )}
1741
+ </p>
1742
+ ) : null}
1743
+ {balanceError ? (
1744
+ <p className="mt-1 text-xs text-status-warning-fg">{balanceError}</p>
1745
+ ) : null}
1746
+ </SummaryPanel>
1747
+ ) : null}
1748
+ </>
1749
+ ) : null}
1750
+ </>
1751
+ ) : null}
1752
+
1753
+ {step === 3 ? (
1754
+ <>
1755
+ <FormField
1756
+ label={t(
1757
+ 'wms.backend.inventory.cycleCount.review.totalCounted',
1758
+ 'Total counted',
1759
+ )}
1760
+ >
1761
+ <div className="rounded-md bg-muted/50 px-3 py-2.5 text-sm text-muted-foreground">
1762
+ {t(
1763
+ 'wms.backend.inventory.cycleCount.review.totalCountedSummary',
1764
+ '{counted} units · {location} · {variant}',
1765
+ {
1766
+ counted: form.countedQuantity,
1767
+ location: locationLabel,
1768
+ variant: variantLabel,
1769
+ },
1770
+ )}
1771
+ </div>
1772
+ </FormField>
1773
+
1774
+ <div className="grid gap-5 sm:grid-cols-2">
1775
+ <FormField
1776
+ label={t('wms.backend.inventory.cycleCount.review.matches', 'Matches')}
1777
+ >
1778
+ <div className="rounded-md border bg-background px-3 py-2.5 text-sm text-foreground">
1779
+ {variance === 0
1780
+ ? t(
1781
+ 'wms.backend.inventory.cycleCount.review.matchesWithinTolerance',
1782
+ 'Within tolerance',
1783
+ )
1784
+ : t('wms.backend.inventory.cycleCount.review.matchesNone', 'No match')}
1785
+ </div>
1786
+ </FormField>
1787
+
1788
+ <FormField
1789
+ label={t('wms.backend.inventory.cycleCount.review.variances', 'Variances')}
1790
+ >
1791
+ <div className="rounded-md border bg-background px-3 py-2.5 text-sm text-foreground">
1792
+ {variance === 0
1793
+ ? t('wms.backend.inventory.cycleCount.review.variancesNone', 'None')
1794
+ : formatSignedQuantity(variance)}
1795
+ </div>
1796
+ </FormField>
1797
+ </div>
1798
+
1799
+ {variance !== 0 ? (
1800
+ <FormField
1801
+ label={t('wms.backend.inventory.cycleCount.review.varianceDetail', 'Variance')}
1802
+ >
1803
+ <div className="space-y-1">
1804
+ <p className="text-xs text-muted-foreground">
1805
+ {locationLabel} · {variantLabel}
1806
+ {form.lotId ? ` · ${lotLabel}` : ''}
1807
+ </p>
1808
+ <div className="rounded-md border bg-background px-3 py-2.5 text-sm text-foreground">
1809
+ {t(
1810
+ 'wms.backend.inventory.cycleCount.review.varianceLine',
1811
+ 'Counted {counted} vs expected {expected} · {delta}',
1812
+ {
1813
+ counted: form.countedQuantity,
1814
+ expected: systemOnHand,
1815
+ delta: formatSignedQuantity(variance),
1816
+ },
1817
+ )}
1818
+ </div>
1819
+ </div>
1820
+ </FormField>
1821
+ ) : (
1822
+ <p className="text-sm text-muted-foreground">
1823
+ {t(
1824
+ 'wms.backend.inventory.cycleCount.review.noVariance',
1825
+ 'Counts match the ledger. Posting will not create a movement.',
1826
+ )}
1827
+ </p>
1828
+ )}
1829
+
1830
+ <FormField
1831
+ label={t(
1832
+ 'wms.backend.inventory.cycleCount.form.autoAdjust',
1833
+ 'Auto-adjust on commit',
1834
+ )}
1835
+ required={variance !== 0}
1836
+ >
1837
+ <div className="flex items-center gap-3">
1838
+ <Switch
1839
+ checked={autoAdjust}
1840
+ onCheckedChange={setAutoAdjust}
1841
+ disabled={submitting || variance === 0}
1842
+ />
1843
+ <p className="text-sm font-medium text-foreground">
1844
+ {variance === 0
1845
+ ? t(
1846
+ 'wms.backend.inventory.cycleCount.form.autoAdjustNoVariance',
1847
+ 'No ledger write required — counts match.',
1848
+ )
1849
+ : autoAdjust
1850
+ ? t(
1851
+ 'wms.backend.inventory.cycleCount.form.autoAdjustHint',
1852
+ 'Yes — writes a cycle-count movement on commit',
1853
+ )
1854
+ : t(
1855
+ 'wms.backend.inventory.cycleCount.form.autoAdjustHintOff',
1856
+ 'No — ledger unchanged on commit',
1857
+ )}
1858
+ </p>
1859
+ </div>
1860
+ </FormField>
1861
+
1862
+ <FormField
1863
+ label={t('wms.backend.inventory.cycleCount.form.reason', 'Reason')}
1864
+ required
1865
+ error={fieldErrors.reason}
1866
+ >
1867
+ <Textarea
1868
+ value={form.reason}
1869
+ onChange={(event) => patchForm({ reason: event.target.value })}
1870
+ placeholder={t(
1871
+ 'wms.backend.inventory.cycleCount.form.reasonPlaceholder',
1872
+ 'Required for variances — auditor context',
1873
+ )}
1874
+ rows={3}
1875
+ disabled={submitting}
1876
+ />
1877
+ </FormField>
1878
+
1879
+ <SummaryPanel
1880
+ title={t(
1881
+ 'wms.backend.inventory.cycleCount.review.commitSummary.title',
1882
+ 'Commit summary',
1883
+ )}
1884
+ >
1885
+ <div className="mt-2 flex items-center justify-between gap-3">
1886
+ <p className="text-sm font-semibold tabular-nums text-foreground">
1887
+ {variance === 0 || !autoAdjust
1888
+ ? t(
1889
+ 'wms.backend.inventory.cycleCount.review.commitSummary.noMovement',
1890
+ 'No movement · ledger unchanged',
1891
+ )
1892
+ : t(
1893
+ 'wms.backend.inventory.cycleCount.review.commitSummary.movement',
1894
+ '{delta} net · 1 ledger write',
1895
+ { delta: formatSignedQuantity(variance) },
1896
+ )}
1897
+ </p>
1898
+ <StatusBadge variant="success">
1899
+ {t('wms.backend.inventory.cycleCount.review.commitSummary.ready', 'Ready')}
1900
+ </StatusBadge>
1901
+ </div>
1902
+ </SummaryPanel>
1903
+ </>
1904
+ ) : null}
1905
+ </div>
1906
+
1907
+ <DialogFooter bordered={false} className="border-t px-6 py-4 sm:justify-between">
1908
+ <p className="hidden text-xs text-muted-foreground md:inline-flex md:items-center md:gap-1.5">
1909
+ <KbdShortcut keys={['⌘', 'Enter']} />
1910
+ <span>/</span>
1911
+ <KbdShortcut keys={['Ctrl', 'Enter']} />
1912
+ <span>{shortcutHint}</span>
1913
+ </p>
1914
+ <div className="flex w-full flex-col-reverse gap-2 md:w-auto md:flex-row md:flex-wrap md:justify-end">
1915
+ {step === 1 ? (
1916
+ <Button
1917
+ type="button"
1918
+ variant="outline"
1919
+ onClick={closeDialog}
1920
+ disabled={loadingBalance || submitting}
1921
+ >
1922
+ {t('common.cancel', 'Cancel')}
1923
+ </Button>
1924
+ ) : (
1925
+ <Button
1926
+ type="button"
1927
+ variant="outline"
1928
+ onClick={() => setStep((current) => (current === 3 ? 2 : 1))}
1929
+ disabled={loadingBalance || submitting}
1930
+ >
1931
+ {t('wms.backend.inventory.cycleCount.steps.back', 'Back')}
1932
+ </Button>
1933
+ )}
1934
+ {step === 2 && (linesPosted > 0 || isQueueComplete) ? (
1935
+ <Button
1936
+ type="button"
1937
+ variant={finishConfirmPending ? 'destructive' : 'outline'}
1938
+ onClick={() => {
1939
+ const remaining = form.expectedSkus - linesPosted
1940
+ if (!finishConfirmPending && remaining > 0 && !isQueueComplete) {
1941
+ setFinishConfirmPending(true)
1942
+ return
1943
+ }
1944
+ closeDialog()
1945
+ }}
1946
+ disabled={loadingBalance || submitting}
1947
+ title={
1948
+ isQueueComplete
1949
+ ? undefined
1950
+ : t(
1951
+ 'wms.backend.inventory.cycleCount.steps.counting.finishTooltip',
1952
+ '{count} line(s) already committed — click to close without losing them',
1953
+ { count: linesPosted },
1954
+ )
1955
+ }
1956
+ >
1957
+ {finishConfirmPending
1958
+ ? t(
1959
+ 'wms.backend.inventory.cycleCount.steps.counting.finishConfirm',
1960
+ 'Confirm finish',
1961
+ )
1962
+ : isQueueComplete
1963
+ ? t(
1964
+ 'wms.backend.inventory.cycleCount.steps.counting.finishComplete',
1965
+ 'Close session',
1966
+ )
1967
+ : t(
1968
+ 'wms.backend.inventory.cycleCount.steps.counting.finish',
1969
+ 'Finish session ({count})',
1970
+ { count: linesPosted },
1971
+ )}
1972
+ </Button>
1973
+ ) : null}
1974
+ {finishConfirmPending ? (
1975
+ <p className="order-first text-xs text-status-warning-fg sm:order-none sm:self-center">
1976
+ {t(
1977
+ 'wms.backend.inventory.cycleCount.steps.counting.finishWarning',
1978
+ '{remaining} item(s) still in scope — confirm to finish early.',
1979
+ { remaining: form.expectedSkus - linesPosted },
1980
+ )}
1981
+ </p>
1982
+ ) : null}
1983
+ {!isQueueComplete ? (
1984
+ <Button type="submit" disabled={primaryDisabled}>
1985
+ {step === 1
1986
+ ? t(
1987
+ 'wms.backend.inventory.cycleCount.steps.setup.submit',
1988
+ 'Start counting',
1989
+ )
1990
+ : step === 2
1991
+ ? t(
1992
+ 'wms.backend.inventory.cycleCount.steps.counting.submit',
1993
+ 'Review variances',
1994
+ )
1995
+ : t(
1996
+ 'wms.backend.inventory.cycleCount.steps.review.submit',
1997
+ 'Commit & count next',
1998
+ )}
1999
+ </Button>
2000
+ ) : null}
2001
+ </div>
2002
+ </DialogFooter>
2003
+ </form>
2004
+ </DialogContent>
2005
+ </Dialog>
2006
+ )
2007
+ }