@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,1018 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useQueryClient } from '@tanstack/react-query'
5
+ import { ArrowRight, Check, FileText } from 'lucide-react'
6
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
+ import { flashMutationError } from '../../lib/flashMutationError'
8
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
9
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
10
+ import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
11
+ import { Button } from '@open-mercato/ui/primitives/button'
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ } from '@open-mercato/ui/primitives/dialog'
20
+ import { Input } from '@open-mercato/ui/primitives/input'
21
+ import { KbdShortcut } from '@open-mercato/ui/primitives/kbd'
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '@open-mercato/ui/primitives/select'
29
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
30
+ import { Switch } from '@open-mercato/ui/primitives/switch'
31
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
32
+ import { createLogger } from '@open-mercato/shared/lib/logger'
33
+ import { cn } from '@open-mercato/shared/lib/utils'
34
+ import {
35
+ countMappedColumns,
36
+ detectColumnMappings,
37
+ mapCsvRowsWithMappings,
38
+ parseCsvText,
39
+ type DetectedColumnMapping,
40
+ type InventoryImportRawRow,
41
+ } from '../../lib/inventoryImportCsv'
42
+ import type { useWmsInventoryMutationAccess } from './useWmsInventoryMutationAccess'
43
+
44
+ const logger = createLogger('wms')
45
+
46
+ type ImportAccess = Pick<
47
+ ReturnType<typeof useWmsInventoryMutationAccess>,
48
+ 'organizationId' | 'tenantId' | 'userId' | 'scopeReady'
49
+ >
50
+
51
+ type WizardStep = 1 | 2 | 3
52
+
53
+ type ValidationRow = {
54
+ rowNumber: number
55
+ status: 'valid' | 'error' | 'warning' | 'skip'
56
+ errors: string[]
57
+ warnings: string[]
58
+ resolved?: {
59
+ warehouseId: string
60
+ locationId: string
61
+ catalogVariantId: string
62
+ quantity: number
63
+ delta: number
64
+ currentOnHand?: number
65
+ lotId?: string
66
+ serialNumber?: string
67
+ sku?: string
68
+ locationCode?: string
69
+ warehouseCode?: string
70
+ }
71
+ }
72
+
73
+ type ValidationResponse = {
74
+ ok?: boolean
75
+ importBatchId?: string
76
+ summary?: {
77
+ totalRows?: number
78
+ validRows?: number
79
+ errorRows?: number
80
+ warningRows?: number
81
+ skipRows?: number
82
+ }
83
+ rows?: ValidationRow[]
84
+ }
85
+
86
+ type ParsedFileMeta = {
87
+ headers: string[]
88
+ rowCount: number
89
+ fileSize: number
90
+ }
91
+
92
+ type ImportInventoryDialogProps = {
93
+ open: boolean
94
+ onOpenChange: (open: boolean) => void
95
+ access: ImportAccess
96
+ }
97
+
98
+ const TARGET_FIELD_KEYS: Array<keyof InventoryImportRawRow> = [
99
+ 'warehouseCode',
100
+ 'locationCode',
101
+ 'sku',
102
+ 'quantity',
103
+ 'lotNumber',
104
+ 'serialNumber',
105
+ ]
106
+
107
+ function ImportStepIndicator({ step }: { step: WizardStep }) {
108
+ const steps = [1, 2, 3] as const
109
+
110
+ return (
111
+ <div className="flex items-center gap-1.5 pt-1" aria-label={`Step ${step} of 3`}>
112
+ {steps.map((stepNumber, index) => {
113
+ const completed = stepNumber < step
114
+ const current = stepNumber === step
115
+
116
+ return (
117
+ <React.Fragment key={stepNumber}>
118
+ {index > 0 ? (
119
+ <div
120
+ className={cn(
121
+ 'h-0.5 w-4 shrink-0',
122
+ stepNumber <= step ? 'bg-foreground' : 'bg-border',
123
+ )}
124
+ aria-hidden="true"
125
+ />
126
+ ) : null}
127
+ <div
128
+ className={cn(
129
+ 'flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-semibold',
130
+ completed || current
131
+ ? 'bg-foreground text-primary-foreground'
132
+ : 'bg-muted text-muted-foreground',
133
+ )}
134
+ aria-current={current ? 'step' : undefined}
135
+ >
136
+ {completed ? <Check className="size-3" aria-hidden="true" /> : stepNumber}
137
+ </div>
138
+ </React.Fragment>
139
+ )
140
+ })}
141
+ </div>
142
+ )
143
+ }
144
+
145
+ function formatFileSize(bytes: number): string {
146
+ if (bytes < 1024) return `${bytes} B`
147
+ const kilobytes = bytes / 1024
148
+ if (kilobytes < 1024) return `${Math.round(kilobytes)} KB`
149
+ return `${(kilobytes / 1024).toFixed(1)} MB`
150
+ }
151
+
152
+ function formatRowCount(value: number): string {
153
+ return new Intl.NumberFormat().format(value)
154
+ }
155
+
156
+ function formatImportIssue(
157
+ code: string,
158
+ t: ReturnType<typeof useT>,
159
+ ): string {
160
+ const duplicateMatch = /^duplicate_of_row_(\d+)$/.exec(code)
161
+ if (duplicateMatch) {
162
+ return t(
163
+ 'wms.backend.inventory.import.validation.duplicate_of_row',
164
+ 'Duplicate of row {row}',
165
+ { row: duplicateMatch[1] },
166
+ )
167
+ }
168
+ return t(`wms.backend.inventory.import.validation.${code}`, code)
169
+ }
170
+
171
+ export function ImportInventoryDialog({ open, onOpenChange, access }: ImportInventoryDialogProps) {
172
+ const t = useT()
173
+ const queryClient = useQueryClient()
174
+ const fileInputRef = React.useRef<HTMLInputElement>(null)
175
+ const [step, setStep] = React.useState<WizardStep>(1)
176
+ const [selectedFile, setSelectedFile] = React.useState<File | null>(null)
177
+ const [parsedFile, setParsedFile] = React.useState<ParsedFileMeta | null>(null)
178
+ const [columnMappings, setColumnMappings] = React.useState<DetectedColumnMapping[]>([])
179
+ const [validation, setValidation] = React.useState<ValidationResponse | null>(null)
180
+ const [validating, setValidating] = React.useState(false)
181
+ const [applying, setApplying] = React.useState(false)
182
+ const [showIssueDetails, setShowIssueDetails] = React.useState(false)
183
+ const [skipDuplicates, setSkipDuplicates] = React.useState(true)
184
+ const [reconcileMode, setReconcileMode] = React.useState(false)
185
+ const [isDragging, setIsDragging] = React.useState(false)
186
+ const { runMutation, retryLastMutation } = useGuardedMutation({ contextId: 'wms-inventory-import' })
187
+ const mutationContext = React.useMemo(
188
+ () => ({ retryLastMutation }),
189
+ [retryLastMutation],
190
+ )
191
+
192
+ const resetState = React.useCallback(() => {
193
+ setStep(1)
194
+ setSelectedFile(null)
195
+ setParsedFile(null)
196
+ setColumnMappings([])
197
+ setValidation(null)
198
+ setValidating(false)
199
+ setApplying(false)
200
+ setShowIssueDetails(false)
201
+ setSkipDuplicates(true)
202
+ setReconcileMode(false)
203
+ setIsDragging(false)
204
+ }, [])
205
+
206
+ const closeDialog = React.useCallback(() => {
207
+ onOpenChange(false)
208
+ resetState()
209
+ }, [onOpenChange, resetState])
210
+
211
+ React.useEffect(() => {
212
+ if (!open) resetState()
213
+ }, [open, resetState])
214
+
215
+ const targetFieldLabel = React.useCallback(
216
+ (field: keyof InventoryImportRawRow) => {
217
+ const labels: Record<keyof InventoryImportRawRow, string> = {
218
+ warehouseCode: t('wms.backend.inventory.import.mapping.fields.warehouseCode', 'warehouse_code'),
219
+ warehouseId: t('wms.backend.inventory.import.mapping.fields.warehouseId', 'warehouse_id'),
220
+ locationCode: t('wms.backend.inventory.import.mapping.fields.locationCode', 'location_code'),
221
+ locationId: t('wms.backend.inventory.import.mapping.fields.locationId', 'location_id'),
222
+ sku: t('wms.backend.inventory.import.mapping.fields.sku', 'sku'),
223
+ catalogVariantId: t(
224
+ 'wms.backend.inventory.import.mapping.fields.catalogVariantId',
225
+ 'catalog_variant_id',
226
+ ),
227
+ quantity: t('wms.backend.inventory.import.mapping.fields.quantity', 'quantity'),
228
+ lotNumber: t('wms.backend.inventory.import.mapping.fields.lotNumber', 'lot_number'),
229
+ lotId: t('wms.backend.inventory.import.mapping.fields.lotId', 'lot_id'),
230
+ serialNumber: t('wms.backend.inventory.import.mapping.fields.serialNumber', 'serial_number'),
231
+ }
232
+ return labels[field]
233
+ },
234
+ [t],
235
+ )
236
+
237
+ const parseSelectedFile = React.useCallback(async (file: File) => {
238
+ const text = await file.text()
239
+ const parsed = parseCsvText(text)
240
+ const meta: ParsedFileMeta = {
241
+ headers: parsed.headers,
242
+ rowCount: parsed.rows.length,
243
+ fileSize: file.size,
244
+ }
245
+ setParsedFile(meta)
246
+ setColumnMappings(detectColumnMappings(parsed.headers))
247
+ setValidation(null)
248
+ }, [])
249
+
250
+ const handleFileSelection = React.useCallback(
251
+ async (file: File | null) => {
252
+ setSelectedFile(file)
253
+ if (!file) {
254
+ setParsedFile(null)
255
+ setColumnMappings([])
256
+ setValidation(null)
257
+ return
258
+ }
259
+ await parseSelectedFile(file)
260
+ },
261
+ [parseSelectedFile],
262
+ )
263
+
264
+ const handleDownloadTemplate = React.useCallback(() => {
265
+ const anchor = document.createElement('a')
266
+ anchor.href = '/api/wms/inventory/import/template'
267
+ anchor.download = 'wms-inventory-import-template.csv'
268
+ anchor.click()
269
+ }, [])
270
+
271
+ const handleAutoMap = React.useCallback(() => {
272
+ if (!parsedFile) return
273
+ setColumnMappings(detectColumnMappings(parsedFile.headers))
274
+ }, [parsedFile])
275
+
276
+ const handleReconcileModeChange = React.useCallback((checked: boolean) => {
277
+ setReconcileMode(checked)
278
+ setValidation(null)
279
+ }, [])
280
+
281
+ const handleMappingChange = React.useCallback(
282
+ (csvColumn: string, value: string) => {
283
+ setColumnMappings((current) =>
284
+ current.map((mapping) => {
285
+ if (mapping.csvColumn !== csvColumn) return mapping
286
+ if (value === 'skip') {
287
+ return { ...mapping, targetField: null, status: 'skipped' }
288
+ }
289
+ return {
290
+ ...mapping,
291
+ targetField: value as keyof InventoryImportRawRow,
292
+ status: 'mapped',
293
+ }
294
+ }),
295
+ )
296
+ setValidation(null)
297
+ },
298
+ [],
299
+ )
300
+
301
+ const handleDragOver = React.useCallback((event: React.DragEvent<HTMLButtonElement>) => {
302
+ event.preventDefault()
303
+ event.stopPropagation()
304
+ setIsDragging(true)
305
+ }, [])
306
+
307
+ const handleDragLeave = React.useCallback((event: React.DragEvent<HTMLButtonElement>) => {
308
+ event.preventDefault()
309
+ event.stopPropagation()
310
+ setIsDragging(false)
311
+ }, [])
312
+
313
+ const handleDrop = React.useCallback(
314
+ (event: React.DragEvent<HTMLButtonElement>) => {
315
+ event.preventDefault()
316
+ event.stopPropagation()
317
+ setIsDragging(false)
318
+ const file = event.dataTransfer.files?.[0]
319
+ if (file) void handleFileSelection(file)
320
+ },
321
+ [handleFileSelection],
322
+ )
323
+
324
+ const handleValidate = React.useCallback(async () => {
325
+ if (!access.scopeReady || !access.organizationId || !access.tenantId) {
326
+ flash(t('wms.backend.inventory.import.errors.scope', 'Select organization scope before importing.'), 'error')
327
+ return false
328
+ }
329
+ if (!selectedFile) {
330
+ flash(t('wms.backend.inventory.import.errors.file', 'Choose a CSV file first.'), 'error')
331
+ return false
332
+ }
333
+
334
+ setValidating(true)
335
+ try {
336
+ const text = await selectedFile.text()
337
+ const parsed = parseCsvText(text)
338
+ const rows = mapCsvRowsWithMappings(parsed.headers, parsed.rows, columnMappings)
339
+ const call = await apiCall<ValidationResponse>('/api/wms/inventory/import/validate', {
340
+ method: 'POST',
341
+ headers: { 'content-type': 'application/json' },
342
+ body: JSON.stringify({
343
+ organizationId: access.organizationId,
344
+ tenantId: access.tenantId,
345
+ skipDuplicates,
346
+ mode: reconcileMode ? 'reconcile' : 'additive',
347
+ rows,
348
+ }),
349
+ })
350
+ if (!call.ok) {
351
+ await raiseCrudError(
352
+ call.response,
353
+ t('wms.backend.inventory.import.errors.validate', 'Validation failed.'),
354
+ )
355
+ }
356
+ setValidation(call.result ?? null)
357
+ if (call.result?.ok) {
358
+ flash(t('wms.backend.inventory.import.flash.validated', 'CSV validated — review rows before applying.'), 'success')
359
+ } else {
360
+ flash(t('wms.backend.inventory.import.flash.validationErrors', 'Fix validation errors before applying.'), 'error')
361
+ }
362
+ return Boolean(call.result)
363
+ } catch (error) {
364
+ logger.error('validate failed', { component: 'ImportInventoryDialog', err: error })
365
+ flash(t('wms.backend.inventory.import.errors.validate', 'Validation failed.'), 'error')
366
+ return false
367
+ } finally {
368
+ setValidating(false)
369
+ }
370
+ }, [
371
+ access.organizationId,
372
+ access.scopeReady,
373
+ access.tenantId,
374
+ columnMappings,
375
+ reconcileMode,
376
+ selectedFile,
377
+ skipDuplicates,
378
+ t,
379
+ ])
380
+
381
+ const applyRows = React.useMemo(() => {
382
+ return (validation?.rows ?? [])
383
+ .filter((row) => row.resolved && (row.status === 'valid' || row.status === 'warning' || row.status === 'skip'))
384
+ .map((row) => ({
385
+ rowNumber: row.rowNumber,
386
+ warehouseId: row.resolved!.warehouseId,
387
+ locationId: row.resolved!.locationId,
388
+ catalogVariantId: row.resolved!.catalogVariantId,
389
+ quantity: row.resolved!.quantity,
390
+ delta: row.resolved!.delta,
391
+ lotId: row.resolved!.lotId,
392
+ serialNumber: row.resolved!.serialNumber,
393
+ }))
394
+ }, [validation?.rows])
395
+
396
+ const overwriteWarningRows = React.useMemo(() => {
397
+ return (validation?.rows ?? []).filter(
398
+ (row) => row.status === 'warning' && row.warnings.includes('overwriting_existing_balance'),
399
+ )
400
+ }, [validation?.rows])
401
+
402
+ const importableRowCount = React.useMemo(() => {
403
+ return applyRows.filter((row) => Math.abs(row.delta) > 0.000001).length
404
+ }, [applyRows])
405
+
406
+ const canApply =
407
+ Boolean(validation?.importBatchId) &&
408
+ importableRowCount > 0
409
+
410
+ const issueRows = React.useMemo(() => {
411
+ return (validation?.rows ?? []).filter(
412
+ (row) => row.status === 'error' || row.status === 'skip' || row.errors.length > 0,
413
+ )
414
+ }, [validation?.rows])
415
+
416
+ const previewRows = React.useMemo(() => {
417
+ return (validation?.rows ?? [])
418
+ .filter((row) => row.resolved && (row.status === 'valid' || row.status === 'warning'))
419
+ .slice(0, 3)
420
+ }, [validation?.rows])
421
+
422
+ const mappedColumnCount = countMappedColumns(columnMappings)
423
+
424
+ const handleApply = React.useCallback(async () => {
425
+ if (!access.scopeReady || !access.organizationId || !access.tenantId) return
426
+ if (!validation?.importBatchId) return
427
+
428
+ const mutationPayload: Record<string, unknown> = {
429
+ organizationId: access.organizationId,
430
+ tenantId: access.tenantId,
431
+ importBatchId: validation.importBatchId,
432
+ rows: applyRows,
433
+ }
434
+
435
+ setApplying(true)
436
+ try {
437
+ const result = await runMutation({
438
+ operation: async () => {
439
+ const call = await apiCall<{
440
+ ok?: boolean
441
+ summary?: { applied?: number; skipped?: number; failed?: number }
442
+ }>(
443
+ '/api/wms/inventory/import/apply',
444
+ {
445
+ method: 'POST',
446
+ headers: { 'content-type': 'application/json' },
447
+ body: JSON.stringify({
448
+ organizationId: access.organizationId,
449
+ tenantId: access.tenantId,
450
+ importBatchId: validation.importBatchId,
451
+ reason: reconcileMode
452
+ ? t('wms.backend.inventory.import.defaultReasonReconcile', 'CSV import opening balance')
453
+ : t('wms.backend.inventory.import.defaultReason', 'CSV import inventory receipt'),
454
+ continueOnError: true,
455
+ mode: reconcileMode ? 'reconcile' : 'additive',
456
+ rows: applyRows,
457
+ }),
458
+ },
459
+ )
460
+ if (!call.ok) {
461
+ await raiseCrudError(
462
+ call.response,
463
+ t('wms.backend.inventory.import.errors.apply', 'Import failed.'),
464
+ )
465
+ }
466
+ return call.result ?? {}
467
+ },
468
+ context: mutationContext,
469
+ mutationPayload,
470
+ })
471
+
472
+ const applied = result?.summary?.applied ?? 0
473
+ const skipped = result?.summary?.skipped ?? 0
474
+ const failed = result?.summary?.failed ?? 0
475
+ if (failed > 0) {
476
+ flash(
477
+ t(
478
+ 'wms.backend.inventory.import.flash.appliedPartial',
479
+ 'Import finished with errors ({applied} applied, {skipped} skipped, {failed} failed).',
480
+ { applied, skipped, failed },
481
+ ),
482
+ 'warning',
483
+ )
484
+ } else {
485
+ flash(
486
+ t('wms.backend.inventory.import.flash.applied', 'Import finished ({applied} applied, {skipped} skipped).', {
487
+ applied,
488
+ skipped,
489
+ }),
490
+ 'success',
491
+ )
492
+ }
493
+ await queryClient.invalidateQueries({ queryKey: ['wms-inventory-console'] })
494
+ await queryClient.invalidateQueries({ queryKey: ['wms-sku-detail'] })
495
+ closeDialog()
496
+ } catch (error) {
497
+ flashMutationError(error, t('wms.backend.inventory.import.errors.apply', 'Import failed.'), t)
498
+ } finally {
499
+ setApplying(false)
500
+ }
501
+ }, [
502
+ access.organizationId,
503
+ access.scopeReady,
504
+ access.tenantId,
505
+ applyRows,
506
+ closeDialog,
507
+ queryClient,
508
+ mutationContext,
509
+ reconcileMode,
510
+ runMutation,
511
+ t,
512
+ validation?.importBatchId,
513
+ ])
514
+
515
+ const handlePrimaryAction = React.useCallback(async () => {
516
+ if (step === 1) {
517
+ if (!selectedFile) {
518
+ flash(t('wms.backend.inventory.import.errors.file', 'Choose a CSV file first.'), 'error')
519
+ return
520
+ }
521
+ setStep(2)
522
+ return
523
+ }
524
+ if (step === 2) {
525
+ const validated = await handleValidate()
526
+ if (validated) setStep(3)
527
+ return
528
+ }
529
+ await handleApply()
530
+ }, [handleApply, handleValidate, selectedFile, step, t])
531
+
532
+ const handleDialogKeyDown = React.useCallback(
533
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
534
+ if (event.key === 'Escape') {
535
+ event.preventDefault()
536
+ closeDialog()
537
+ return
538
+ }
539
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
540
+ event.preventDefault()
541
+ if (validating || applying) return
542
+ if (step === 3 && !canApply) return
543
+ void handlePrimaryAction()
544
+ }
545
+ },
546
+ [applying, canApply, closeDialog, handlePrimaryAction, step, validating],
547
+ )
548
+
549
+ const stepSubtitle = React.useMemo(() => {
550
+ if (step === 1) {
551
+ return t('wms.backend.inventory.import.steps.upload.subtitle', 'Step 1 of 3 · Upload your file')
552
+ }
553
+ if (step === 2) {
554
+ return t(
555
+ 'wms.backend.inventory.import.steps.mapping.subtitle',
556
+ 'Step 2 of 3 · Map CSV columns to fields',
557
+ )
558
+ }
559
+ return t('wms.backend.inventory.import.steps.review.subtitle', 'Step 3 of 3 · Review and import')
560
+ }, [step, t])
561
+
562
+ const shortcutHint = React.useMemo(() => {
563
+ if (step === 3) {
564
+ return t('wms.backend.inventory.import.shortcut.import', 'to import')
565
+ }
566
+ return t('wms.backend.inventory.import.shortcut.continue', 'continue')
567
+ }, [step, t])
568
+
569
+ const primaryDisabled =
570
+ validating ||
571
+ applying ||
572
+ (step === 1 && !selectedFile) ||
573
+ (step === 3 && !canApply)
574
+
575
+ const primaryLabel = React.useMemo(() => {
576
+ if (step === 1 || step === 2) {
577
+ return validating
578
+ ? t('wms.backend.inventory.import.actions.validating', 'Validating…')
579
+ : t('wms.backend.inventory.import.actions.next', 'Next')
580
+ }
581
+ return applying
582
+ ? t('wms.backend.inventory.import.actions.applying', 'Importing…')
583
+ : t('wms.backend.inventory.import.actions.importRows', 'Import {count} rows', {
584
+ count: formatRowCount(importableRowCount),
585
+ })
586
+ }, [applying, importableRowCount, step, t, validating])
587
+
588
+ return (
589
+ <Dialog open={open} onOpenChange={(next) => (next ? onOpenChange(true) : closeDialog())}>
590
+ <DialogContent
591
+ className="max-w-2xl gap-0 overflow-hidden p-0"
592
+ onKeyDown={handleDialogKeyDown}
593
+ >
594
+ <div className="border-b px-6 py-4 pr-12">
595
+ <DialogHeader className="space-y-1 text-left">
596
+ <DialogTitle>{t('wms.backend.inventory.import.dialog.title', 'Import CSV')}</DialogTitle>
597
+ <DialogDescription>{stepSubtitle}</DialogDescription>
598
+ <ImportStepIndicator step={step} />
599
+ </DialogHeader>
600
+ </div>
601
+
602
+ <div className="flex max-h-[min(70vh,640px)] flex-col gap-5 overflow-y-auto px-6 py-6">
603
+ {step === 1 ? (
604
+ <>
605
+ <button
606
+ type="button"
607
+ className={cn(
608
+ 'flex w-full flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-5 py-8 text-center transition-colors',
609
+ isDragging
610
+ ? 'border-border bg-muted/60'
611
+ : selectedFile
612
+ ? 'border-border bg-muted/40 hover:bg-muted/60'
613
+ : 'border-border bg-muted/20 hover:bg-muted/40',
614
+ )}
615
+ onClick={() => fileInputRef.current?.click()}
616
+ onDragOver={handleDragOver}
617
+ onDragLeave={handleDragLeave}
618
+ onDrop={handleDrop}
619
+ >
620
+ {selectedFile ? (
621
+ <>
622
+ <div className="flex items-center gap-2">
623
+ <FileText className="size-5 text-foreground" aria-hidden="true" />
624
+ <p className="text-sm font-semibold text-foreground">{selectedFile.name}</p>
625
+ </div>
626
+ {parsedFile ? (
627
+ <p className="text-xs text-muted-foreground">
628
+ {t(
629
+ 'wms.backend.inventory.import.upload.fileMeta',
630
+ '{rows} rows · {size} · detected',
631
+ {
632
+ rows: formatRowCount(parsedFile.rowCount),
633
+ size: formatFileSize(parsedFile.fileSize),
634
+ },
635
+ )}
636
+ </p>
637
+ ) : null}
638
+ <span className="text-xs font-medium text-foreground">
639
+ {t('wms.backend.inventory.import.actions.chooseDifferent', 'Choose different file')}
640
+ </span>
641
+ </>
642
+ ) : (
643
+ <>
644
+ <FileText className="size-5 text-muted-foreground" aria-hidden="true" />
645
+ <p className="text-sm font-medium text-foreground">
646
+ {t('wms.backend.inventory.import.upload.dropzoneEmpty', 'Choose CSV file or drop here')}
647
+ </p>
648
+ </>
649
+ )}
650
+ </button>
651
+ <Input
652
+ ref={fileInputRef}
653
+ type="file"
654
+ accept=".csv,text/csv"
655
+ className="hidden"
656
+ onChange={(event) => {
657
+ void handleFileSelection(event.target.files?.[0] ?? null)
658
+ }}
659
+ />
660
+
661
+ <div className="grid gap-3 sm:grid-cols-2">
662
+ <div className="space-y-1.5">
663
+ <p className="text-sm font-semibold text-foreground">
664
+ {t('wms.backend.inventory.import.upload.format', 'Format')}
665
+ </p>
666
+ <Select value="csv-comma" disabled>
667
+ <SelectTrigger>
668
+ <SelectValue />
669
+ </SelectTrigger>
670
+ <SelectContent>
671
+ <SelectItem value="csv-comma">
672
+ {t(
673
+ 'wms.backend.inventory.import.upload.formatValue',
674
+ 'CSV · comma-separated',
675
+ )}
676
+ </SelectItem>
677
+ </SelectContent>
678
+ </Select>
679
+ </div>
680
+ <div className="space-y-1.5">
681
+ <p className="text-sm font-semibold text-foreground">
682
+ {t('wms.backend.inventory.import.upload.encoding', 'Encoding')}
683
+ </p>
684
+ <Select value="utf8" disabled>
685
+ <SelectTrigger>
686
+ <SelectValue />
687
+ </SelectTrigger>
688
+ <SelectContent>
689
+ <SelectItem value="utf8">
690
+ {t(
691
+ 'wms.backend.inventory.import.upload.encodingValue',
692
+ 'UTF-8 (auto-detected)',
693
+ )}
694
+ </SelectItem>
695
+ </SelectContent>
696
+ </Select>
697
+ </div>
698
+ </div>
699
+
700
+ <div className="space-y-2">
701
+ <div className="flex justify-start">
702
+ <Button type="button" variant="outline" onClick={() => void handleDownloadTemplate()}>
703
+ {t('wms.backend.inventory.import.actions.template', 'Download template')}
704
+ </Button>
705
+ </div>
706
+ <p className="text-xs text-muted-foreground">
707
+ {t(
708
+ 'wms.backend.inventory.import.upload.skuHint',
709
+ 'The sku column must match an existing catalog product variant in your organization. Warehouse and location codes must already exist in WMS.',
710
+ )}
711
+ </p>
712
+ <p className="text-xs text-muted-foreground">
713
+ {t(
714
+ 'wms.backend.inventory.import.upload.quantityHint',
715
+ 'The quantity column is added to whatever stock already exists at that warehouse/location — it is not a target balance.',
716
+ )}
717
+ </p>
718
+ </div>
719
+
720
+ <div className="flex items-start gap-3 rounded-lg border border-status-warning-border bg-status-warning-bg px-3 py-3">
721
+ <Switch checked={reconcileMode} onCheckedChange={handleReconcileModeChange} />
722
+ <div className="space-y-0.5">
723
+ <p className="text-sm font-semibold text-status-warning-text">
724
+ {t('wms.backend.inventory.import.upload.reconcileMode', 'Reconcile to exact balance')}
725
+ </p>
726
+ <p className="text-xs text-status-warning-text">
727
+ {t(
728
+ 'wms.backend.inventory.import.upload.reconcileModeHint',
729
+ 'Existing stock at each location is overwritten to match the file exactly — including reducing it. Use this for a full stocktake or opening-balance import. Leave off to add quantities to existing stock instead.',
730
+ )}
731
+ </p>
732
+ </div>
733
+ </div>
734
+ </>
735
+ ) : null}
736
+
737
+ {step === 2 ? (
738
+ <>
739
+ <div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 px-3 py-2 text-xs font-medium text-foreground">
740
+ <p>
741
+ {t(
742
+ 'wms.backend.inventory.import.mapping.summary',
743
+ '{mapped} of {total} columns mapped · {rows} rows detected',
744
+ {
745
+ mapped: mappedColumnCount,
746
+ total: columnMappings.length,
747
+ rows: formatRowCount(parsedFile?.rowCount ?? 0),
748
+ },
749
+ )}
750
+ </p>
751
+ <Button
752
+ type="button"
753
+ variant="ghost"
754
+ size="sm"
755
+ className="h-auto px-1 py-0 text-xs font-medium"
756
+ onClick={handleAutoMap}
757
+ >
758
+ {t('wms.backend.inventory.import.actions.autoMap', 'Auto-map')}
759
+ <ArrowRight className="size-3.5" aria-hidden="true" />
760
+ </Button>
761
+ </div>
762
+
763
+ <div className="rounded-lg border bg-muted/30 px-3 py-2">
764
+ <div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)_auto] items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
765
+ <span>{t('wms.backend.inventory.import.mapping.csvColumn', 'CSV column')}</span>
766
+ <span aria-hidden="true" />
767
+ <span>{t('wms.backend.inventory.import.mapping.targetField', 'Target field')}</span>
768
+ <span className="text-right">
769
+ {t('wms.backend.inventory.import.mapping.status', 'Status')}
770
+ </span>
771
+ </div>
772
+ </div>
773
+
774
+ <div className="space-y-2">
775
+ {columnMappings.map((mapping) => (
776
+ <div
777
+ key={mapping.csvColumn}
778
+ className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)_auto] items-center gap-2 rounded-lg border px-3 py-2.5"
779
+ >
780
+ <p className="truncate text-sm font-semibold text-foreground">{mapping.csvColumn}</p>
781
+ <span className="text-sm text-muted-foreground" aria-hidden="true">
782
+
783
+ </span>
784
+ <Select
785
+ value={mapping.targetField ?? 'skip'}
786
+ onValueChange={(value) => handleMappingChange(mapping.csvColumn, value)}
787
+ >
788
+ <SelectTrigger className="h-9">
789
+ <SelectValue />
790
+ </SelectTrigger>
791
+ <SelectContent>
792
+ {TARGET_FIELD_KEYS.map((field) => (
793
+ <SelectItem key={field} value={field}>
794
+ {targetFieldLabel(field)}
795
+ </SelectItem>
796
+ ))}
797
+ <SelectItem value="skip">
798
+ {t('wms.backend.inventory.import.mapping.skipColumn', '— skip column')}
799
+ </SelectItem>
800
+ </SelectContent>
801
+ </Select>
802
+ <div className="flex justify-end">
803
+ <StatusBadge variant={mapping.status === 'mapped' ? 'success' : 'neutral'}>
804
+ {mapping.status === 'mapped'
805
+ ? t('wms.backend.inventory.import.mapping.status.mapped', 'Mapped')
806
+ : t('wms.backend.inventory.import.mapping.status.skipped', 'Skipped')}
807
+ </StatusBadge>
808
+ </div>
809
+ </div>
810
+ ))}
811
+ </div>
812
+
813
+ <div className="flex items-start gap-3 rounded-lg border px-3 py-3">
814
+ <Switch checked={skipDuplicates} onCheckedChange={setSkipDuplicates} />
815
+ <div className="space-y-0.5">
816
+ <p className="text-sm font-semibold text-foreground">
817
+ {t('wms.backend.inventory.import.review.skipDuplicates', 'Skip duplicates')}
818
+ </p>
819
+ <p className="text-xs text-muted-foreground">
820
+ {t(
821
+ 'wms.backend.inventory.import.review.skipDuplicatesHint',
822
+ 'Duplicate rows in the CSV file will be skipped instead of failing validation',
823
+ )}
824
+ </p>
825
+ </div>
826
+ </div>
827
+ </>
828
+ ) : null}
829
+
830
+ {step === 3 && validation ? (
831
+ <>
832
+ <div className="grid gap-3 sm:grid-cols-3">
833
+ <div className="rounded-lg border px-3.5 py-3">
834
+ <p className="text-2xl font-bold tabular-nums text-foreground">
835
+ {formatRowCount(importableRowCount)}
836
+ </p>
837
+ <p className="text-xs text-muted-foreground">
838
+ {t('wms.backend.inventory.import.review.stats.rows', 'Rows to import')}
839
+ </p>
840
+ </div>
841
+ <div className="rounded-lg border px-3.5 py-3">
842
+ <p className="text-2xl font-bold tabular-nums text-status-success-icon">
843
+ {mappedColumnCount}
844
+ </p>
845
+ <p className="text-xs text-muted-foreground">
846
+ {t('wms.backend.inventory.import.review.stats.columns', 'Columns mapped')}
847
+ </p>
848
+ </div>
849
+ <div className="rounded-lg border px-3.5 py-3">
850
+ <p className="text-2xl font-bold tabular-nums text-status-warning-icon">
851
+ {issueRows.length}
852
+ </p>
853
+ <p className="text-xs text-muted-foreground">
854
+ {t('wms.backend.inventory.import.review.stats.issues', 'Validation issues')}
855
+ </p>
856
+ </div>
857
+ </div>
858
+
859
+ {issueRows.length > 0 ? (
860
+ <div className="rounded-lg border border-status-warning-border bg-status-warning-bg px-4 py-3.5">
861
+ <div className="flex items-start justify-between gap-3">
862
+ <div className="space-y-1">
863
+ <p className="text-sm font-semibold text-status-warning-text">
864
+ {t(
865
+ 'wms.backend.inventory.import.review.banner.title',
866
+ '{count} rows will be skipped on import',
867
+ { count: issueRows.length },
868
+ )}
869
+ </p>
870
+ <div className="space-y-0.5 text-xs text-muted-foreground">
871
+ {(showIssueDetails ? issueRows : issueRows.slice(0, 2)).map((row) => (
872
+ <p key={row.rowNumber}>
873
+ {t(
874
+ 'wms.backend.inventory.import.review.banner.rowIssue',
875
+ 'Row {row}: {issue}',
876
+ {
877
+ row: row.rowNumber,
878
+ issue: [...row.errors, ...row.warnings].map((code) => formatImportIssue(code, t)).join(', ') || row.status,
879
+ },
880
+ )}
881
+ </p>
882
+ ))}
883
+ </div>
884
+ </div>
885
+ {issueRows.length > 2 ? (
886
+ <Button
887
+ type="button"
888
+ variant="ghost"
889
+ size="sm"
890
+ className="h-auto shrink-0 px-1 py-0 text-xs font-medium text-status-warning-text hover:text-status-warning-text"
891
+ onClick={() => setShowIssueDetails((current) => !current)}
892
+ >
893
+ {showIssueDetails
894
+ ? t('wms.backend.inventory.import.actions.hideDetails', 'Hide details')
895
+ : t('wms.backend.inventory.import.actions.viewDetails', 'View details →')}
896
+ </Button>
897
+ ) : null}
898
+ </div>
899
+ </div>
900
+ ) : null}
901
+
902
+ {overwriteWarningRows.length > 0 ? (
903
+ <div className="rounded-lg border border-status-warning-border bg-status-warning-bg px-4 py-3.5">
904
+ <div className="space-y-1">
905
+ <p className="text-sm font-semibold text-status-warning-text">
906
+ {t(
907
+ 'wms.backend.inventory.import.review.overwriteBanner.title',
908
+ '{count} rows will change existing stock levels',
909
+ { count: overwriteWarningRows.length },
910
+ )}
911
+ </p>
912
+ <div className="space-y-0.5 text-xs text-muted-foreground">
913
+ {overwriteWarningRows.map((row) => {
914
+ const current = row.resolved?.currentOnHand ?? 0
915
+ const quantity = row.resolved?.quantity ?? 0
916
+ const delta = row.resolved?.delta ?? 0
917
+ const signedDelta = delta > 0 ? `+${delta}` : `${delta}`
918
+ return (
919
+ <p key={row.rowNumber}>
920
+ {t(
921
+ 'wms.backend.inventory.import.review.overwriteBanner.rowDetail',
922
+ 'Row {row}: {location} — {current} → {quantity} ({delta})',
923
+ {
924
+ row: row.rowNumber,
925
+ location: row.resolved?.locationCode ?? '—',
926
+ current,
927
+ quantity,
928
+ delta: signedDelta,
929
+ },
930
+ )}
931
+ </p>
932
+ )
933
+ })}
934
+ </div>
935
+ </div>
936
+ </div>
937
+ ) : null}
938
+
939
+ <div className="space-y-2">
940
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
941
+ {t('wms.backend.inventory.import.review.preview.title', 'Preview · first 3 rows')}
942
+ </p>
943
+ <div className="overflow-hidden rounded-lg border">
944
+ <div className="grid grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)_minmax(0,1fr)_auto] gap-3 border-b bg-muted/40 px-3.5 py-2.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
945
+ <span>{t('wms.backend.inventory.import.columns.sku', 'SKU')}</span>
946
+ <span>{t('wms.backend.inventory.import.columns.location', 'Location')}</span>
947
+ <span>{t('wms.backend.inventory.import.review.preview.lot', 'Lot')}</span>
948
+ <span className="text-right">
949
+ {t('wms.backend.inventory.import.review.preview.qty', 'Qty')}
950
+ </span>
951
+ </div>
952
+ {previewRows.length > 0 ? (
953
+ previewRows.map((row) => (
954
+ <div
955
+ key={row.rowNumber}
956
+ className="grid grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)_minmax(0,1fr)_auto] gap-3 border-b px-3.5 py-2.5 text-xs last:border-b-0"
957
+ >
958
+ <span className="truncate font-medium text-foreground">
959
+ {row.resolved?.sku ?? '—'}
960
+ </span>
961
+ <span className="truncate text-foreground">
962
+ {row.resolved?.locationCode ?? '—'}
963
+ </span>
964
+ <span className="truncate text-muted-foreground">
965
+ {row.resolved?.serialNumber ?? row.resolved?.lotId ?? '—'}
966
+ </span>
967
+ <span className="text-right font-semibold tabular-nums text-foreground">
968
+ {row.resolved?.quantity ?? '—'}
969
+ </span>
970
+ </div>
971
+ ))
972
+ ) : (
973
+ <p className="px-3.5 py-4 text-sm text-muted-foreground">
974
+ {t('wms.backend.inventory.import.review.preview.empty', 'No preview rows available.')}
975
+ </p>
976
+ )}
977
+ </div>
978
+ </div>
979
+ </>
980
+ ) : null}
981
+ </div>
982
+
983
+ <DialogFooter bordered={false} className="border-t px-6 py-4 sm:justify-between">
984
+ <p className="hidden text-xs text-muted-foreground sm:inline-flex sm:items-center sm:gap-1.5">
985
+ <KbdShortcut keys={['⌘', 'Enter']} />
986
+ <span>/</span>
987
+ <KbdShortcut keys={['Ctrl', 'Enter']} />
988
+ <span>{shortcutHint}</span>
989
+ </p>
990
+ <div className="flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row">
991
+ {step === 1 ? (
992
+ <Button type="button" variant="outline" onClick={closeDialog} disabled={validating || applying}>
993
+ {t('wms.backend.inventory.import.actions.cancel', 'Cancel')}
994
+ </Button>
995
+ ) : (
996
+ <Button
997
+ type="button"
998
+ variant="outline"
999
+ onClick={() => setStep((current) => (current === 3 ? 2 : 1))}
1000
+ disabled={validating || applying}
1001
+ >
1002
+ {t('wms.backend.inventory.import.actions.back', 'Back')}
1003
+ </Button>
1004
+ )}
1005
+ <Button
1006
+ type="button"
1007
+ disabled={primaryDisabled}
1008
+ onClick={() => void handlePrimaryAction()}
1009
+ >
1010
+ {primaryLabel}
1011
+ {step !== 3 ? <ArrowRight className="size-4" aria-hidden="true" /> : null}
1012
+ </Button>
1013
+ </div>
1014
+ </DialogFooter>
1015
+ </DialogContent>
1016
+ </Dialog>
1017
+ )
1018
+ }