Home
Wiki
Home
Wiki
  1. 2. Components
  • Back to home
  • 1. Themes
  • Vs Code
    • Getting Started
  • Kitchenware
    • Layout
      • New Layout
      • Legacy Layout
    • Components
      • Announcement
      • Banner Carousel
      • Banner With Products Carousel
      • Blog Category List
      • Blog List
      • Brand List
      • Brands Carousel
      • Breadcrumb
      • Call To Action
      • Cart
      • Categories List
      • Change Password
      • Checkout
      • Cookie Manager
      • Filter list
      • Footer
      • Forgot Password
      • Form
      • Hero Carousel
      • Icon Block
      • Invitation
      • Last Visited Products
      • Layout
      • Login
      • Map
      • Nav Bar
      • Offer
      • Product Attachments
      • Product Attributes
      • Product Documentation
      • Product Expected
      • Product Modal
      • Products Block
      • Products Carousel
      • Product Single
      • Profile
      • Quote
      • Register
      • Related Products
      • Search
      • Stores
      • Subscribe Newsletter
      • Text with Image
      • Top Bar
      • Video
    • Reusables
      • Getting Started
    • Assets
      • Getting Started
    • SDK
      • Products
        • _findProductsByCategory
        • _findProductsByIds
        • _findProductsByTitle
        • _findProductsByFilter
        • _findProductsByCriteria
        • _findProductsAndCalculate
        • _findProductsThenCalculate
        • _getProductAttributeSet
        • _setLastVisited
      • Categories
        • _findCategoryTreeById
        • _findCategoriesByIds
        • _findCategoryByAlias
        • _findCategoryTreeByAlias
        • _getCategoryContent
      • Collections
        • _getCollectionContent
        • _findCollectionsByIds
        • _findCollectionsByIdsThenCalculate
      • Brands
        • _getBrandContent
        • _findBrandsByIds
      • Cart
        • _addToCartMulti
        • _addToCart
        • _setCart
        • _clearCart
        • _setCartListener
        • _removeFromCart
        • _calculateCart
      • Checkout
        • _startCheckout
        • _updateCheckout
        • _completeCheckout
      • Shopping Lists
        • _getShoppingLists
        • _updateShoppingList
        • _createShoppingList
        • _deleteShoppingList
        • _getShoppingListByAlias
      • Navigation
        • _getFooterMenu
        • _getHeaderMenu
      • Users
        • _getUserById
      • Utils
        • _calculateCurrency
        • _getCurrencySymbol
        • _getCulture
        • _subscribeToNewsletter
        • _findUnitsByIds
  • Noir
    • 0. Introduction
    • 6. FAQ
    • 1. Structure
      • _Overview
      • LayoutA.liquid
      • ComponentsList.liquid
      • Metas.liquid
      • CssVariables.liquid
      • Json.liquid
      • GoogleTagManager.liquid
      • StagingButton.liquid
    • 2. Components
      • _Overview
      • Announcement
      • BannerCarousel
      • BlogCategoryList
      • BlogList
      • BrandList
      • Breadcrumb
      • Cart
      • CategoriesList
      • ChangePassword
      • Checkout
      • CookieManager
      • FilterList
      • Footer
      • ForgotPassword
      • Form
      • IconBlock
      • Invitation
      • LastVisitedProducts
      • Login
      • Map
      • NavBar
      • ProductAttachments
      • ProductAttributes
      • ProductComparison
      • ProductDocumentation
      • ProductMixList
      • ProductsBlock
      • ProductsCarousel
      • ProductSingle
      • Profile
      • Quote
      • Register
      • RelatedProducts
      • SingleBlog
      • Stores
      • TextWithImage
      • ThankYouPage
      • TopBar
      • Wishlist
      • Offer
    • 3. Reusables
      • _Overview
      • Addresses
      • AddressForm
      • AnnouncementModal
      • BackToTop
      • BillingRetail
      • Company
      • General
      • Login
      • LoginModal
      • MonthlyTransactions
      • Orders
      • Payment
      • ProductAttachments
      • ProductAttributes
      • ProductComparisonButton
      • ProductComparisonFloatingButton
      • ProductGridItem
      • ProductListItem
      • ProductModal
      • ProfileInfo
      • PromptModal
      • Quotes
      • Register
      • Services
      • Shipping
      • ShoppingLists
      • ShoppingListsButton
      • ShoppingListsNavbar
      • Toast
      • Transactions
      • Users
      • VariantContent
      • WishlistButton
    • 4. Assets
      • Fonts
      • Images
      • Templates
      • Javascript
        • _Overview
        • theme.js
      • Css - Scss
        • _Overview
        • ThemeClasses
    • 5. SDK
      • _Overview
      • LiquidGlobals
      • ServicesSDK
  1. 2. Components

Offer

Purpose#

