@revenexx/cover 0.1.0

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 (408) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/app/api/account.ts +8 -0
  4. package/app/api/categories.ts +3 -0
  5. package/app/api/checkout.ts +6 -0
  6. package/app/api/images.ts +4 -0
  7. package/app/api/markets.ts +3 -0
  8. package/app/api/product.ts +3 -0
  9. package/app/api/products.ts +4 -0
  10. package/app/api/search.ts +4 -0
  11. package/app/app.config.ts +29 -0
  12. package/app/assets/css/cover.css +25 -0
  13. package/app/components/account/AccountMenu.vue +30 -0
  14. package/app/components/account/AccountShell.vue +18 -0
  15. package/app/components/account/AccountSidebar.vue +90 -0
  16. package/app/components/account/address/AccountAddressCard.vue +287 -0
  17. package/app/components/account/carts/AccountCartsTable.vue +284 -0
  18. package/app/components/account/dashboard/AccountDashboard.vue +351 -0
  19. package/app/components/account/directorder/AccountDirectOrder.vue +512 -0
  20. package/app/components/account/governance/AccountApprovalLimitsTable.vue +221 -0
  21. package/app/components/account/governance/AccountCostCenterDetail.vue +276 -0
  22. package/app/components/account/governance/AccountCostCentersTable.vue +252 -0
  23. package/app/components/account/governance/AccountRequisitionDetail.vue +295 -0
  24. package/app/components/account/governance/AccountRequisitionsTable.vue +255 -0
  25. package/app/components/account/governance/AccountWorkflowDetail.vue +215 -0
  26. package/app/components/account/governance/AccountWorkflowsTable.vue +168 -0
  27. package/app/components/account/governance/GovernanceApprovalLimitModal.vue +183 -0
  28. package/app/components/account/governance/GovernanceCostCenterModal.vue +188 -0
  29. package/app/components/account/governance/GovernanceWorkflowModal.vue +191 -0
  30. package/app/components/account/orderlists/AccountOrderListDetail.vue +349 -0
  31. package/app/components/account/orderlists/AccountOrderListsTable.vue +352 -0
  32. package/app/components/account/orders/AccountOrderDetail.vue +376 -0
  33. package/app/components/account/orders/AccountOrdersTable.vue +281 -0
  34. package/app/components/account/preferences/AccountPreferences.vue +50 -0
  35. package/app/components/auth/AuthForgotPasswordPanel.vue +88 -0
  36. package/app/components/auth/AuthLoginPanel.vue +103 -0
  37. package/app/components/auth/AuthLoginTeaser.vue +24 -0
  38. package/app/components/auth/AuthPageHeader.vue +21 -0
  39. package/app/components/auth/AuthRegisterPanel.vue +431 -0
  40. package/app/components/auth/AuthRegisterTeaser.vue +24 -0
  41. package/app/components/auth/AuthResetPasswordPanel.vue +115 -0
  42. package/app/components/cart/CartButton.vue +42 -0
  43. package/app/components/cart/actions/CartCostCenterModal.vue +110 -0
  44. package/app/components/cart/actions/CartExportModal.vue +82 -0
  45. package/app/components/cart/actions/CartPositionTextsModal.vue +92 -0
  46. package/app/components/cart/actions/CartSaveToListModal.vue +177 -0
  47. package/app/components/cart/actions/CartWorkflowPickerModal.vue +64 -0
  48. package/app/components/cart/drawer/CartDrawer.vue +49 -0
  49. package/app/components/cart/item/CartItem.vue +318 -0
  50. package/app/components/cart/manage/CartNameModal.vue +68 -0
  51. package/app/components/cart/position/CartPosition.vue +369 -0
  52. package/app/components/cart/quickadd/CartQuickAdd.vue +145 -0
  53. package/app/components/cart/recommend/CartCrossSell.vue +102 -0
  54. package/app/components/cart/recommend/CartRecommendCard.vue +59 -0
  55. package/app/components/cart/recommend/CartReorderStrip.vue +108 -0
  56. package/app/components/cart/requisition/CartRequisitionGroup.vue +74 -0
  57. package/app/components/cart/skeleton/CartSkeleton.vue +18 -0
  58. package/app/components/cart/summary/CartSummary.vue +25 -0
  59. package/app/components/cart/summary/CartSummaryBreakdown.vue +35 -0
  60. package/app/components/cart/switcher/CartSwitcher.vue +147 -0
  61. package/app/components/cart/target/CartTargetModal.vue +129 -0
  62. package/app/components/cart/view/CartApprovalsPanel.vue +89 -0
  63. package/app/components/cart/view/CartListHeader.vue +86 -0
  64. package/app/components/cart/view/CartPositionList.vue +94 -0
  65. package/app/components/cart/view/CartSummaryPanel.vue +254 -0
  66. package/app/components/cart/view/CartView.vue +310 -0
  67. package/app/components/category/info/CategoryPrice.vue +47 -0
  68. package/app/components/category/info/CategorySku.vue +15 -0
  69. package/app/components/category/info/CategoryStock.vue +25 -0
  70. package/app/components/category/list/CategoryItemCount.vue +27 -0
  71. package/app/components/category/list/CategoryListPagination.vue +81 -0
  72. package/app/components/category/skeleton/CategoryDetailSkeleton.vue +12 -0
  73. package/app/components/category/skeleton/CategoryListSkeleton.vue +65 -0
  74. package/app/components/category/view/CategoryViewToggle.vue +33 -0
  75. package/app/components/category/view/GridView.vue +78 -0
  76. package/app/components/category/view/ListView.vue +80 -0
  77. package/app/components/checkout/confirmation/CheckoutConfirmationView.vue +289 -0
  78. package/app/components/checkout/form/CheckoutSchemaForm.vue +72 -0
  79. package/app/components/checkout/onepage/CheckoutAddressPickerModal.vue +260 -0
  80. package/app/components/checkout/onepage/CheckoutAddressSection.vue +186 -0
  81. package/app/components/checkout/onepage/CheckoutDeliverySection.vue +158 -0
  82. package/app/components/checkout/onepage/CheckoutGate.vue +67 -0
  83. package/app/components/checkout/onepage/CheckoutGuestDetailsSection.vue +154 -0
  84. package/app/components/checkout/onepage/CheckoutNotesSection.vue +62 -0
  85. package/app/components/checkout/onepage/CheckoutOnePageView.vue +77 -0
  86. package/app/components/checkout/onepage/CheckoutPaymentSection.vue +181 -0
  87. package/app/components/checkout/onepage/CheckoutSectionTitle.vue +18 -0
  88. package/app/components/checkout/onepage/CheckoutSummarySection.vue +211 -0
  89. package/app/components/checkout/onepage/CheckoutWorkflowPanel.vue +38 -0
  90. package/app/components/layout/AppLogo.vue +21 -0
  91. package/app/components/layout/footer/Footer.vue +148 -0
  92. package/app/components/layout/header/AppBar.vue +75 -0
  93. package/app/components/layout/header/CategoryNav.vue +93 -0
  94. package/app/components/layout/header/HeaderActionItem.vue +81 -0
  95. package/app/components/layout/header/LocaleSwitcher.vue +123 -0
  96. package/app/components/layout/header/MainNav.vue +61 -0
  97. package/app/components/layout/header/MobileMenu.vue +81 -0
  98. package/app/components/layout/header/TopBar.vue +63 -0
  99. package/app/components/listing/ListingView.vue +227 -0
  100. package/app/components/product/detail/ProductDetailAddToCart.vue +188 -0
  101. package/app/components/product/detail/ProductDetailDescription.vue +20 -0
  102. package/app/components/product/detail/ProductDetailImageGallery.vue +58 -0
  103. package/app/components/product/detail/ProductDetailPricing.vue +108 -0
  104. package/app/components/product/detail/ProductDetailSaveToList.vue +291 -0
  105. package/app/components/product/detail/ProductDetailSpecifications.vue +33 -0
  106. package/app/components/product/detail/ProductDetailStock.vue +36 -0
  107. package/app/components/search/filter/SearchFacets.vue +256 -0
  108. package/app/components/search/filter/SearchFacetsDrawer.vue +58 -0
  109. package/app/components/ui/AppShell.vue +7 -0
  110. package/app/components/ui/debug/PiniaStateCard.vue +106 -0
  111. package/app/components/ui/feedback/EmptyState.vue +30 -0
  112. package/app/components/ui/feedback/ErrorBoundary.vue +44 -0
  113. package/app/components/ui/feedback/NotFound.vue +34 -0
  114. package/app/components/ui/forms/CountrySelection.vue +14 -0
  115. package/app/components/ui/forms/FormKitDatePicker.vue +124 -0
  116. package/app/components/ui/forms/PasswordInput.vue +136 -0
  117. package/app/components/ui/forms/SearchInput.vue +213 -0
  118. package/app/components/ui/table/TableSortButton.vue +51 -0
  119. package/app/components/ui/table/TableToolbar.vue +66 -0
  120. package/app/composables/useAccount.ts +27 -0
  121. package/app/composables/useAccountAddresses.ts +116 -0
  122. package/app/composables/useAccountOrders.ts +18 -0
  123. package/app/composables/useAppUrl.ts +38 -0
  124. package/app/composables/useAuthStore.ts +202 -0
  125. package/app/composables/useB2BContext.ts +23 -0
  126. package/app/composables/useCartActions.ts +25 -0
  127. package/app/composables/useCartCalculation.ts +90 -0
  128. package/app/composables/useCartSelection.ts +46 -0
  129. package/app/composables/useCartStore.ts +837 -0
  130. package/app/composables/useCartSummaryFormatting.ts +28 -0
  131. package/app/composables/useCategories.ts +29 -0
  132. package/app/composables/useCheckoutOnePage.ts +438 -0
  133. package/app/composables/useCheckoutProfileSource.ts +8 -0
  134. package/app/composables/useCheckoutSchema.ts +28 -0
  135. package/app/composables/useDataTable.ts +14 -0
  136. package/app/composables/useFormValidation.ts +29 -0
  137. package/app/composables/useIcons.ts +17 -0
  138. package/app/composables/useLocalePreferences.ts +45 -0
  139. package/app/composables/useMarkets.ts +15 -0
  140. package/app/composables/useProduct.ts +89 -0
  141. package/app/composables/useProductCard.ts +8 -0
  142. package/app/composables/useProductCardActions.ts +106 -0
  143. package/app/composables/useProductImage.ts +12 -0
  144. package/app/composables/useProductListing.ts +210 -0
  145. package/app/composables/useProductUrl.ts +17 -0
  146. package/app/composables/useProducts.ts +130 -0
  147. package/app/composables/useSearch.ts +12 -0
  148. package/app/composables/useThemeStore.ts +81 -0
  149. package/app/composables/useViewMode.ts +18 -0
  150. package/app/config/countries.ts +74 -0
  151. package/app/config/icons.ts +283 -0
  152. package/app/config/navigation.ts +191 -0
  153. package/app/config/palette.ts +13 -0
  154. package/app/config/themes.ts +20 -0
  155. package/app/formkit.config.ts +50 -0
  156. package/app/interfaces/account/address-list.ts +34 -0
  157. package/app/interfaces/account/order-list.ts +47 -0
  158. package/app/interfaces/account/order-lists.ts +35 -0
  159. package/app/interfaces/account/profile.ts +17 -0
  160. package/app/interfaces/account.ts +3 -0
  161. package/app/interfaces/address.ts +9 -0
  162. package/app/interfaces/auth.ts +104 -0
  163. package/app/interfaces/b2b.ts +234 -0
  164. package/app/interfaces/cart-calculation.ts +131 -0
  165. package/app/interfaces/cart-item.ts +45 -0
  166. package/app/interfaces/checkout-draft.ts +4 -0
  167. package/app/interfaces/checkout.ts +43 -0
  168. package/app/interfaces/delivery.ts +13 -0
  169. package/app/interfaces/market.ts +24 -0
  170. package/app/interfaces/payment.ts +19 -0
  171. package/app/interfaces/persisted-cart.ts +10 -0
  172. package/app/interfaces/product-detail.ts +54 -0
  173. package/app/interfaces/product-list.ts +33 -0
  174. package/app/interfaces/search-facets.ts +14 -0
  175. package/app/interfaces/validation.ts +14 -0
  176. package/app/layouts/default.vue +78 -0
  177. package/app/layouts/focus.vue +80 -0
  178. package/app/plugins/formkit-locale.client.ts +23 -0
  179. package/app/shared/constants.ts +1 -0
  180. package/app/validations/companyName.ts +18 -0
  181. package/app/validations/emailFormat.ts +10 -0
  182. package/app/validations/formValidationConfig.ts +50 -0
  183. package/app/validations/maxLength.ts +12 -0
  184. package/app/validations/minLength.ts +9 -0
  185. package/app/validations/optionalCompanyName.ts +23 -0
  186. package/app/validations/passwordsMatch.ts +5 -0
  187. package/app/validations/phoneNumber.ts +19 -0
  188. package/app/validations/required.ts +11 -0
  189. package/app/validations/termsRequired.ts +4 -0
  190. package/app/validations/types.ts +10 -0
  191. package/app/validations/zipCode.ts +15 -0
  192. package/i18n/locales/de/account.json +458 -0
  193. package/i18n/locales/de/auth.json +155 -0
  194. package/i18n/locales/de/cart.json +263 -0
  195. package/i18n/locales/de/checkout.json +217 -0
  196. package/i18n/locales/de/common.json +80 -0
  197. package/i18n/locales/de/cover.json +33 -0
  198. package/i18n/locales/de/order.json +1 -0
  199. package/i18n/locales/de/product.json +71 -0
  200. package/i18n/locales/de/search.json +54 -0
  201. package/i18n/locales/de/validation.json +12 -0
  202. package/i18n/locales/en/account.json +458 -0
  203. package/i18n/locales/en/auth.json +155 -0
  204. package/i18n/locales/en/cart.json +263 -0
  205. package/i18n/locales/en/checkout.json +217 -0
  206. package/i18n/locales/en/common.json +80 -0
  207. package/i18n/locales/en/cover.json +33 -0
  208. package/i18n/locales/en/order.json +1 -0
  209. package/i18n/locales/en/product.json +71 -0
  210. package/i18n/locales/en/search.json +54 -0
  211. package/i18n/locales/en/validation.json +12 -0
  212. package/nuxt.config.ts +109 -0
  213. package/package.json +65 -0
  214. package/public/img/product-placeholder.svg +8 -0
  215. package/public/templates/direct-order-template.csv +3 -0
  216. package/public/templates/direct-order-template.xlsx +0 -0
  217. package/server/api/account/address/[id].delete.ts +51 -0
  218. package/server/api/account/address/[id].put.ts +83 -0
  219. package/server/api/account/address/index.post.ts +60 -0
  220. package/server/api/account/addresses.get.ts +25 -0
  221. package/server/api/account/context.get.ts +16 -0
  222. package/server/api/account/governance/[domain]/[id].put.ts +57 -0
  223. package/server/api/account/governance/[domain].post.ts +111 -0
  224. package/server/api/account/order-lists/[id].delete.ts +22 -0
  225. package/server/api/account/order-lists/[id].get.ts +17 -0
  226. package/server/api/account/order-lists/[id].put.ts +46 -0
  227. package/server/api/account/order-lists/index.get.ts +14 -0
  228. package/server/api/account/order-lists/index.post.ts +38 -0
  229. package/server/api/account/orders/[id].get.ts +35 -0
  230. package/server/api/account/orders.get.ts +35 -0
  231. package/server/api/account/profile.get.ts +56 -0
  232. package/server/api/account/profile.put.ts +128 -0
  233. package/server/api/account/requisitions/[id].get.ts +15 -0
  234. package/server/api/account/requisitions.get.ts +23 -0
  235. package/server/api/auth/login.post.ts +37 -0
  236. package/server/api/auth/logout.post.ts +4 -0
  237. package/server/api/auth/me.get.ts +3 -0
  238. package/server/api/auth/personas.get.ts +7 -0
  239. package/server/api/auth/recovery.post.ts +32 -0
  240. package/server/api/auth/recovery.put.ts +37 -0
  241. package/server/api/auth/register.post.ts +126 -0
  242. package/server/api/cart/calculate.post.ts +25 -0
  243. package/server/api/cart/export.post.ts +81 -0
  244. package/server/api/cart/index.delete.ts +10 -0
  245. package/server/api/cart/index.get.ts +10 -0
  246. package/server/api/cart/sync.post.ts +14 -0
  247. package/server/api/carts/[id]/activate.post.ts +18 -0
  248. package/server/api/carts/[id]/index.delete.ts +18 -0
  249. package/server/api/carts/[id]/index.put.ts +19 -0
  250. package/server/api/carts/[id]/items.post.ts +21 -0
  251. package/server/api/carts/index.get.ts +14 -0
  252. package/server/api/carts/index.post.ts +14 -0
  253. package/server/api/categories/[slug].get.ts +22 -0
  254. package/server/api/categories/index.get.ts +11 -0
  255. package/server/api/checkout/profile.get.ts +21 -0
  256. package/server/api/checkout/schema/[key].get.ts +19 -0
  257. package/server/api/checkout/session.post.ts +8 -0
  258. package/server/api/images/detail/[filename].get.ts +18 -0
  259. package/server/api/images/list/[filename].get.ts +17 -0
  260. package/server/api/markets.get.ts +9 -0
  261. package/server/api/orders/index.post.ts +376 -0
  262. package/server/api/payment/methods.post.ts +65 -0
  263. package/server/api/product/[id].get.ts +29 -0
  264. package/server/api/products/[id].get.ts +30 -0
  265. package/server/api/products/index.get.ts +21 -0
  266. package/server/api/requisitions/index.post.ts +67 -0
  267. package/server/api/shipping/rates.post.ts +70 -0
  268. package/server/api/themes/index.get.ts +9 -0
  269. package/server/api/typesense/drop.get.ts +22 -0
  270. package/server/api/typesense/health.get.ts +9 -0
  271. package/server/api/typesense/search.post.ts +199 -0
  272. package/server/api/typesense/seed.get.ts +112 -0
  273. package/server/api/typesense/suggest.post.ts +69 -0
  274. package/server/config/account/organization.json +146 -0
  275. package/server/config/account/personas.json +169 -0
  276. package/server/config/account/user.json +8 -0
  277. package/server/config/account/workflows.json +19 -0
  278. package/server/data/account/address-list.json +103 -0
  279. package/server/data/account/order-list.json +491 -0
  280. package/server/data/account/order-lists.json +149 -0
  281. package/server/data/account/profile.json +9 -0
  282. package/server/data/account/registration-requests.json +3 -0
  283. package/server/data/account/requisitions.json +686 -0
  284. package/server/data/categories.json +186 -0
  285. package/server/data/forms/checkout/add-address.json +24 -0
  286. package/server/data/list/5137-1.png +0 -0
  287. package/server/data/list/5498-1.png +0 -0
  288. package/server/data/list/5498-2.png +0 -0
  289. package/server/data/list/5498-3.png +0 -0
  290. package/server/data/list/5498-4.png +0 -0
  291. package/server/data/list/5498-5.png +0 -0
  292. package/server/data/list/5498-6.png +0 -0
  293. package/server/data/list/5519-1.png +0 -0
  294. package/server/data/list/5713-1.png +0 -0
  295. package/server/data/list/5789-1.png +0 -0
  296. package/server/data/list/5930-1.png +0 -0
  297. package/server/data/list/6127-1.png +0 -0
  298. package/server/data/list/6234-1.png +0 -0
  299. package/server/data/list/6238-1.png +0 -0
  300. package/server/data/list/6246-1.png +0 -0
  301. package/server/data/list/6270-1.png +0 -0
  302. package/server/data/list/6330-1.png +0 -0
  303. package/server/data/list/6336-1.png +0 -0
  304. package/server/data/list/6360-1.png +0 -0
  305. package/server/data/list/6363-1.png +0 -0
  306. package/server/data/list/6375-1.png +0 -0
  307. package/server/data/list/6385-1.png +0 -0
  308. package/server/data/list/6413-1.png +0 -0
  309. package/server/data/list/6418-1.png +0 -0
  310. package/server/data/list/6465-1.png +0 -0
  311. package/server/data/list/6477-1.png +0 -0
  312. package/server/data/list/6509-1.png +0 -0
  313. package/server/data/list/6545-1.png +0 -0
  314. package/server/data/list/6548-1.png +0 -0
  315. package/server/data/list/6566-1.png +0 -0
  316. package/server/data/list/6581-1.png +0 -0
  317. package/server/data/list/6609-1.png +0 -0
  318. package/server/data/list/6611-1.png +0 -0
  319. package/server/data/list/6641-1.png +0 -0
  320. package/server/data/list/6659-1.png +0 -0
  321. package/server/data/list/6662-1.png +0 -0
  322. package/server/data/list/6689-1.png +0 -0
  323. package/server/data/list/6698-1.png +0 -0
  324. package/server/data/list/6701-1.png +0 -0
  325. package/server/data/list/6752-1.png +0 -0
  326. package/server/data/list/6755-1.png +0 -0
  327. package/server/data/list/6837-1.png +0 -0
  328. package/server/data/list/6841-1.png +0 -0
  329. package/server/data/list/6844-1.png +0 -0
  330. package/server/data/list/6846-1.png +0 -0
  331. package/server/data/list/6886-1.png +0 -0
  332. package/server/data/list/6895-1.png +0 -0
  333. package/server/data/list/6897-1.png +0 -0
  334. package/server/data/list/6919-1.png +0 -0
  335. package/server/data/list/6977-1.png +0 -0
  336. package/server/data/list/6983-1.png +0 -0
  337. package/server/data/list/6984-1.png +0 -0
  338. package/server/data/list/6985-1.png +0 -0
  339. package/server/data/list/6986-1.png +0 -0
  340. package/server/data/list/6989-1.png +0 -0
  341. package/server/data/list/6995-1.png +0 -0
  342. package/server/data/list/6998-1.png +0 -0
  343. package/server/data/markets.json +24 -0
  344. package/server/data/product-detail.json +2450 -0
  345. package/server/data/product-list.json +2450 -0
  346. package/server/data/themes.json +8 -0
  347. package/server/interfaces/account.ts +20 -0
  348. package/server/interfaces/auth.ts +32 -0
  349. package/server/interfaces/b2bContext.ts +23 -0
  350. package/server/interfaces/cart.ts +46 -0
  351. package/server/interfaces/cartCalculation.ts +17 -0
  352. package/server/interfaces/category.ts +12 -0
  353. package/server/interfaces/checkoutProfile.ts +5 -0
  354. package/server/interfaces/log.ts +3 -0
  355. package/server/interfaces/market.ts +10 -0
  356. package/server/interfaces/product.ts +21 -0
  357. package/server/interfaces/schema.ts +20 -0
  358. package/server/interfaces/theme.ts +10 -0
  359. package/server/services/ApiAuthService.ts +138 -0
  360. package/server/services/ApiCartService.ts +254 -0
  361. package/server/services/ApiCategoryService.ts +25 -0
  362. package/server/services/ApiMarketService.ts +71 -0
  363. package/server/services/ApiProductService.ts +53 -0
  364. package/server/services/LocalFileCategoryService.ts +23 -0
  365. package/server/services/LocalFileCheckoutProfileService.ts +117 -0
  366. package/server/services/LocalFileProductService.ts +128 -0
  367. package/server/services/LocalFileSchemaService.ts +70 -0
  368. package/server/services/LocalFileThemeService.ts +18 -0
  369. package/server/services/LogService.ts +28 -0
  370. package/server/services/MockAccountService.ts +58 -0
  371. package/server/services/MockAuthService.ts +105 -0
  372. package/server/services/MockB2BContextService.ts +149 -0
  373. package/server/services/MockCartCalculationService.ts +395 -0
  374. package/server/services/MockMarketService.ts +18 -0
  375. package/server/services/SdkAccountService.ts +56 -0
  376. package/server/services/SdkAuthService.ts +83 -0
  377. package/server/services/SessionCartService.ts +31 -0
  378. package/server/utils/accountService.ts +30 -0
  379. package/server/utils/authCookie.ts +13 -0
  380. package/server/utils/authService.ts +43 -0
  381. package/server/utils/b2bService.ts +49 -0
  382. package/server/utils/cartService.ts +59 -0
  383. package/server/utils/categoryService.ts +19 -0
  384. package/server/utils/checkoutProfileService.ts +9 -0
  385. package/server/utils/checkoutSession.ts +38 -0
  386. package/server/utils/coverData.ts +84 -0
  387. package/server/utils/governanceStore.ts +38 -0
  388. package/server/utils/i18n.ts +76 -0
  389. package/server/utils/inventoryService.ts +14 -0
  390. package/server/utils/liveCatalog.ts +234 -0
  391. package/server/utils/liveInventories.ts +76 -0
  392. package/server/utils/liveOrders.ts +139 -0
  393. package/server/utils/livePrices.ts +93 -0
  394. package/server/utils/locale.ts +39 -0
  395. package/server/utils/logService.ts +19 -0
  396. package/server/utils/marketService.ts +24 -0
  397. package/server/utils/orderService.ts +14 -0
  398. package/server/utils/paymentService.ts +14 -0
  399. package/server/utils/priceService.ts +14 -0
  400. package/server/utils/productService.ts +28 -0
  401. package/server/utils/productsSchema.ts +30 -0
  402. package/server/utils/revenexxApi.ts +136 -0
  403. package/server/utils/schemaService.ts +25 -0
  404. package/server/utils/serviceMode.ts +70 -0
  405. package/server/utils/shippingService.ts +14 -0
  406. package/server/utils/shopSdk.ts +88 -0
  407. package/server/utils/themeService.ts +16 -0
  408. package/server/utils/typesense.ts +27 -0
