Components_Offer_Default and shown on the offer page.model.State.{
"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",
"...": "..."
}
}settings.idcomp-{{ id }}.settings.cssClass(UNDEFINED).statemodel.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.offerDetailmodel.OfferDetail. The main payload: offerLines, validation, totals, and (for existing offers) status, contact, title.translationsmodel.Translations. Localized labels and modal/error messages.x-data='offerdefault.initComponent({{ modelData | serialize | escape }}, "{{ updateOfferErrorMessage }}")'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).offerLines is empty) shows a "no products" message with a link back.offerDetail.validation.isSuccess is false, grouped validation error blocks are shown and editing/submitting is guarded.x-data='offerdefault.initComponent(..., "...")'initComponent(...) seeds Alpine.store("offer").offerData with the server OfferDetail, parses validation, and returns the Alpine state + handlers.firstLoading, isLoading, offerValidation, debounce/cancel maps).Alpine.store("offer").offerData as the source of truth for lines and totals.Alpine.store("offer").offerData = data and parses validation via parseOfferValidation(...).resize / scroll listeners, then clears firstLoading.offerData.validation.lines.this.headerHeight so sticky elements can be positioned correctly.DD/MM/YYYY).top offset on the sticky summary element.selectItem event for a clicked product.lineId from Alpine.store("offer").offerData.offerLines, then triggers a full pricing recalculation.id, falling back to lineId, then productVariantId).updateOfferPricing(...) per line (default 800ms) and cancels any pending request for the same line.servicesreusabledefault.updateOfferPricing(...).Alpine.store("offer").offerData with the response; shows a toast on failure.quantityConstraints.absolute.step (clamped to maximum) and triggers a debounced pricing update.quantityConstraints.absolute.step (clamped to minimum) and triggers a debounced pricing update."", "-", trailing separators, and comma decimals), then triggers a debounced pricing update.Alpine.store("offer").offerData.serviceAmount, and triggers a whole-offer recalculation.Alpine.store("modal").createOfferAction(...) (the send variant passes sendEmail = true).offerData.title, expiresAt, contact, and notes from the modal fields.servicesreusabledefault.createOffer(...) (redirects to /offer/{id} on success) or updates an existing one via servicesreusabledefault.updateOffer(...).sendEmail switches the messaging and redirect timing./checkout?offer={id}.servicesreusabledefault.rejectOffer(...), updates the store, and shows a toast.title, mpn, price) from the current offer lines, used for prompt generation.offerData: lines, totals, contact, status).initComponent(...) and updated by pricing/create/update/reject flows.$store.offer (e.g. $store.offer.offerData.offerLines).Alpine.store("toast").removeAll();
Alpine.store("toast").add(message, "ic-warning", "error");shouldReinitialize, open({...}), and close()).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.offer, toast, modalservicesreusabledefault) for the offer API calls listed abovepromptmodalreusabledefault.isDataValid(...) (used before submitting modal data)offerData.-, trailing separators, comma decimals) and only recalculate once a valid number is parsed.Get state, available actions depend on offerData.status (e.g. only Draft offers expose edit/reject).