The Offer component renders the offer (quotation) page UI, including:
Offer line items (with quantity, margin, and sales-price editing, plus line removal)
Live pricing recalculation (line value, margin, service amount, totals, VAT)
Offer validation errors (e.g. product not found / quantity rules / stock issues)
Summary section (purchase value, sales value, estimated profit, final amount)
Create/save offer flow (with recipient contact details) via a modal
Order offer and reject offer flows via confirmation modals
It is resolved by the platform as Components_Offer_Default and shown on the offer page.
The component is state-driven: the layout/markup it renders depends on model.State.

Inputs (model contract)#

Model shape (storefront example)#

{
  "offerDetail": {
    "status": "Draft",
    "notes": "",
    "companyId": "Company Id",
    "offerLines": [
      {
        "lineId": "Line Id",
        "productId": "Product Id",
        "productVariantId": "Variant Id",
        "productTitle": "Sample title",
        "imageLink": "https://example.com/sample",
        "link": "product/sample",
        "quantity": 2,
        "price": 89.9,
        "priceText": "89.90 EUR",
        "originalPrice": 89.9,
        "netValue": 179.8,
        "netValueText": "179.80 EUR",
        "lineValue": 179.8,
        "lineValueText": "179.80 EUR",
        "margin": 0,
        "marginText": "0 %",
        "categoryId": "Category Id",
        "unitId": "Unit Id",
        "unitText": "Sample unit",
        "vatAmount": 17.98,
        "vatRate": 10,
        "quantityConstraints": {
          "additive": { "minimum": 1, "maximum": 9987, "step": 1, "isValid": true },
          "absolute": { "minimum": 1, "maximum": 9989, "step": 1, "isValid": true }
        }
      }
    ],
    "validation": {
      "errors": [],
      "isSuccess": true,
      "lines": [
        { "lineId": "Line Id", "isSuccess": true, "errors": [] }
      ]
    },
    "serviceAmount": 0,
    "serviceAmountText": "0.00 EUR",
    "totalProfit": 0,
    "totalProfitText": "0.00 EUR",
    "totalPrice": 1979.8,
    "totalPriceText": "1979.80 EUR",
    "totalPriceWithProfit": 1979.8,
    "totalPriceWithProfitText": "1979.80 EUR",
    "finalAmount": 1979.8,
    "finalAmountText": "1979.80 EUR",
    "vatAmount": 217.48,
    "vatAmountText": "217.48 EUR"
  },
  "state": "Create",
  "name": "Offer",
  "view": "Default",
  "section": "SectionA",
  "settings": {
    "id": "Component Id",
    "section": "SectionA",
    "type": "NoirOffer",
    "name": "Offer",
    "configuredInContentApi": true,
    "view": "Default",
    "displayName": "",
    "cssClass": ""
  },
  "translations": {
    "backToCart": "Sample translation",
    "products": "Sample translation",
    "price": "Sample translation",
    "...": "..."
  }
}

Required fields#

settings.id
Used for the wrapper id: comp-{{ id }}.

Optional fields#

settings.cssClass
Applied to the wrapper only when non-empty and not (UNDEFINED).
state
Read in Liquid as model.State. Controls which layout is rendered:
Create / Convert — editable offer (create a new offer or convert one from the cart).
Get — read-only view of an existing offer.
offerDetail
Read in Liquid as model.OfferDetail. The main payload: offerLines, validation, totals, and (for existing offers) status, contact, title.
translations
Read in Liquid as model.Translations. Localized labels and modal/error messages.

Template behavior (Liquid + Alpine)#

The component initializes via:
x-data='offerdefault.initComponent({{ modelData | serialize | escape }}, "{{ updateOfferErrorMessage }}")'
The Liquid pre-builds escaped JSON translation payloads (for the create, order, and reject modals) and passes them to the matching handlers.
State-based rendering:
Create / Convert — renders the editable table (quantity/margin/sales-price inputs, remove buttons), the summary box with an editable service amount, and the create/save action.
Get — renders the read-only offer; some actions (edit/reject/order) are gated by $store.offer.offerData.status (e.g. only Draft offers can be edited).
Edge cases:
Empty offer (offerLines is empty) shows a "no products" message with a link back.
When offerDetail.validation.isSuccess is false, grouped validation error blocks are shown and editing/submitting is guarded.

JavaScript#

Global object#

The Offer component exposes a global object:
It is initialized from Liquid via Alpine:
x-data='offerdefault.initComponent(..., "...")'
initComponent(...) seeds Alpine.store("offer").offerData with the server OfferDetail, parses validation, and returns the Alpine state + handlers.

initComponent#

Factory that returns the Alpine state + handlers for the Offer UI.
Holds local UI state (firstLoading, isLoading, offerValidation, debounce/cancel maps).
Treats Alpine.store("offer").offerData as the source of truth for lines and totals.

