@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/app/api/account.ts +8 -0
- package/app/api/categories.ts +3 -0
- package/app/api/checkout.ts +6 -0
- package/app/api/images.ts +4 -0
- package/app/api/markets.ts +3 -0
- package/app/api/product.ts +3 -0
- package/app/api/products.ts +4 -0
- package/app/api/search.ts +4 -0
- package/app/app.config.ts +29 -0
- package/app/assets/css/cover.css +25 -0
- package/app/components/account/AccountMenu.vue +30 -0
- package/app/components/account/AccountShell.vue +18 -0
- package/app/components/account/AccountSidebar.vue +90 -0
- package/app/components/account/address/AccountAddressCard.vue +287 -0
- package/app/components/account/carts/AccountCartsTable.vue +284 -0
- package/app/components/account/dashboard/AccountDashboard.vue +351 -0
- package/app/components/account/directorder/AccountDirectOrder.vue +512 -0
- package/app/components/account/governance/AccountApprovalLimitsTable.vue +221 -0
- package/app/components/account/governance/AccountCostCenterDetail.vue +276 -0
- package/app/components/account/governance/AccountCostCentersTable.vue +252 -0
- package/app/components/account/governance/AccountRequisitionDetail.vue +295 -0
- package/app/components/account/governance/AccountRequisitionsTable.vue +255 -0
- package/app/components/account/governance/AccountWorkflowDetail.vue +215 -0
- package/app/components/account/governance/AccountWorkflowsTable.vue +168 -0
- package/app/components/account/governance/GovernanceApprovalLimitModal.vue +183 -0
- package/app/components/account/governance/GovernanceCostCenterModal.vue +188 -0
- package/app/components/account/governance/GovernanceWorkflowModal.vue +191 -0
- package/app/components/account/orderlists/AccountOrderListDetail.vue +349 -0
- package/app/components/account/orderlists/AccountOrderListsTable.vue +352 -0
- package/app/components/account/orders/AccountOrderDetail.vue +376 -0
- package/app/components/account/orders/AccountOrdersTable.vue +281 -0
- package/app/components/account/preferences/AccountPreferences.vue +50 -0
- package/app/components/auth/AuthForgotPasswordPanel.vue +88 -0
- package/app/components/auth/AuthLoginPanel.vue +103 -0
- package/app/components/auth/AuthLoginTeaser.vue +24 -0
- package/app/components/auth/AuthPageHeader.vue +21 -0
- package/app/components/auth/AuthRegisterPanel.vue +431 -0
- package/app/components/auth/AuthRegisterTeaser.vue +24 -0
- package/app/components/auth/AuthResetPasswordPanel.vue +115 -0
- package/app/components/cart/CartButton.vue +42 -0
- package/app/components/cart/actions/CartCostCenterModal.vue +110 -0
- package/app/components/cart/actions/CartExportModal.vue +82 -0
- package/app/components/cart/actions/CartPositionTextsModal.vue +92 -0
- package/app/components/cart/actions/CartSaveToListModal.vue +177 -0
- package/app/components/cart/actions/CartWorkflowPickerModal.vue +64 -0
- package/app/components/cart/drawer/CartDrawer.vue +49 -0
- package/app/components/cart/item/CartItem.vue +318 -0
- package/app/components/cart/manage/CartNameModal.vue +68 -0
- package/app/components/cart/position/CartPosition.vue +369 -0
- package/app/components/cart/quickadd/CartQuickAdd.vue +145 -0
- package/app/components/cart/recommend/CartCrossSell.vue +102 -0
- package/app/components/cart/recommend/CartRecommendCard.vue +59 -0
- package/app/components/cart/recommend/CartReorderStrip.vue +108 -0
- package/app/components/cart/requisition/CartRequisitionGroup.vue +74 -0
- package/app/components/cart/skeleton/CartSkeleton.vue +18 -0
- package/app/components/cart/summary/CartSummary.vue +25 -0
- package/app/components/cart/summary/CartSummaryBreakdown.vue +35 -0
- package/app/components/cart/switcher/CartSwitcher.vue +147 -0
- package/app/components/cart/target/CartTargetModal.vue +129 -0
- package/app/components/cart/view/CartApprovalsPanel.vue +89 -0
- package/app/components/cart/view/CartListHeader.vue +86 -0
- package/app/components/cart/view/CartPositionList.vue +94 -0
- package/app/components/cart/view/CartSummaryPanel.vue +254 -0
- package/app/components/cart/view/CartView.vue +310 -0
- package/app/components/category/info/CategoryPrice.vue +47 -0
- package/app/components/category/info/CategorySku.vue +15 -0
- package/app/components/category/info/CategoryStock.vue +25 -0
- package/app/components/category/list/CategoryItemCount.vue +27 -0
- package/app/components/category/list/CategoryListPagination.vue +81 -0
- package/app/components/category/skeleton/CategoryDetailSkeleton.vue +12 -0
- package/app/components/category/skeleton/CategoryListSkeleton.vue +65 -0
- package/app/components/category/view/CategoryViewToggle.vue +33 -0
- package/app/components/category/view/GridView.vue +78 -0
- package/app/components/category/view/ListView.vue +80 -0
- package/app/components/checkout/confirmation/CheckoutConfirmationView.vue +289 -0
- package/app/components/checkout/form/CheckoutSchemaForm.vue +72 -0
- package/app/components/checkout/onepage/CheckoutAddressPickerModal.vue +260 -0
- package/app/components/checkout/onepage/CheckoutAddressSection.vue +186 -0
- package/app/components/checkout/onepage/CheckoutDeliverySection.vue +158 -0
- package/app/components/checkout/onepage/CheckoutGate.vue +67 -0
- package/app/components/checkout/onepage/CheckoutGuestDetailsSection.vue +154 -0
- package/app/components/checkout/onepage/CheckoutNotesSection.vue +62 -0
- package/app/components/checkout/onepage/CheckoutOnePageView.vue +77 -0
- package/app/components/checkout/onepage/CheckoutPaymentSection.vue +181 -0
- package/app/components/checkout/onepage/CheckoutSectionTitle.vue +18 -0
- package/app/components/checkout/onepage/CheckoutSummarySection.vue +211 -0
- package/app/components/checkout/onepage/CheckoutWorkflowPanel.vue +38 -0
- package/app/components/layout/AppLogo.vue +21 -0
- package/app/components/layout/footer/Footer.vue +148 -0
- package/app/components/layout/header/AppBar.vue +75 -0
- package/app/components/layout/header/CategoryNav.vue +93 -0
- package/app/components/layout/header/HeaderActionItem.vue +81 -0
- package/app/components/layout/header/LocaleSwitcher.vue +123 -0
- package/app/components/layout/header/MainNav.vue +61 -0
- package/app/components/layout/header/MobileMenu.vue +81 -0
- package/app/components/layout/header/TopBar.vue +63 -0
- package/app/components/listing/ListingView.vue +227 -0
- package/app/components/product/detail/ProductDetailAddToCart.vue +188 -0
- package/app/components/product/detail/ProductDetailDescription.vue +20 -0
- package/app/components/product/detail/ProductDetailImageGallery.vue +58 -0
- package/app/components/product/detail/ProductDetailPricing.vue +108 -0
- package/app/components/product/detail/ProductDetailSaveToList.vue +291 -0
- package/app/components/product/detail/ProductDetailSpecifications.vue +33 -0
- package/app/components/product/detail/ProductDetailStock.vue +36 -0
- package/app/components/search/filter/SearchFacets.vue +256 -0
- package/app/components/search/filter/SearchFacetsDrawer.vue +58 -0
- package/app/components/ui/AppShell.vue +7 -0
- package/app/components/ui/debug/PiniaStateCard.vue +106 -0
- package/app/components/ui/feedback/EmptyState.vue +30 -0
- package/app/components/ui/feedback/ErrorBoundary.vue +44 -0
- package/app/components/ui/feedback/NotFound.vue +34 -0
- package/app/components/ui/forms/CountrySelection.vue +14 -0
- package/app/components/ui/forms/FormKitDatePicker.vue +124 -0
- package/app/components/ui/forms/PasswordInput.vue +136 -0
- package/app/components/ui/forms/SearchInput.vue +213 -0
- package/app/components/ui/table/TableSortButton.vue +51 -0
- package/app/components/ui/table/TableToolbar.vue +66 -0
- package/app/composables/useAccount.ts +27 -0
- package/app/composables/useAccountAddresses.ts +116 -0
- package/app/composables/useAccountOrders.ts +18 -0
- package/app/composables/useAppUrl.ts +38 -0
- package/app/composables/useAuthStore.ts +202 -0
- package/app/composables/useB2BContext.ts +23 -0
- package/app/composables/useCartActions.ts +25 -0
- package/app/composables/useCartCalculation.ts +90 -0
- package/app/composables/useCartSelection.ts +46 -0
- package/app/composables/useCartStore.ts +837 -0
- package/app/composables/useCartSummaryFormatting.ts +28 -0
- package/app/composables/useCategories.ts +29 -0
- package/app/composables/useCheckoutOnePage.ts +438 -0
- package/app/composables/useCheckoutProfileSource.ts +8 -0
- package/app/composables/useCheckoutSchema.ts +28 -0
- package/app/composables/useDataTable.ts +14 -0
- package/app/composables/useFormValidation.ts +29 -0
- package/app/composables/useIcons.ts +17 -0
- package/app/composables/useLocalePreferences.ts +45 -0
- package/app/composables/useMarkets.ts +15 -0
- package/app/composables/useProduct.ts +89 -0
- package/app/composables/useProductCard.ts +8 -0
- package/app/composables/useProductCardActions.ts +106 -0
- package/app/composables/useProductImage.ts +12 -0
- package/app/composables/useProductListing.ts +210 -0
- package/app/composables/useProductUrl.ts +17 -0
- package/app/composables/useProducts.ts +130 -0
- package/app/composables/useSearch.ts +12 -0
- package/app/composables/useThemeStore.ts +81 -0
- package/app/composables/useViewMode.ts +18 -0
- package/app/config/countries.ts +74 -0
- package/app/config/icons.ts +283 -0
- package/app/config/navigation.ts +191 -0
- package/app/config/palette.ts +13 -0
- package/app/config/themes.ts +20 -0
- package/app/formkit.config.ts +50 -0
- package/app/interfaces/account/address-list.ts +34 -0
- package/app/interfaces/account/order-list.ts +47 -0
- package/app/interfaces/account/order-lists.ts +35 -0
- package/app/interfaces/account/profile.ts +17 -0
- package/app/interfaces/account.ts +3 -0
- package/app/interfaces/address.ts +9 -0
- package/app/interfaces/auth.ts +104 -0
- package/app/interfaces/b2b.ts +234 -0
- package/app/interfaces/cart-calculation.ts +131 -0
- package/app/interfaces/cart-item.ts +45 -0
- package/app/interfaces/checkout-draft.ts +4 -0
- package/app/interfaces/checkout.ts +43 -0
- package/app/interfaces/delivery.ts +13 -0
- package/app/interfaces/market.ts +24 -0
- package/app/interfaces/payment.ts +19 -0
- package/app/interfaces/persisted-cart.ts +10 -0
- package/app/interfaces/product-detail.ts +54 -0
- package/app/interfaces/product-list.ts +33 -0
- package/app/interfaces/search-facets.ts +14 -0
- package/app/interfaces/validation.ts +14 -0
- package/app/layouts/default.vue +78 -0
- package/app/layouts/focus.vue +80 -0
- package/app/plugins/formkit-locale.client.ts +23 -0
- package/app/shared/constants.ts +1 -0
- package/app/validations/companyName.ts +18 -0
- package/app/validations/emailFormat.ts +10 -0
- package/app/validations/formValidationConfig.ts +50 -0
- package/app/validations/maxLength.ts +12 -0
- package/app/validations/minLength.ts +9 -0
- package/app/validations/optionalCompanyName.ts +23 -0
- package/app/validations/passwordsMatch.ts +5 -0
- package/app/validations/phoneNumber.ts +19 -0
- package/app/validations/required.ts +11 -0
- package/app/validations/termsRequired.ts +4 -0
- package/app/validations/types.ts +10 -0
- package/app/validations/zipCode.ts +15 -0
- package/i18n/locales/de/account.json +458 -0
- package/i18n/locales/de/auth.json +155 -0
- package/i18n/locales/de/cart.json +263 -0
- package/i18n/locales/de/checkout.json +217 -0
- package/i18n/locales/de/common.json +80 -0
- package/i18n/locales/de/cover.json +33 -0
- package/i18n/locales/de/order.json +1 -0
- package/i18n/locales/de/product.json +71 -0
- package/i18n/locales/de/search.json +54 -0
- package/i18n/locales/de/validation.json +12 -0
- package/i18n/locales/en/account.json +458 -0
- package/i18n/locales/en/auth.json +155 -0
- package/i18n/locales/en/cart.json +263 -0
- package/i18n/locales/en/checkout.json +217 -0
- package/i18n/locales/en/common.json +80 -0
- package/i18n/locales/en/cover.json +33 -0
- package/i18n/locales/en/order.json +1 -0
- package/i18n/locales/en/product.json +71 -0
- package/i18n/locales/en/search.json +54 -0
- package/i18n/locales/en/validation.json +12 -0
- package/nuxt.config.ts +109 -0
- package/package.json +65 -0
- package/public/img/product-placeholder.svg +8 -0
- package/public/templates/direct-order-template.csv +3 -0
- package/public/templates/direct-order-template.xlsx +0 -0
- package/server/api/account/address/[id].delete.ts +51 -0
- package/server/api/account/address/[id].put.ts +83 -0
- package/server/api/account/address/index.post.ts +60 -0
- package/server/api/account/addresses.get.ts +25 -0
- package/server/api/account/context.get.ts +16 -0
- package/server/api/account/governance/[domain]/[id].put.ts +57 -0
- package/server/api/account/governance/[domain].post.ts +111 -0
- package/server/api/account/order-lists/[id].delete.ts +22 -0
- package/server/api/account/order-lists/[id].get.ts +17 -0
- package/server/api/account/order-lists/[id].put.ts +46 -0
- package/server/api/account/order-lists/index.get.ts +14 -0
- package/server/api/account/order-lists/index.post.ts +38 -0
- package/server/api/account/orders/[id].get.ts +35 -0
- package/server/api/account/orders.get.ts +35 -0
- package/server/api/account/profile.get.ts +56 -0
- package/server/api/account/profile.put.ts +128 -0
- package/server/api/account/requisitions/[id].get.ts +15 -0
- package/server/api/account/requisitions.get.ts +23 -0
- package/server/api/auth/login.post.ts +37 -0
- package/server/api/auth/logout.post.ts +4 -0
- package/server/api/auth/me.get.ts +3 -0
- package/server/api/auth/personas.get.ts +7 -0
- package/server/api/auth/recovery.post.ts +32 -0
- package/server/api/auth/recovery.put.ts +37 -0
- package/server/api/auth/register.post.ts +126 -0
- package/server/api/cart/calculate.post.ts +25 -0
- package/server/api/cart/export.post.ts +81 -0
- package/server/api/cart/index.delete.ts +10 -0
- package/server/api/cart/index.get.ts +10 -0
- package/server/api/cart/sync.post.ts +14 -0
- package/server/api/carts/[id]/activate.post.ts +18 -0
- package/server/api/carts/[id]/index.delete.ts +18 -0
- package/server/api/carts/[id]/index.put.ts +19 -0
- package/server/api/carts/[id]/items.post.ts +21 -0
- package/server/api/carts/index.get.ts +14 -0
- package/server/api/carts/index.post.ts +14 -0
- package/server/api/categories/[slug].get.ts +22 -0
- package/server/api/categories/index.get.ts +11 -0
- package/server/api/checkout/profile.get.ts +21 -0
- package/server/api/checkout/schema/[key].get.ts +19 -0
- package/server/api/checkout/session.post.ts +8 -0
- package/server/api/images/detail/[filename].get.ts +18 -0
- package/server/api/images/list/[filename].get.ts +17 -0
- package/server/api/markets.get.ts +9 -0
- package/server/api/orders/index.post.ts +376 -0
- package/server/api/payment/methods.post.ts +65 -0
- package/server/api/product/[id].get.ts +29 -0
- package/server/api/products/[id].get.ts +30 -0
- package/server/api/products/index.get.ts +21 -0
- package/server/api/requisitions/index.post.ts +67 -0
- package/server/api/shipping/rates.post.ts +70 -0
- package/server/api/themes/index.get.ts +9 -0
- package/server/api/typesense/drop.get.ts +22 -0
- package/server/api/typesense/health.get.ts +9 -0
- package/server/api/typesense/search.post.ts +199 -0
- package/server/api/typesense/seed.get.ts +112 -0
- package/server/api/typesense/suggest.post.ts +69 -0
- package/server/config/account/organization.json +146 -0
- package/server/config/account/personas.json +169 -0
- package/server/config/account/user.json +8 -0
- package/server/config/account/workflows.json +19 -0
- package/server/data/account/address-list.json +103 -0
- package/server/data/account/order-list.json +491 -0
- package/server/data/account/order-lists.json +149 -0
- package/server/data/account/profile.json +9 -0
- package/server/data/account/registration-requests.json +3 -0
- package/server/data/account/requisitions.json +686 -0
- package/server/data/categories.json +186 -0
- package/server/data/forms/checkout/add-address.json +24 -0
- package/server/data/list/5137-1.png +0 -0
- package/server/data/list/5498-1.png +0 -0
- package/server/data/list/5498-2.png +0 -0
- package/server/data/list/5498-3.png +0 -0
- package/server/data/list/5498-4.png +0 -0
- package/server/data/list/5498-5.png +0 -0
- package/server/data/list/5498-6.png +0 -0
- package/server/data/list/5519-1.png +0 -0
- package/server/data/list/5713-1.png +0 -0
- package/server/data/list/5789-1.png +0 -0
- package/server/data/list/5930-1.png +0 -0
- package/server/data/list/6127-1.png +0 -0
- package/server/data/list/6234-1.png +0 -0
- package/server/data/list/6238-1.png +0 -0
- package/server/data/list/6246-1.png +0 -0
- package/server/data/list/6270-1.png +0 -0
- package/server/data/list/6330-1.png +0 -0
- package/server/data/list/6336-1.png +0 -0
- package/server/data/list/6360-1.png +0 -0
- package/server/data/list/6363-1.png +0 -0
- package/server/data/list/6375-1.png +0 -0
- package/server/data/list/6385-1.png +0 -0
- package/server/data/list/6413-1.png +0 -0
- package/server/data/list/6418-1.png +0 -0
- package/server/data/list/6465-1.png +0 -0
- package/server/data/list/6477-1.png +0 -0
- package/server/data/list/6509-1.png +0 -0
- package/server/data/list/6545-1.png +0 -0
- package/server/data/list/6548-1.png +0 -0
- package/server/data/list/6566-1.png +0 -0
- package/server/data/list/6581-1.png +0 -0
- package/server/data/list/6609-1.png +0 -0
- package/server/data/list/6611-1.png +0 -0
- package/server/data/list/6641-1.png +0 -0
- package/server/data/list/6659-1.png +0 -0
- package/server/data/list/6662-1.png +0 -0
- package/server/data/list/6689-1.png +0 -0
- package/server/data/list/6698-1.png +0 -0
- package/server/data/list/6701-1.png +0 -0
- package/server/data/list/6752-1.png +0 -0
- package/server/data/list/6755-1.png +0 -0
- package/server/data/list/6837-1.png +0 -0
- package/server/data/list/6841-1.png +0 -0
- package/server/data/list/6844-1.png +0 -0
- package/server/data/list/6846-1.png +0 -0
- package/server/data/list/6886-1.png +0 -0
- package/server/data/list/6895-1.png +0 -0
- package/server/data/list/6897-1.png +0 -0
- package/server/data/list/6919-1.png +0 -0
- package/server/data/list/6977-1.png +0 -0
- package/server/data/list/6983-1.png +0 -0
- package/server/data/list/6984-1.png +0 -0
- package/server/data/list/6985-1.png +0 -0
- package/server/data/list/6986-1.png +0 -0
- package/server/data/list/6989-1.png +0 -0
- package/server/data/list/6995-1.png +0 -0
- package/server/data/list/6998-1.png +0 -0
- package/server/data/markets.json +24 -0
- package/server/data/product-detail.json +2450 -0
- package/server/data/product-list.json +2450 -0
- package/server/data/themes.json +8 -0
- package/server/interfaces/account.ts +20 -0
- package/server/interfaces/auth.ts +32 -0
- package/server/interfaces/b2bContext.ts +23 -0
- package/server/interfaces/cart.ts +46 -0
- package/server/interfaces/cartCalculation.ts +17 -0
- package/server/interfaces/category.ts +12 -0
- package/server/interfaces/checkoutProfile.ts +5 -0
- package/server/interfaces/log.ts +3 -0
- package/server/interfaces/market.ts +10 -0
- package/server/interfaces/product.ts +21 -0
- package/server/interfaces/schema.ts +20 -0
- package/server/interfaces/theme.ts +10 -0
- package/server/services/ApiAuthService.ts +138 -0
- package/server/services/ApiCartService.ts +254 -0
- package/server/services/ApiCategoryService.ts +25 -0
- package/server/services/ApiMarketService.ts +71 -0
- package/server/services/ApiProductService.ts +53 -0
- package/server/services/LocalFileCategoryService.ts +23 -0
- package/server/services/LocalFileCheckoutProfileService.ts +117 -0
- package/server/services/LocalFileProductService.ts +128 -0
- package/server/services/LocalFileSchemaService.ts +70 -0
- package/server/services/LocalFileThemeService.ts +18 -0
- package/server/services/LogService.ts +28 -0
- package/server/services/MockAccountService.ts +58 -0
- package/server/services/MockAuthService.ts +105 -0
- package/server/services/MockB2BContextService.ts +149 -0
- package/server/services/MockCartCalculationService.ts +395 -0
- package/server/services/MockMarketService.ts +18 -0
- package/server/services/SdkAccountService.ts +56 -0
- package/server/services/SdkAuthService.ts +83 -0
- package/server/services/SessionCartService.ts +31 -0
- package/server/utils/accountService.ts +30 -0
- package/server/utils/authCookie.ts +13 -0
- package/server/utils/authService.ts +43 -0
- package/server/utils/b2bService.ts +49 -0
- package/server/utils/cartService.ts +59 -0
- package/server/utils/categoryService.ts +19 -0
- package/server/utils/checkoutProfileService.ts +9 -0
- package/server/utils/checkoutSession.ts +38 -0
- package/server/utils/coverData.ts +84 -0
- package/server/utils/governanceStore.ts +38 -0
- package/server/utils/i18n.ts +76 -0
- package/server/utils/inventoryService.ts +14 -0
- package/server/utils/liveCatalog.ts +234 -0
- package/server/utils/liveInventories.ts +76 -0
- package/server/utils/liveOrders.ts +139 -0
- package/server/utils/livePrices.ts +93 -0
- package/server/utils/locale.ts +39 -0
- package/server/utils/logService.ts +19 -0
- package/server/utils/marketService.ts +24 -0
- package/server/utils/orderService.ts +14 -0
- package/server/utils/paymentService.ts +14 -0
- package/server/utils/priceService.ts +14 -0
- package/server/utils/productService.ts +28 -0
- package/server/utils/productsSchema.ts +30 -0
- package/server/utils/revenexxApi.ts +136 -0
- package/server/utils/schemaService.ts +25 -0
- package/server/utils/serviceMode.ts +70 -0
- package/server/utils/shippingService.ts +14 -0
- package/server/utils/shopSdk.ts +88 -0
- package/server/utils/themeService.ts +16 -0
- package/server/utils/typesense.ts +27 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "id": "midnight", "name": "Midnight", "colors": { "primary": "indigo", "neutral": "zinc" }, "tokens": { "radius": "0.375rem" } },
|
|
3
|
+
{ "id": "aurora", "name": "Aurora", "colors": { "primary": "violet", "neutral": "slate" }, "tokens": { "radius": "0.5rem" } },
|
|
4
|
+
{ "id": "ember", "name": "Ember", "colors": { "primary": "orange", "neutral": "stone" }, "tokens": { "radius": "0.375rem" } },
|
|
5
|
+
{ "id": "ocean", "name": "Ocean", "colors": { "primary": "sky", "neutral": "slate" }, "tokens": { "radius": "0.5rem" } },
|
|
6
|
+
{ "id": "forest", "name": "Forest", "colors": { "primary": "emerald", "neutral": "zinc" }, "tokens": { "radius": "0.25rem" } },
|
|
7
|
+
{ "id": "blossom", "name": "Blossom", "colors": { "primary": "rose", "neutral": "zinc" }, "tokens": { "radius": "0.75rem" } }
|
|
8
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
|
|
3
|
+
export interface AccountUser {
|
|
4
|
+
readonly id: number | string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly email: string;
|
|
7
|
+
readonly initials: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Service contract for resolving the current authenticated user.
|
|
12
|
+
* Implementations may read from a session, identity provider, or mock source.
|
|
13
|
+
* Register the active implementation via app.config → accountService.
|
|
14
|
+
*
|
|
15
|
+
* The H3 event carries the request context (session cookie) that live
|
|
16
|
+
* implementations need; mock implementations may ignore it.
|
|
17
|
+
*/
|
|
18
|
+
export interface IAccountService {
|
|
19
|
+
getUser(event?: H3Event): Promise<AccountUser>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
|
|
3
|
+
import type { AuthRole, AuthUser } from "../../app/interfaces/auth";
|
|
4
|
+
|
|
5
|
+
export interface AuthPersona {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly email: string;
|
|
9
|
+
readonly role: AuthRole;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AuthLoginResult {
|
|
14
|
+
user: AuthUser;
|
|
15
|
+
session: { id: string; expire: string };
|
|
16
|
+
/** Public-API logins: the customers-app contact behind the user (cart owner). */
|
|
17
|
+
contactId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Service contract for authentication.
|
|
22
|
+
* - "mock" signs B2B demo personas in without any external dependency
|
|
23
|
+
* (see server/config/account/personas.json)
|
|
24
|
+
* - "sdk" authenticates against the platform via the revenexx web SDK
|
|
25
|
+
* Both implementations manage the same session cookie, so the client-side
|
|
26
|
+
* auth flow (store, guards, middleware) is identical.
|
|
27
|
+
*/
|
|
28
|
+
export interface IAuthService {
|
|
29
|
+
login(event: H3Event, email: string, password: string): Promise<AuthLoginResult>;
|
|
30
|
+
me(event: H3Event): Promise<AuthUser | null>;
|
|
31
|
+
logout(event: H3Event): Promise<void>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { B2BContext, Requisition } from "../../app/interfaces/b2b";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Service contract for the per-user B2B context: organization settings,
|
|
5
|
+
* cost centers, spending limits and workflow memberships.
|
|
6
|
+
*
|
|
7
|
+
* Register the active implementation via app.config → b2bService.
|
|
8
|
+
*/
|
|
9
|
+
export interface IB2BContextService {
|
|
10
|
+
/** Context for a user; `null` userId returns the guest context. */
|
|
11
|
+
getContext(userId: string | null): Promise<B2BContext>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service contract for requisitions (order requests traveling through
|
|
16
|
+
* approval workflows).
|
|
17
|
+
*/
|
|
18
|
+
export interface IRequisitionService {
|
|
19
|
+
/** Open requisitions the given user may load into the cart. */
|
|
20
|
+
listOpen(userId: string): Promise<Requisition[]>;
|
|
21
|
+
/** All requisitions visible to the user (any status, incl. audit trail). */
|
|
22
|
+
list(userId: string): Promise<Requisition[]>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
|
|
3
|
+
import type { CartItem } from "../../app/interfaces/cart-item";
|
|
4
|
+
|
|
5
|
+
/** Summary row for the cart switcher / account page. */
|
|
6
|
+
export interface CartSummaryRow {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
itemCount: number;
|
|
10
|
+
positions: number;
|
|
11
|
+
value: number;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
active: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Service contract for the ACTIVE cart — what the storefront syncs.
|
|
18
|
+
* Implementations resolve the owner from the request themselves:
|
|
19
|
+
* - "mock" keys an in-memory store by the cart session cookie
|
|
20
|
+
* - "api" persists into the carts app (guest = session cart, logged-in =
|
|
21
|
+
* the contact's current cart)
|
|
22
|
+
*/
|
|
23
|
+
export interface ICartService {
|
|
24
|
+
getItems(event: H3Event): Promise<CartItem[]>;
|
|
25
|
+
saveItems(event: H3Event, items: CartItem[]): Promise<void>;
|
|
26
|
+
clearItems(event: H3Event): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Multi-cart management — live mode only (the carts app is the source of
|
|
31
|
+
* truth). Mock mode keeps the demo shop's local multi-cart behaviour.
|
|
32
|
+
*/
|
|
33
|
+
export interface ICartManager {
|
|
34
|
+
listCarts(event: H3Event): Promise<{ carts: CartSummaryRow[]; activeCartId: string | null }>;
|
|
35
|
+
createCart(event: H3Event, name: string, activate: boolean): Promise<CartSummaryRow>;
|
|
36
|
+
renameCart(event: H3Event, id: string, name: string): Promise<void>;
|
|
37
|
+
deleteCart(event: H3Event, id: string): Promise<void>;
|
|
38
|
+
/** Activates the cart and answers its items (the new active cart). */
|
|
39
|
+
activateCart(event: H3Event, id: string): Promise<CartItem[]>;
|
|
40
|
+
/** Adds one line to a non-active cart (the target-cart dialog). */
|
|
41
|
+
addItemToCart(event: H3Event, id: string, item: CartItem): Promise<void>;
|
|
42
|
+
/** Login hand-over: session carts adopt the contact or merge into the current cart. */
|
|
43
|
+
claimSession(event: H3Event, contactId: string): Promise<void>;
|
|
44
|
+
/** Order hand-over: freeze the owner's current cart as ordered (+order_ref). */
|
|
45
|
+
orderActiveCart(event: H3Event, orderRef: string): Promise<string | null>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { B2BContext } from "../../app/interfaces/b2b";
|
|
2
|
+
import type { CartCalculation, CartCalculationRequest } from "../../app/interfaces/cart-calculation";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Service contract for cart calculation. ERP-backed implementations send
|
|
6
|
+
* the cart downstream and map the document response into the generic
|
|
7
|
+
* CartCalculation shape; the mock computes the non-ERP subset.
|
|
8
|
+
*
|
|
9
|
+
* Register the active implementation via app.config → cartService.
|
|
10
|
+
*/
|
|
11
|
+
export interface ICartCalculationService {
|
|
12
|
+
calculate(
|
|
13
|
+
request: CartCalculationRequest,
|
|
14
|
+
context: B2BContext,
|
|
15
|
+
locale: string,
|
|
16
|
+
): Promise<CartCalculation>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProductCategory } from "../../app/config/navigation";
|
|
2
|
+
import type { Locale } from "../utils/locale";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Service contract for category data access.
|
|
6
|
+
* Implementations may read from local JSON, a database, or a remote API.
|
|
7
|
+
* Register the active implementation via app.config → categoryService.
|
|
8
|
+
*/
|
|
9
|
+
export interface ICategoryService {
|
|
10
|
+
listCategories(locale?: Locale): Promise<ProductCategory[]>;
|
|
11
|
+
getCategoryBySlug(slug: string, locale?: Locale): Promise<ProductCategory | null>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ShopMarket } from "../../app/interfaces/market";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Service contract for market data — the storefront's market backbone
|
|
5
|
+
* (markets app): which markets exist, their currency and their locales.
|
|
6
|
+
* Register the active implementation via app.config → marketService.
|
|
7
|
+
*/
|
|
8
|
+
export interface IMarketService {
|
|
9
|
+
listMarkets(): Promise<ShopMarket[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Product } from "../../app/composables/useProducts";
|
|
2
|
+
import type { ProductDetail } from "../../app/interfaces/product-detail";
|
|
3
|
+
import type { Locale } from "../utils/locale";
|
|
4
|
+
|
|
5
|
+
export interface ProductListFilter {
|
|
6
|
+
category?: string;
|
|
7
|
+
subcategory?: string;
|
|
8
|
+
/** Request locale — live implementations serve locale-scoped documents. */
|
|
9
|
+
locale?: Locale;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Service contract for product data access.
|
|
14
|
+
* Implementations may read from local JSON, a database, or a remote API.
|
|
15
|
+
* Register the active implementation via app.config → productService.
|
|
16
|
+
*/
|
|
17
|
+
export interface IProductService {
|
|
18
|
+
listProducts(filter?: ProductListFilter): Promise<Product[]>;
|
|
19
|
+
getProductById(id: string, locale?: Locale): Promise<Product | null>;
|
|
20
|
+
getProductDetail(id: string, locale?: Locale): Promise<ProductDetail | null>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SchemaValue = string | boolean | number | null | SchemaObject | SchemaValue[];
|
|
2
|
+
export interface SchemaObject { [key: string]: SchemaValue }
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Service contract for loading and translating form schemas by key.
|
|
6
|
+
* Implementations may read from local JSON files, a remote CMS, a database, etc.
|
|
7
|
+
* Register the active implementation via app.config → schemaService.
|
|
8
|
+
*/
|
|
9
|
+
export interface ISchemaService {
|
|
10
|
+
getSchema(key: string, locale: string): Promise<SchemaObject>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Capability contract for translating schema i18n keys in-place.
|
|
15
|
+
* A class may implement both ISchemaService and ISchemaTranslator,
|
|
16
|
+
* with getSchema() delegating to translateSchema() internally.
|
|
17
|
+
*/
|
|
18
|
+
export interface ISchemaTranslator {
|
|
19
|
+
translateSchema(schema: SchemaValue, locale: string): SchemaValue;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Theme } from "../../app/config/themes";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Service contract for theme data access.
|
|
5
|
+
* Implementations may read from local JSON, a database, or a remote API.
|
|
6
|
+
* Register the active implementation via app.config → themeService.
|
|
7
|
+
*/
|
|
8
|
+
export interface IThemeService {
|
|
9
|
+
listThemes(): Promise<Theme[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
|
|
3
|
+
import type { AuthRole, AuthUser, StoredSession } from "../../app/interfaces/auth";
|
|
4
|
+
import type { AuthLoginResult, IAuthService } from "../interfaces/auth";
|
|
5
|
+
|
|
6
|
+
/** Session payload of the customers app's auth passthrough. */
|
|
7
|
+
interface ApiSession {
|
|
8
|
+
$id: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
secret?: string;
|
|
11
|
+
expire: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Contact row mirrored by the customers app (system of record). */
|
|
15
|
+
interface ApiContact {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
first_name?: string | null;
|
|
19
|
+
last_name?: string | null;
|
|
20
|
+
role?: string | null;
|
|
21
|
+
organization_id?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const KNOWN_ROLES = new Set<AuthRole>(["buyer", "approver", "admin", "requester"]);
|
|
25
|
+
|
|
26
|
+
function toAuthRole(role?: string | null): AuthRole | undefined {
|
|
27
|
+
return role && KNOWN_ROLES.has(role as AuthRole) ? (role as AuthRole) : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function contactName(contact: ApiContact | null, fallback: string): string {
|
|
31
|
+
const name = [contact?.first_name, contact?.last_name].filter(Boolean).join(" ").trim();
|
|
32
|
+
return name || fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Live authentication via the public revenexx API (customers app):
|
|
37
|
+
* POST /v1/customers/auth/login → platform session (incl. secret) + contact
|
|
38
|
+
* POST /v1/customers/auth/me → platform user + contact
|
|
39
|
+
* POST /v1/customers/auth/logout → revokes the platform session
|
|
40
|
+
*
|
|
41
|
+
* Session state lives in the same cookie as the other implementations, so
|
|
42
|
+
* the client-side auth flow (store, guards, middleware) is identical. The
|
|
43
|
+
* session secret stays server-side only — the BFF never forwards it.
|
|
44
|
+
*/
|
|
45
|
+
export class ApiAuthService implements IAuthService {
|
|
46
|
+
async login(event: H3Event, email: string, password: string): Promise<AuthLoginResult> {
|
|
47
|
+
const api = useRevenexxApi();
|
|
48
|
+
const { session, contact } = await api.post<{ session: ApiSession; contact: ApiContact | null }>(
|
|
49
|
+
"/v1/customers/auth/login",
|
|
50
|
+
{ email, password },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const storedSession: StoredSession = {
|
|
54
|
+
id: session.$id,
|
|
55
|
+
expire: session.expire,
|
|
56
|
+
userId: session.userId,
|
|
57
|
+
...(contact?.id ? { contactId: contact.id } : {}),
|
|
58
|
+
...(contact?.organization_id ? { organizationId: contact.organization_id } : {}),
|
|
59
|
+
};
|
|
60
|
+
setCookie(event, SESSION_COOKIE_NAME, JSON.stringify(storedSession), sessionCookieOptions(session.expire));
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
user: {
|
|
64
|
+
$id: session.userId,
|
|
65
|
+
name: contactName(contact, email),
|
|
66
|
+
email: contact?.email ?? email,
|
|
67
|
+
role: toAuthRole(contact?.role),
|
|
68
|
+
},
|
|
69
|
+
session: { id: session.$id, expire: session.expire },
|
|
70
|
+
...(contact?.id ? { contactId: contact.id } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async me(event: H3Event): Promise<AuthUser | null> {
|
|
75
|
+
const raw = getCookie(event, SESSION_COOKIE_NAME);
|
|
76
|
+
if (!raw) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let parsed: StoredSession;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(raw) as StoredSession;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
getLogService().error("Failed to parse session cookie", toErrorContext(err));
|
|
86
|
+
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!parsed.userId) {
|
|
91
|
+
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// session_id makes the customers app validate the session is still
|
|
97
|
+
// alive — a logged-out/revoked session answers 401 here.
|
|
98
|
+
const { user, contact } = await useRevenexxApi().post<{
|
|
99
|
+
user: { $id: string; name?: string; email: string };
|
|
100
|
+
contact: ApiContact | null;
|
|
101
|
+
}>("/v1/customers/auth/me", { user_id: parsed.userId, session_id: parsed.id });
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
$id: user.$id,
|
|
105
|
+
name: contactName(contact, user.name ?? user.email),
|
|
106
|
+
email: user.email,
|
|
107
|
+
role: toAuthRole(contact?.role),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (!isApiUserError(err)) {
|
|
112
|
+
getLogService().error("API request failed: auth/me", apiErrorContext(err));
|
|
113
|
+
}
|
|
114
|
+
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async logout(event: H3Event): Promise<void> {
|
|
120
|
+
const raw = getCookie(event, SESSION_COOKIE_NAME);
|
|
121
|
+
if (raw) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(raw) as StoredSession;
|
|
124
|
+
if (parsed.userId && parsed.id) {
|
|
125
|
+
await useRevenexxApi().post("/v1/customers/auth/logout", {
|
|
126
|
+
user_id: parsed.userId,
|
|
127
|
+
session_id: parsed.id,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
getLogService().error("API request failed: auth/logout", apiErrorContext(err));
|
|
133
|
+
// always clear the cookie regardless of API result
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
|
|
3
|
+
import type { CartItem } from "../../app/interfaces/cart-item";
|
|
4
|
+
import type { StoredSession } from "../../app/interfaces/auth";
|
|
5
|
+
import type { CartSummaryRow, ICartManager, ICartService } from "../interfaces/cart";
|
|
6
|
+
|
|
7
|
+
/** Cart row of the carts app. */
|
|
8
|
+
interface ApiCart {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
status: string;
|
|
12
|
+
contact_id: string | null;
|
|
13
|
+
session_key: string | null;
|
|
14
|
+
currency: string;
|
|
15
|
+
is_current: boolean;
|
|
16
|
+
item_count: number;
|
|
17
|
+
subtotal: number | string;
|
|
18
|
+
updated_at: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Line item row of the carts app. */
|
|
22
|
+
interface ApiCartLine {
|
|
23
|
+
id: string;
|
|
24
|
+
type: string;
|
|
25
|
+
product_id: string | null;
|
|
26
|
+
sku: string | null;
|
|
27
|
+
name: string;
|
|
28
|
+
quantity: number | string;
|
|
29
|
+
unit: string | null;
|
|
30
|
+
unit_price: number | string;
|
|
31
|
+
tax_rate: number | string;
|
|
32
|
+
snapshot: Record<string, unknown> | null;
|
|
33
|
+
configuration: Record<string, unknown> | null;
|
|
34
|
+
position: number;
|
|
35
|
+
metadata: Record<string, unknown> | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CartOwner {
|
|
39
|
+
contactId: string | null;
|
|
40
|
+
sessionKey: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
44
|
+
const DEFAULT_CART_NAME = "Mein Warenkorb";
|
|
45
|
+
|
|
46
|
+
/** The request's cart owner: the logged-in contact, else the guest session. */
|
|
47
|
+
function resolveOwner(event: H3Event): CartOwner {
|
|
48
|
+
let contactId: string | null = null;
|
|
49
|
+
const raw = getCookie(event, SESSION_COOKIE_NAME);
|
|
50
|
+
if (raw) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw) as StoredSession;
|
|
53
|
+
contactId = parsed.contactId ?? null;
|
|
54
|
+
}
|
|
55
|
+
catch { /* guest */ }
|
|
56
|
+
}
|
|
57
|
+
return { contactId, sessionKey: getOrCreateCartSessionId(event) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ownerQuery(owner: CartOwner): Record<string, string> {
|
|
61
|
+
return owner.contactId ? { contact_id: owner.contactId } : { session_key: owner.sessionKey };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Storefront CartItem → carts-app line. The snapshot is the loose product copy. */
|
|
65
|
+
function toApiLine(item: CartItem, position: number): Record<string, unknown> {
|
|
66
|
+
return {
|
|
67
|
+
type: "product",
|
|
68
|
+
product_id: UUID.test(item.id) ? item.id : null,
|
|
69
|
+
sku: item.sku,
|
|
70
|
+
name: item.name,
|
|
71
|
+
quantity: item.quantity,
|
|
72
|
+
unit: item.unit ?? null,
|
|
73
|
+
unit_price: item.price,
|
|
74
|
+
tax_rate: item.taxRate ?? 0,
|
|
75
|
+
position,
|
|
76
|
+
snapshot: {
|
|
77
|
+
id: item.id,
|
|
78
|
+
image: item.image,
|
|
79
|
+
...(item.name2 !== undefined ? { name2: item.name2 } : {}),
|
|
80
|
+
...(item.priceTiers !== undefined ? { priceTiers: item.priceTiers } : {}),
|
|
81
|
+
...(item.maxOrderQuantity !== undefined ? { maxOrderQuantity: item.maxOrderQuantity } : {}),
|
|
82
|
+
},
|
|
83
|
+
metadata: {
|
|
84
|
+
...(item.lineKey !== undefined ? { lineKey: item.lineKey } : {}),
|
|
85
|
+
...(item.customSku !== undefined ? { customSku: item.customSku } : {}),
|
|
86
|
+
...(item.costCenterId !== undefined ? { costCenterId: item.costCenterId } : {}),
|
|
87
|
+
...(item.positionTexts !== undefined ? { positionTexts: item.positionTexts } : {}),
|
|
88
|
+
...(item.nonCatalog !== undefined ? { nonCatalog: item.nonCatalog } : {}),
|
|
89
|
+
...(item.requisitionId !== undefined ? { requisitionId: item.requisitionId } : {}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** carts-app line → storefront CartItem (snapshot/metadata restore the extras). */
|
|
95
|
+
function toCartItem(line: ApiCartLine): CartItem {
|
|
96
|
+
// The snapshot/metadata blobs were written by toApiLine — restoring the
|
|
97
|
+
// original field types is safe by construction.
|
|
98
|
+
const snapshot = (line.snapshot ?? {}) as Partial<CartItem> & { id?: string; image?: string };
|
|
99
|
+
const metadata = (line.metadata ?? {}) as Partial<CartItem>;
|
|
100
|
+
return {
|
|
101
|
+
id: String(snapshot.id ?? line.product_id ?? line.sku ?? line.id),
|
|
102
|
+
name: line.name,
|
|
103
|
+
sku: line.sku ?? "",
|
|
104
|
+
image: String(snapshot.image ?? ""),
|
|
105
|
+
price: Number(line.unit_price),
|
|
106
|
+
quantity: Number(line.quantity),
|
|
107
|
+
...(line.unit ? { unit: line.unit } : {}),
|
|
108
|
+
taxRate: Number(line.tax_rate),
|
|
109
|
+
...(snapshot.name2 !== undefined ? { name2: snapshot.name2 } : {}),
|
|
110
|
+
...(snapshot.priceTiers !== undefined ? { priceTiers: snapshot.priceTiers } : {}),
|
|
111
|
+
...(snapshot.maxOrderQuantity !== undefined ? { maxOrderQuantity: snapshot.maxOrderQuantity } : {}),
|
|
112
|
+
...(metadata.lineKey !== undefined ? { lineKey: metadata.lineKey } : {}),
|
|
113
|
+
...(metadata.customSku !== undefined ? { customSku: metadata.customSku } : {}),
|
|
114
|
+
...(metadata.costCenterId !== undefined ? { costCenterId: metadata.costCenterId } : {}),
|
|
115
|
+
...(metadata.positionTexts !== undefined ? { positionTexts: metadata.positionTexts } : {}),
|
|
116
|
+
...(metadata.nonCatalog !== undefined ? { nonCatalog: metadata.nonCatalog } : {}),
|
|
117
|
+
...(metadata.requisitionId !== undefined ? { requisitionId: metadata.requisitionId } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toSummary(cart: ApiCart): CartSummaryRow {
|
|
122
|
+
return {
|
|
123
|
+
id: cart.id,
|
|
124
|
+
name: cart.name,
|
|
125
|
+
itemCount: Number(cart.item_count),
|
|
126
|
+
positions: Number(cart.item_count), // distinct lines are not denormalized — itemCount is the honest figure
|
|
127
|
+
value: Number(cart.subtotal),
|
|
128
|
+
updatedAt: cart.updated_at,
|
|
129
|
+
active: cart.is_current,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Live carts via the public revenexx API (carts app). The active cart of
|
|
135
|
+
* the owner (guest session or logged-in contact) carries the storefront
|
|
136
|
+
* cart; any number of further named carts is managed server-side.
|
|
137
|
+
*/
|
|
138
|
+
export class ApiCartService implements ICartService, ICartManager {
|
|
139
|
+
private async listActive(owner: CartOwner): Promise<ApiCart[]> {
|
|
140
|
+
const { items } = await useRevenexxApi().get<{ items: ApiCart[] }>("/v1/carts", {
|
|
141
|
+
...ownerQuery(owner),
|
|
142
|
+
status: "active",
|
|
143
|
+
limit: 100,
|
|
144
|
+
order: "updated_at.desc",
|
|
145
|
+
});
|
|
146
|
+
return items;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async ensureActiveCart(owner: CartOwner): Promise<ApiCart> {
|
|
150
|
+
const carts = await this.listActive(owner);
|
|
151
|
+
const current = carts.find(c => c.is_current) ?? carts[0];
|
|
152
|
+
if (current) {
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
return useRevenexxApi().post<ApiCart>("/v1/carts", {
|
|
156
|
+
name: DEFAULT_CART_NAME,
|
|
157
|
+
...(owner.contactId ? { contact_id: owner.contactId } : { session_key: owner.sessionKey }),
|
|
158
|
+
is_current: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- ICartService (the active cart) --------------------------------
|
|
163
|
+
|
|
164
|
+
async getItems(event: H3Event): Promise<CartItem[]> {
|
|
165
|
+
const owner = resolveOwner(event);
|
|
166
|
+
const carts = await this.listActive(owner);
|
|
167
|
+
const current = carts.find(c => c.is_current) ?? carts[0];
|
|
168
|
+
if (!current) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const { items } = await useRevenexxApi().get<{ items: ApiCartLine[] }>(`/v1/carts/${current.id}/items`);
|
|
172
|
+
return items.map(toCartItem);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async saveItems(event: H3Event, items: CartItem[]): Promise<void> {
|
|
176
|
+
const owner = resolveOwner(event);
|
|
177
|
+
const cart = await this.ensureActiveCart(owner);
|
|
178
|
+
await useRevenexxApi().request(`/v1/carts/${cart.id}/items`, {
|
|
179
|
+
method: "PUT",
|
|
180
|
+
body: { items: items.map((item, index) => toApiLine(item, index)) },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async clearItems(event: H3Event): Promise<void> {
|
|
185
|
+
await this.saveItems(event, []);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---- ICartManager (multi-cart) --------------------------------------
|
|
189
|
+
|
|
190
|
+
async listCarts(event: H3Event): Promise<{ carts: CartSummaryRow[]; activeCartId: string | null }> {
|
|
191
|
+
const owner = resolveOwner(event);
|
|
192
|
+
const carts = await this.listActive(owner);
|
|
193
|
+
const active = carts.find(c => c.is_current) ?? carts[0] ?? null;
|
|
194
|
+
return {
|
|
195
|
+
carts: carts.map(c => ({ ...toSummary(c), active: c.id === active?.id })),
|
|
196
|
+
activeCartId: active?.id ?? null,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async createCart(event: H3Event, name: string, activate: boolean): Promise<CartSummaryRow> {
|
|
201
|
+
const owner = resolveOwner(event);
|
|
202
|
+
const cart = await useRevenexxApi().post<ApiCart>("/v1/carts", {
|
|
203
|
+
name: name.trim() || DEFAULT_CART_NAME,
|
|
204
|
+
...(owner.contactId ? { contact_id: owner.contactId } : { session_key: owner.sessionKey }),
|
|
205
|
+
is_current: activate,
|
|
206
|
+
});
|
|
207
|
+
return { ...toSummary(cart), active: activate };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async renameCart(event: H3Event, id: string, name: string): Promise<void> {
|
|
211
|
+
await useRevenexxApi().put(`/v1/carts/${id}`, { name: name.trim() });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async deleteCart(event: H3Event, id: string): Promise<void> {
|
|
215
|
+
await useRevenexxApi().request(`/v1/carts/${id}`, { method: "DELETE" });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async activateCart(event: H3Event, id: string): Promise<CartItem[]> {
|
|
219
|
+
await useRevenexxApi().post(`/v1/carts/${id}/activate`, {});
|
|
220
|
+
const { items } = await useRevenexxApi().get<{ items: ApiCartLine[] }>(`/v1/carts/${id}/items`);
|
|
221
|
+
return items.map(toCartItem);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async addItemToCart(event: H3Event, id: string, item: CartItem): Promise<void> {
|
|
225
|
+
await useRevenexxApi().post(`/v1/carts/${id}/items`, toApiLine(item, 0));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async orderActiveCart(event: H3Event, orderRef: string): Promise<string | null> {
|
|
229
|
+
const owner = resolveOwner(event);
|
|
230
|
+
const carts = await this.listActive(owner);
|
|
231
|
+
const current = carts.find(c => c.is_current) ?? carts[0];
|
|
232
|
+
if (!current) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
await useRevenexxApi().post(`/v1/carts/${current.id}/order`, { order_ref: orderRef });
|
|
236
|
+
return current.id;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async claimSession(event: H3Event, contactId: string): Promise<void> {
|
|
240
|
+
const sessionKey = getCookie(event, "cover-cart-session");
|
|
241
|
+
if (!sessionKey) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Merge into the customer's current cart when one exists — otherwise
|
|
245
|
+
// the session cart is adopted as the customer's cart.
|
|
246
|
+
const customerCarts = await this.listActive({ contactId, sessionKey: "" });
|
|
247
|
+
const target = customerCarts.find(c => c.is_current) ?? customerCarts[0];
|
|
248
|
+
await useRevenexxApi().post("/v1/carts/claim", {
|
|
249
|
+
session_key: sessionKey,
|
|
250
|
+
contact_id: contactId,
|
|
251
|
+
...(target ? { target_cart_id: target.id } : {}),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProductCategory } from "../../app/config/navigation";
|
|
2
|
+
import type { ICategoryService } from "../interfaces/category";
|
|
3
|
+
import type { Locale } from "../utils/locale";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live categories via the public revenexx API — the products app's category
|
|
7
|
+
* tree, flattened rows turned into the storefront's two-level navigation
|
|
8
|
+
* (see liveCatalog.ts for the tree building and caching).
|
|
9
|
+
*/
|
|
10
|
+
export class ApiCategoryService implements ICategoryService {
|
|
11
|
+
async listCategories(locale: Locale = "de"): Promise<ProductCategory[]> {
|
|
12
|
+
return getLiveCategories(locale);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async getCategoryBySlug(slug: string, locale: Locale = "de"): Promise<ProductCategory | null> {
|
|
16
|
+
const categories = await this.listCategories(locale);
|
|
17
|
+
const direct = categories.find(c => c.slug === slug);
|
|
18
|
+
if (direct) {
|
|
19
|
+
return direct;
|
|
20
|
+
}
|
|
21
|
+
// Subcategory slugs resolve to their parent so breadcrumbs still work.
|
|
22
|
+
const parent = categories.find(c => c.subcategories.some(s => s.slug === slug));
|
|
23
|
+
return parent ?? null;
|
|
24
|
+
}
|
|
25
|
+
}
|