@@ -0,0 +1,837 @@
1
+ import { defineStore, skipHydrate } from "pinia";
2
+
3
+ import { getPriceWithOptionalTax } from "../composables/useProducts";
4
+ import type { Requisition } from "../interfaces/b2b";
5
+ import type { CartItem } from "../interfaces/cart-item";
6
+ import type { CartRequisitionMeta, PersistedCart } from "../interfaces/persisted-cart";
7
+
8
+ /** A stashed (non-active) named cart — the multiple-carts feature. */
9
+ export interface NamedCart {
10
+ id: string;
11
+ name: string;
12
+ items: CartItem[];
13
+ requisitions: CartRequisitionMeta[];
14
+ updatedAt: string;
15
+ }
16
+
17
+ /** Summary row for cart listings (switcher, account page). */
18
+ export interface CartSummaryEntry {
19
+ id: string;
20
+ name: string;
21
+ itemCount: number;
22
+ positions: number;
23
+ value: number;
24
+ updatedAt: string;
25
+ active: boolean;
26
+ }
27
+
28
+ const MULTI_CART_STORAGE = "cover-carts";
29
+ const ADD_PREF_STORAGE = "cover-cart-add-pref";
30
+
31
+ export const useCartStore = defineStore("cart", () => {
32
+ const { public: { cartInactivityTtl, taxIncludedPrices } } = useRuntimeConfig();
33
+ const items = skipHydrate(ref<CartItem[]>([]));
34
+ /** Meta of requisitions currently loaded into the cart. */
35
+ const requisitions = skipHydrate(ref<CartRequisitionMeta[]>([]));
36
+ const isOpen = ref(false);
37
+
38
+ /* ---- live carts (carts app via the BFF) ----------------------------- */
39
+ // The dev-mode cookie is the demo/live switch — readable client-side by
40
+ // design. In live mode the carts app is the source of truth: the active
41
+ // cart syncs through /api/cart, the multi-cart list through /api/carts.
42
+ const devModeCookie = useCookie<string | null>("cover-dev-mode", { default: () => null, watch: true });
43
+ const liveCarts = computed(() => {
44
+ const raw = devModeCookie.value;
45
+ if (!raw) {
46
+ return false;
47
+ }
48
+ try {
49
+ const parsed = (typeof raw === "string" ? JSON.parse(raw) : raw) as { mode?: string; overrides?: Record<string, string> };
50
+ const override = parsed.overrides?.cartService;
51
+ if (override) {
52
+ return override === "api";
53
+ }
54
+ return parsed.mode === "live";
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ });
60
+ const SERVER_CART_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
61
+
62
+ /** Server-side cart list (live mode) — feeds switcher + account page. */
63
+ const serverCarts = skipHydrate(ref<CartSummaryEntry[]>([]));
64
+
65
+ const refreshCartList = async () => {
66
+ if (!import.meta.client || !liveCarts.value) {
67
+ return;
68
+ }
69
+ try {
70
+ const data = await $fetch<{ carts: CartSummaryEntry[]; activeCartId: string | null; managed: boolean }>("/api/carts");
71
+ if (data.managed) {
72
+ serverCarts.value = data.carts;
73
+ const active = data.carts.find(c => c.active);
74
+ if (active) {
75
+ activeCartId.value = active.id;
76
+ activeCartName.value = active.name;
77
+ }
78
+ }
79
+ }
80
+ catch { /* best-effort */ }
81
+ };
82
+
83
+ /* ---- multiple carts (feature-flagged via org settings) ------------- */
84
+ const activeCartId = skipHydrate(ref("default"));
85
+ const activeCartName = skipHydrate(ref("Mein Warenkorb"));
86
+ /** All carts EXCEPT the active one — `items`/`requisitions` stay the active cart. */
87
+ const extraCarts = skipHydrate(ref<NamedCart[]>([]));
88
+ /** Skip the target-cart dialog and always add to the active cart. */
89
+ const alwaysActiveCart = skipHydrate(ref(false));
90
+ /** Set by the modal host when the org flag is on and the dialog wanted. */
91
+ const interceptAdds = ref(false);
92
+ /** Last single-add waiting for a target-cart decision (drives the modal). */
93
+ const pendingAdd = ref<{ lineKey: string; name: string } | null>(null);
94
+
95
+ let lineCounter = 0;
96
+ const nextLineKey = (prefix: string): string => `${prefix}-${Date.now().toString(36)}-${++lineCounter}`;
97
+
98
+ const resolveUnitPrice = (item: CartItem): number => {
99
+ if (!item.priceTiers?.length) {
100
+ return item.price;
101
+ }
102
+
103
+ let effectivePrice = item.price;
104
+ for (const tier of item.priceTiers) {
105
+ if (item.quantity >= tier.minQuantity) {
106
+ effectivePrice = tier.unitPrice;
107
+ }
108
+ else {
109
+ break;
110
+ }
111
+ }
112
+ return effectivePrice;
113
+ };
114
+
115
+ const effectivePrices = computed(() => {
116
+ const map = new Map<string, number>();
117
+ for (const item of items.value) {
118
+ map.set(item.id, resolveUnitPrice(item));
119
+ }
120
+ return map;
121
+ });
122
+
123
+ const itemCount = computed(() => items.value.reduce((sum, i) => sum + i.quantity, 0));
124
+ const subtotal = computed(() =>
125
+ items.value.reduce((sum, i) => sum + resolveUnitPrice(i) * i.quantity, 0),
126
+ );
127
+ const isEmpty = computed(() => items.value.length === 0);
128
+
129
+ const TAX_RATE = 0.19;
130
+
131
+ const normalizeTaxRate = (taxRate: number): number => {
132
+ const normalizedRate = taxRate > 1 ? taxRate / 100 : taxRate;
133
+
134
+ if (!Number.isFinite(normalizedRate) || normalizedRate < 0) {
135
+ return TAX_RATE;
136
+ }
137
+
138
+ return normalizedRate;
139
+ };
140
+
141
+ const taxByRate = computed(() => {
142
+ const grouped = new Map<number, number>();
143
+
144
+ for (const item of items.value) {
145
+ const taxRateRaw
146
+ = typeof item.taxRate === "number" && Number.isFinite(item.taxRate)
147
+ ? item.taxRate
148
+ : TAX_RATE;
149
+ const taxRate = normalizeTaxRate(taxRateRaw);
150
+ const itemTax = resolveUnitPrice(item) * item.quantity * taxRate;
151
+
152
+ grouped.set(taxRate, (grouped.get(taxRate) ?? 0) + itemTax);
153
+ }
154
+
155
+ return [...grouped.entries()]
156
+ .map(([rate, amount]) => ({ rate, amount }))
157
+ .sort((a, b) => a.rate - b.rate);
158
+ });
159
+
160
+ const visibleTaxRows = computed(() =>
161
+ taxByRate.value.filter(row => row.rate > 0),
162
+ );
163
+ const tax = computed(() =>
164
+ taxByRate.value.reduce((sum, row) => sum + row.amount, 0),
165
+ );
166
+ const subtotalDisplay = computed(() =>
167
+ items.value.reduce((sum, item) => {
168
+ const unitPrice = resolveUnitPrice(item);
169
+ if (!taxIncludedPrices) {
170
+ return sum + unitPrice * item.quantity;
171
+ }
172
+
173
+ const taxRateRaw
174
+ = typeof item.taxRate === "number" && Number.isFinite(item.taxRate)
175
+ ? item.taxRate
176
+ : TAX_RATE;
177
+ const unitPriceWithTax = getPriceWithOptionalTax(unitPrice, taxRateRaw, true);
178
+
179
+ return sum + unitPriceWithTax * item.quantity;
180
+ }, 0),
181
+ );
182
+ const total = computed(() => subtotal.value + tax.value);
183
+
184
+ const addItem = (
185
+ product: Omit<CartItem, "quantity">,
186
+ options?: { chooseTarget?: boolean },
187
+ ): boolean => {
188
+ const existing = items.value.find(i => i.id === product.id && !i.requisitionId);
189
+ let addedKey: string | null = null;
190
+ if (existing) {
191
+ const maxOrderQuantity = existing.maxOrderQuantity ?? product.maxOrderQuantity;
192
+ if (typeof maxOrderQuantity === "number" && existing.quantity >= maxOrderQuantity) {
193
+ return false;
194
+ }
195
+ if (typeof existing.maxOrderQuantity !== "number" && typeof product.maxOrderQuantity === "number") {
196
+ existing.maxOrderQuantity = product.maxOrderQuantity;
197
+ }
198
+ existing.quantity++;
199
+ addedKey = existing.lineKey ?? null;
200
+ }
201
+ else {
202
+ if (typeof product.maxOrderQuantity === "number" && product.maxOrderQuantity < 1) {
203
+ return false;
204
+ }
205
+ const lineKey = nextLineKey("li");
206
+ items.value.push({ ...product, lineKey, quantity: 1 });
207
+ addedKey = lineKey;
208
+ }
209
+ if (options?.chooseTarget && interceptAdds.value && addedKey) {
210
+ pendingAdd.value = { lineKey: addedKey, name: product.name };
211
+ }
212
+ return true;
213
+ };
214
+
215
+ const removeItem = (id: string) => {
216
+ items.value = items.value.filter(i => i.id !== id);
217
+ };
218
+
219
+ const updateQuantity = (id: string, quantity: number) => {
220
+ if (quantity <= 0) {
221
+ removeItem(id);
222
+ return;
223
+ }
224
+ const item = items.value.find(i => i.id === id);
225
+ if (typeof item?.maxOrderQuantity === "number" && quantity > item.maxOrderQuantity) {
226
+ item.quantity = item.maxOrderQuantity;
227
+ return;
228
+ }
229
+ if (item) {
230
+ item.quantity = quantity;
231
+ }
232
+ };
233
+
234
+ const clearCart = () => {
235
+ items.value = [];
236
+ requisitions.value = [];
237
+ };
238
+
239
+ const keyOf = (item: CartItem): string => item.lineKey ?? `id-${item.id}`;
240
+
241
+ const findByKey = (lineKey: string): CartItem | undefined =>
242
+ items.value.find(i => keyOf(i) === lineKey);
243
+
244
+ const removeByKeys = (lineKeys: string[]) => {
245
+ const keys = new Set(lineKeys);
246
+ items.value = items.value.filter(i => !keys.has(keyOf(i)));
247
+ pruneEmptyRequisitions();
248
+ };
249
+
250
+ const updateQuantityByKey = (lineKey: string, quantity: number) => {
251
+ const item = findByKey(lineKey);
252
+ if (!item) {
253
+ return;
254
+ }
255
+ if (quantity <= 0) {
256
+ removeByKeys([lineKey]);
257
+ return;
258
+ }
259
+ if (typeof item.maxOrderQuantity === "number" && quantity > item.maxOrderQuantity) {
260
+ item.quantity = item.maxOrderQuantity;
261
+ return;
262
+ }
263
+ item.quantity = quantity;
264
+ };
265
+
266
+ const setCostCenter = (lineKeys: string[], costCenterId: string | undefined) => {
267
+ const keys = new Set(lineKeys);
268
+ for (const item of items.value) {
269
+ if (keys.has(keyOf(item))) {
270
+ item.costCenterId = costCenterId;
271
+ }
272
+ }
273
+ };
274
+
275
+ const setPositionDetails = (
276
+ lineKeys: string[],
277
+ details: { positionTexts?: string[]; customSku?: string },
278
+ ) => {
279
+ const keys = new Set(lineKeys);
280
+ for (const item of items.value) {
281
+ if (!keys.has(keyOf(item))) {
282
+ continue;
283
+ }
284
+ if (details.positionTexts !== undefined) {
285
+ const texts = details.positionTexts.map(t => t.trim());
286
+ while (texts.length && texts[texts.length - 1] === "") {
287
+ texts.pop();
288
+ }
289
+ item.positionTexts = texts.length ? texts : undefined;
290
+ }
291
+ if (details.customSku !== undefined) {
292
+ item.customSku = details.customSku.trim() || undefined;
293
+ }
294
+ }
295
+ };
296
+
297
+ /**
298
+ * Reorders a position within its scope (free items or one requisition) —
299
+ * requisition positions never leave their group (spec).
300
+ */
301
+ const moveWithinScope = (scope: string | undefined, fromIndex: number, toIndex: number) => {
302
+ const scopeIndices = items.value
303
+ .map((item, index) => ({ item, index }))
304
+ .filter(({ item }) => item.requisitionId === scope)
305
+ .map(({ index }) => index);
306
+ const from = scopeIndices[fromIndex];
307
+ const to = scopeIndices[toIndex];
308
+ if (from === undefined || to === undefined || from === to) {
309
+ return;
310
+ }
311
+ const next = [...items.value];
312
+ const [moved] = next.splice(from, 1);
313
+ next.splice(to, 0, moved!);
314
+ items.value = next;
315
+ };
316
+
317
+ /**
318
+ * Loads a requisition into the cart. Per spec, requisitions may only be
319
+ * combined when their delivery addresses match (company, street, postal
320
+ * code, city, country). Returns false when the validation fails.
321
+ */
322
+ const addRequisition = (requisition: Requisition): boolean => {
323
+ if (requisitions.value.some(r => r.id === requisition.id)) {
324
+ return false;
325
+ }
326
+ const addressKey = (a?: Requisition["deliveryAddress"]) =>
327
+ a ? [a.company, a.street, a.postalCode, a.city, a.country].join("|") : "";
328
+ const existingWithAddress = requisitions.value.find(r => r.deliveryAddress);
329
+ if (
330
+ existingWithAddress
331
+ && requisition.deliveryAddress
332
+ && addressKey(existingWithAddress.deliveryAddress) !== addressKey(requisition.deliveryAddress)
333
+ ) {
334
+ return false;
335
+ }
336
+ const { items: reqItems, ...meta } = requisition;
337
+ requisitions.value = [...requisitions.value, meta];
338
+ items.value = [
339
+ ...items.value,
340
+ ...reqItems.map(item => ({
341
+ ...item,
342
+ image: item.image ?? "",
343
+ lineKey: nextLineKey(requisition.id),
344
+ requisitionId: requisition.id,
345
+ })),
346
+ ];
347
+ return true;
348
+ };
349
+
350
+ /** Unloads a requisition (meta + its positions) from the cart. */
351
+ const removeRequisition = (requisitionId: string) => {
352
+ requisitions.value = requisitions.value.filter(r => r.id !== requisitionId);
353
+ items.value = items.value.filter(i => i.requisitionId !== requisitionId);
354
+ };
355
+
356
+ const pruneEmptyRequisitions = () => {
357
+ requisitions.value = requisitions.value.filter(r =>
358
+ items.value.some(i => i.requisitionId === r.id),
359
+ );
360
+ };
361
+
362
+ const syncToServer = async () => {
363
+ if (!import.meta.client) {
364
+ return;
365
+ }
366
+ try {
367
+ await $fetch("/api/cart/sync", {
368
+ method: "POST",
369
+ body: { items: items.value },
370
+ });
371
+ }
372
+ catch { /* local cart is the source of truth; server sync is best-effort */ }
373
+ };
374
+
375
+ const clearServerCart = async () => {
376
+ if (!import.meta.client) {
377
+ return;
378
+ }
379
+ try {
380
+ await $fetch("/api/cart", { method: "DELETE" });
381
+ }
382
+ catch { /* best-effort */ }
383
+ };
384
+
385
+ const initFromServer = async () => {
386
+ if (!import.meta.client) {
387
+ return;
388
+ }
389
+ try {
390
+ const data = await $fetch<{ items: CartItem[] }>("/api/cart");
391
+ if (liveCarts.value) {
392
+ // Live mode: the carts app is the source of truth.
393
+ if (data.items.length > 0) {
394
+ items.value = data.items;
395
+ }
396
+ else if (items.value.length > 0) {
397
+ void syncToServer();
398
+ }
399
+ void refreshCartList();
400
+ }
401
+ else if (data.items.length > 0 && items.value.length === 0) {
402
+ items.value = data.items;
403
+ }
404
+ else if (items.value.length > 0) {
405
+ void syncToServer();
406
+ }
407
+ }
408
+ catch { /* best-effort; continue with local cart */ }
409
+ };
410
+
411
+ /** After a live login the BFF merged/claimed the session carts — adopt the server state. */
412
+ const onAuthLogin = async () => {
413
+ if (!import.meta.client || !liveCarts.value) {
414
+ return;
415
+ }
416
+ try {
417
+ const data = await $fetch<{ items: CartItem[] }>("/api/cart");
418
+ items.value = data.items;
419
+ requisitions.value = [];
420
+ await refreshCartList();
421
+ }
422
+ catch { /* best-effort */ }
423
+ };
424
+
425
+ /** Logout: the customer's carts stay server-side; the local view resets to the guest scope. */
426
+ const onAuthLogout = async () => {
427
+ if (!import.meta.client || !liveCarts.value) {
428
+ return;
429
+ }
430
+ items.value = [];
431
+ requisitions.value = [];
432
+ serverCarts.value = [];
433
+ activeCartId.value = "default";
434
+ activeCartName.value = "Mein Warenkorb";
435
+ await refreshCartList();
436
+ };
437
+
438
+ const getEffectivePrice = (id: string) => {
439
+ return effectivePrices.value.get(id) ?? 0;
440
+ };
441
+
442
+ /* ---- multiple carts: switch, create, rename, delete ----------------- */
443
+ const valueOf = (cartItems: CartItem[]): number =>
444
+ Math.round(cartItems.reduce((sum, item) => sum + resolveUnitPrice(item) * item.quantity, 0) * 100) / 100;
445
+
446
+ const persistMultiCarts = () => {
447
+ if (!import.meta.client) {
448
+ return;
449
+ }
450
+ localStorage.setItem(MULTI_CART_STORAGE, JSON.stringify({
451
+ activeId: activeCartId.value,
452
+ activeName: activeCartName.value,
453
+ extra: extraCarts.value,
454
+ }));
455
+ };
456
+
457
+ /** All carts (active first) as summary rows for switcher and account page. */
458
+ const cartSummaries = computed<CartSummaryEntry[]>(() => {
459
+ if (liveCarts.value && serverCarts.value.length > 0) {
460
+ // Server truth, but the ACTIVE row reflects the local cart live
461
+ // (instant feedback between debounced syncs).
462
+ return serverCarts.value
463
+ .map(row => row.active
464
+ ? { ...row, itemCount: itemCount.value, positions: items.value.length, value: valueOf(items.value) }
465
+ : row)
466
+ .sort((a, b) => Number(b.active) - Number(a.active));
467
+ }
468
+ return localCartSummaries.value;
469
+ });
470
+
471
+ const localCartSummaries = computed<CartSummaryEntry[]>(() => [
472
+ {
473
+ id: activeCartId.value,
474
+ name: activeCartName.value,
475
+ itemCount: itemCount.value,
476
+ positions: items.value.length,
477
+ value: valueOf(items.value),
478
+ updatedAt: new Date().toISOString(),
479
+ active: true,
480
+ },
481
+ ...extraCarts.value.map(cart => ({
482
+ id: cart.id,
483
+ name: cart.name,
484
+ itemCount: cart.items.reduce((sum, i) => sum + i.quantity, 0),
485
+ positions: cart.items.length,
486
+ value: valueOf(cart.items),
487
+ updatedAt: cart.updatedAt,
488
+ active: false,
489
+ })),
490
+ ]);
491
+
492
+ const switchCart = (cartId: string) => {
493
+ if (cartId === activeCartId.value) {
494
+ return;
495
+ }
496
+ if (liveCarts.value) {
497
+ if (!SERVER_CART_ID.test(cartId)) {
498
+ return;
499
+ }
500
+ void (async () => {
501
+ try {
502
+ const data = await $fetch<{ items: CartItem[] }>(`/api/carts/${cartId}/activate`, { method: "POST" });
503
+ items.value = data.items;
504
+ requisitions.value = [];
505
+ const row = serverCarts.value.find(c => c.id === cartId);
506
+ activeCartId.value = cartId;
507
+ if (row) {
508
+ activeCartName.value = row.name;
509
+ }
510
+ await refreshCartList();
511
+ }
512
+ catch { /* keep the current cart on failure */ }
513
+ })();
514
+ return;
515
+ }
516
+ const target = extraCarts.value.find(cart => cart.id === cartId);
517
+ if (!target) {
518
+ return;
519
+ }
520
+ // Stash the current active cart, then activate the target.
521
+ extraCarts.value = [
522
+ {
523
+ id: activeCartId.value,
524
+ name: activeCartName.value,
525
+ items: items.value,
526
+ requisitions: requisitions.value,
527
+ updatedAt: new Date().toISOString(),
528
+ },
529
+ ...extraCarts.value.filter(cart => cart.id !== cartId),
530
+ ];
531
+ activeCartId.value = target.id;
532
+ activeCartName.value = target.name;
533
+ items.value = target.items;
534
+ requisitions.value = target.requisitions;
535
+ persistMultiCarts();
536
+ void syncToServer();
537
+ };
538
+
539
+ const createCart = (name: string, activate = true): string => {
540
+ if (liveCarts.value) {
541
+ void (async () => {
542
+ try {
543
+ const row = await $fetch<CartSummaryEntry>("/api/carts", { method: "POST", body: { name, activate } });
544
+ if (activate) {
545
+ items.value = [];
546
+ requisitions.value = [];
547
+ activeCartId.value = row.id;
548
+ activeCartName.value = row.name;
549
+ }
550
+ await refreshCartList();
551
+ }
552
+ catch { /* best-effort */ }
553
+ })();
554
+ return activeCartId.value;
555
+ }
556
+ const id = `cart-${Date.now().toString(36)}`;
557
+ extraCarts.value = [
558
+ ...extraCarts.value,
559
+ { id, name: name.trim() || "Warenkorb", items: [], requisitions: [], updatedAt: new Date().toISOString() },
560
+ ];
561
+ if (activate) {
562
+ switchCart(id);
563
+ }
564
+ else {
565
+ persistMultiCarts();
566
+ }
567
+ return id;
568
+ };
569
+
570
+ const renameCart = (cartId: string, name: string) => {
571
+ const trimmed = name.trim();
572
+ if (!trimmed) {
573
+ return;
574
+ }
575
+ if (liveCarts.value) {
576
+ if (cartId === activeCartId.value) {
577
+ activeCartName.value = trimmed;
578
+ }
579
+ // Only server carts (uuid ids) exist in the carts app — the local
580
+ // placeholder id ("default") has nothing to rename server-side.
581
+ if (SERVER_CART_ID.test(cartId)) {
582
+ void $fetch(`/api/carts/${cartId}`, { method: "PUT", body: { name: trimmed } })
583
+ .then(() => refreshCartList())
584
+ .catch(() => { /* best-effort */ });
585
+ }
586
+ return;
587
+ }
588
+ if (cartId === activeCartId.value) {
589
+ activeCartName.value = trimmed;
590
+ }
591
+ else {
592
+ const cart = extraCarts.value.find(c => c.id === cartId);
593
+ if (cart) {
594
+ cart.name = trimmed;
595
+ }
596
+ }
597
+ persistMultiCarts();
598
+ };
599
+
600
+ const deleteCart = (cartId: string) => {
601
+ if (liveCarts.value) {
602
+ if (!SERVER_CART_ID.test(cartId)) {
603
+ return;
604
+ }
605
+ void (async () => {
606
+ try {
607
+ await $fetch(`/api/carts/${cartId}`, { method: "DELETE" });
608
+ if (cartId === activeCartId.value) {
609
+ items.value = [];
610
+ requisitions.value = [];
611
+ await refreshCartList();
612
+ const next = serverCarts.value.find(c => c.id !== cartId);
613
+ if (next) {
614
+ switchCart(next.id);
615
+ }
616
+ else {
617
+ activeCartId.value = "default";
618
+ activeCartName.value = "Mein Warenkorb";
619
+ }
620
+ }
621
+ else {
622
+ await refreshCartList();
623
+ }
624
+ }
625
+ catch { /* best-effort */ }
626
+ })();
627
+ return;
628
+ }
629
+ if (cartId === activeCartId.value) {
630
+ // Deleting the active cart activates the next one (or a fresh default).
631
+ const next = extraCarts.value[0];
632
+ if (next) {
633
+ extraCarts.value = extraCarts.value.filter(cart => cart.id !== next.id);
634
+ activeCartId.value = next.id;
635
+ activeCartName.value = next.name;
636
+ items.value = next.items;
637
+ requisitions.value = next.requisitions;
638
+ }
639
+ else {
640
+ activeCartId.value = "default";
641
+ activeCartName.value = "Mein Warenkorb";
642
+ items.value = [];
643
+ requisitions.value = [];
644
+ }
645
+ }
646
+ else {
647
+ extraCarts.value = extraCarts.value.filter(cart => cart.id !== cartId);
648
+ }
649
+ persistMultiCarts();
650
+ void syncToServer();
651
+ };
652
+
653
+ /** Moves the line of a pending add into another cart (target-cart dialog). */
654
+ const moveLineToCart = (lineKey: string, cartId: string) => {
655
+ if (cartId === activeCartId.value) {
656
+ return;
657
+ }
658
+ const target = extraCarts.value.find(cart => cart.id === cartId);
659
+ const line = findByKey(lineKey);
660
+ if (!target || !line) {
661
+ return;
662
+ }
663
+ items.value = items.value.filter(i => keyOf(i) !== lineKey);
664
+ const existing = target.items.find(i => i.id === line.id && !i.requisitionId);
665
+ if (existing) {
666
+ existing.quantity += line.quantity;
667
+ }
668
+ else {
669
+ target.items.push(line);
670
+ }
671
+ target.updatedAt = new Date().toISOString();
672
+ persistMultiCarts();
673
+ };
674
+
675
+ const setAlwaysActiveCart = (value: boolean) => {
676
+ alwaysActiveCart.value = value;
677
+ if (import.meta.client) {
678
+ localStorage.setItem(ADD_PREF_STORAGE, value ? "1" : "0");
679
+ }
680
+ };
681
+
682
+ const openCart = () => {
683
+ isOpen.value = true;
684
+ };
685
+ const closeCart = () => {
686
+ isOpen.value = false;
687
+ };
688
+ const toggleCart = () => {
689
+ isOpen.value = !isOpen.value;
690
+ };
691
+
692
+ if (import.meta.client) {
693
+ const COOKIE_NAME = "cover-cart-has-items";
694
+ const cookieMaxAge = Math.floor(cartInactivityTtl / 1000);
695
+
696
+ const setCartCookie = () => {
697
+ document.cookie = `${COOKIE_NAME}=1; path=/; SameSite=Lax; max-age=${cookieMaxAge}`;
698
+ };
699
+ const clearCartCookie = () => {
700
+ document.cookie = `${COOKIE_NAME}=; path=/; SameSite=Lax; max-age=0`;
701
+ };
702
+
703
+ let inactivityTimer: ReturnType<typeof setTimeout> | null = null;
704
+
705
+ const clearTimer = () => {
706
+ if (inactivityTimer) {
707
+ clearTimeout(inactivityTimer);
708
+ inactivityTimer = null;
709
+ }
710
+ };
711
+
712
+ const scheduleExpiry = (delay = cartInactivityTtl) => {
713
+ clearTimer();
714
+ inactivityTimer = setTimeout(() => {
715
+ items.value = [];
716
+ }, delay);
717
+ };
718
+
719
+ let lastPersistAt = 0;
720
+ const PERSIST_THROTTLE = 30_000;
721
+
722
+ const onUserActivity = () => {
723
+ if (items.value.length === 0) {
724
+ return;
725
+ }
726
+ setCartCookie();
727
+ scheduleExpiry();
728
+ const now = Date.now();
729
+ if (now - lastPersistAt > PERSIST_THROTTLE) {
730
+ lastPersistAt = now;
731
+ localStorage.setItem("cover-cart", JSON.stringify({
732
+ items: items.value,
733
+ requisitions: requisitions.value,
734
+ lastActivityAt: new Date(now).toISOString(),
735
+ }));
736
+ }
737
+ };
738
+
739
+ document.addEventListener("click", onUserActivity, { passive: true });
740
+ document.addEventListener("scroll", onUserActivity, { passive: true, capture: true });
741
+
742
+ // Restore the multi-cart state (stashed carts + active cart meta and
743
+ // the add-target preference) — independent of the active cart's TTL.
744
+ const savedMulti = localStorage.getItem(MULTI_CART_STORAGE);
745
+ if (savedMulti) {
746
+ try {
747
+ const parsed = JSON.parse(savedMulti) as {
748
+ activeId: string;
749
+ activeName: string;
750
+ extra: NamedCart[];
751
+ };
752
+ activeCartId.value = parsed.activeId || "default";
753
+ activeCartName.value = parsed.activeName || "Mein Warenkorb";
754
+ extraCarts.value = parsed.extra ?? [];
755
+ }
756
+ catch {
757
+ // ignore malformed stored data
758
+ }
759
+ }
760
+ alwaysActiveCart.value = localStorage.getItem(ADD_PREF_STORAGE) === "1";
761
+
762
+ const saved = localStorage.getItem("cover-cart");
763
+ if (saved) {
764
+ try {
765
+ const { items: savedItems, requisitions: savedRequisitions, lastActivityAt } = JSON.parse(saved) as PersistedCart;
766
+ const elapsed = Date.now() - new Date(lastActivityAt).getTime();
767
+ if (elapsed < cartInactivityTtl) {
768
+ items.value = savedItems;
769
+ requisitions.value = savedRequisitions ?? [];
770
+ setCartCookie();
771
+ scheduleExpiry(cartInactivityTtl - elapsed);
772
+ }
773
+ else {
774
+ localStorage.removeItem("cover-cart");
775
+ }
776
+ }
777
+ catch {
778
+ // ignore malformed stored data
779
+ }
780
+ }
781
+
782
+ let syncTimer: ReturnType<typeof setTimeout> | null = null;
783
+
784
+ const debouncedSyncToServer = () => {
785
+ if (syncTimer) {
786
+ clearTimeout(syncTimer);
787
+ }
788
+ syncTimer = setTimeout(() => {
789
+ syncTimer = null;
790
+ void syncToServer();
791
+ }, 500);
792
+ };
793
+
794
+ watch(items, (val) => {
795
+ if (val.length > 0) {
796
+ setCartCookie();
797
+ localStorage.setItem("cover-cart", JSON.stringify({
798
+ items: val,
799
+ requisitions: requisitions.value,
800
+ lastActivityAt: new Date().toISOString(),
801
+ }));
802
+ scheduleExpiry();
803
+ }
804
+ else {
805
+ clearCartCookie();
806
+ clearTimer();
807
+ localStorage.removeItem("cover-cart");
808
+ }
809
+ debouncedSyncToServer();
810
+ }, { deep: true });
811
+
812
+ onScopeDispose(() => {
813
+ clearTimer();
814
+ if (syncTimer) {
815
+ clearTimeout(syncTimer);
816
+ }
817
+ document.removeEventListener("click", onUserActivity);
818
+ document.removeEventListener("scroll", onUserActivity, { capture: true });
819
+ });
820
+ }
821
+
822
+ return {
823
+ items, requisitions, isOpen, itemCount, effectivePrices, getEffectivePrice,
824
+ subtotal, subtotalDisplay, visibleTaxRows, tax, total, TAX_RATE, isEmpty,
825
+ addItem, removeItem, updateQuantity, clearCart,
826
+ keyOf, removeByKeys, updateQuantityByKey, setCostCenter, setPositionDetails,
827
+ moveWithinScope, addRequisition, removeRequisition,
828
+ openCart, closeCart, toggleCart,
829
+ syncToServer, clearServerCart, initFromServer,
830
+ // live carts (carts app)
831
+ liveCarts, serverCarts, refreshCartList, onAuthLogin, onAuthLogout,
832
+ // multiple carts (feature-flagged)
833
+ activeCartId, activeCartName, extraCarts, cartSummaries,
834
+ switchCart, createCart, renameCart, deleteCart, moveLineToCart,
835
+ alwaysActiveCart, setAlwaysActiveCart, interceptAdds, pendingAdd,
836
+ };
837
+ });