init#

Runs on Alpine mount.
Sets Alpine.store("offer").offerData = data and parses validation via parseOfferValidation(...).
Measures sticky offsets and wires resize / scroll listeners, then clears firstLoading.

parseOfferValidation#

Builds a map of validation errors grouped by error type from offerData.validation.lines.

itemInvalid#

Returns whether a given line id belongs to a failed validation line.

updateHeaderHeight#

Stores the current header height in this.headerHeight so sticky elements can be positioned correctly.

formatDate#

Formats a date value (default format DD/MM/YYYY).

updateStickyElementTop#

Applies the stored header height as the top offset on the sticky summary element.

sendSelectEvent#

Sends a GA selectItem event for a clicked product.

removeItem#

Removes an offer line by lineId from Alpine.store("offer").offerData.offerLines, then triggers a full pricing recalculation.

getOfferLineKey#

Returns a stable key for an offer line (id, falling back to lineId, then productVariantId).

debounceUpdate#

Debounces updateOfferPricing(...) per line (default 800ms) and cancels any pending request for the same line.

updateOfferPricing#

Recalculates pricing (a single line or the whole offer) via servicesreusabledefault.updateOfferPricing(...).
Supports debounced calls and per-line axios cancel tokens.
Updates Alpine.store("offer").offerData with the response; shows a toast on failure.

increaseQuantity#

Increases a line's quantity by quantityConstraints.absolute.step (clamped to maximum) and triggers a debounced pricing update.

decreaseQuantity#

Decreases a line's quantity by quantityConstraints.absolute.step (clamped to minimum) and triggers a debounced pricing update.

updateQuantity#

Sets the line quantity from input and triggers a debounced pricing update.

updateMargin#

Parses the margin input (allows intermediate states like "", "-", trailing separators, and comma decimals), then triggers a debounced pricing update.

updateSalesPrice#

Parses the sales-price input (same intermediate-state handling as margin) and triggers a debounced pricing update.

updateServiceAmount#

Parses the service-amount input, stores it on Alpine.store("offer").offerData.serviceAmount, and triggers a whole-offer recalculation.

handleCreateOffer#

Opens the create/save modal (title, recipient contact, expiration date, comments) via Alpine.store("modal").
The save and send actions both delegate to createOfferAction(...) (the send variant passes sendEmail = true).

createOfferAction#

Validates the modal data and persists the offer.
Fills offerData.title, expiresAt, contact, and notes from the modal fields.
Creates a new offer via servicesreusabledefault.createOffer(...) (redirects to /offer/{id} on success) or updates an existing one via servicesreusabledefault.updateOffer(...).
Shows success/error toasts; sendEmail switches the messaging and redirect timing.

handleOrderOffer#

Opens a confirmation modal; on complete, redirects to /checkout?offer={id}.

handleRejectOffer#

Opens a confirmation modal; on confirm, calls servicesreusabledefault.rejectOffer(...), updates the store, and shows a toast.

buildPromptGenerateItemsFromOfferLines#

Builds a simplified items array (title, mpn, price) from the current offer lines, used for prompt generation.

Global Alpine stores (used by Offer)#

Alpine.store("offer")#

The single source of truth for the offer (offerData: lines, totals, contact, status).
Seeded by initComponent(...) and updated by pricing/create/update/reject flows.
Templates read it via $store.offer (e.g. $store.offer.offerData.offerLines).

Alpine.store("toast")#

Used for success/error feedback.
Alpine.store("toast").removeAll();
Alpine.store("toast").add(message, "ic-warning", "error");

Alpine.store("modal")#

Drives the create, order, and reject confirmation dialogs (uses shouldReinitialize, open({...}), and close()).

Services / API calls#

The Offer component uses servicesreusabledefault for all offer operations:
servicesreusabledefault.updateOfferPricing — recalculates offer pricing (line or whole offer).
servicesreusabledefault.createOffer — creates a new offer (optionally emails the recipient).
servicesreusabledefault.updateOffer — updates an existing offer.
servicesreusabledefault.rejectOffer — rejects/cancels an offer.

Dependencies#

Alpine.js
Alpine stores: offer, toast, modal
Services layer (servicesreusabledefault) for the offer API calls listed above
Prompt modal validation via promptmodalreusabledefault.isDataValid(...) (used before submitting modal data)

Notes#

Pricing is server-prepared: editable fields (quantity/margin/sales price/service amount) are sent to the backend, which returns recalculated totals that replace offerData.
Numeric inputs accept intermediate typing states (empty, -, trailing separators, comma decimals) and only recalculate once a valid number is parsed.
In Get state, available actions depend on offerData.status (e.g. only Draft offers expose edit/reject).
Modified at 2026-06-12 13:55:25
Previous
Wishlist
Next
_Overview
Built with