UNPKG

@wallfar/ocd-studio-core-sdk

Version:

Helper SDK for our OneClick Studio modules

844 lines (835 loc) 30.9 kB
import { reactive, ref, watch, toRefs, computed, unref, onMounted, onBeforeUnmount } from 'vue'; import { useHead, useAsyncData } from 'nuxt/app'; import { getDoc, doc, getDocs, query, collection, where, limit, orderBy, startAfter } from 'firebase/firestore'; import { v as validateAddress } from './shared/ocd-studio-core-sdk.ChPIrm-j.mjs'; import { toCalendarDate, today, getLocalTimeZone, CalendarDate } from '@internationalized/date'; async function fetchProductFirebase(config, slug) { const { db, products: products_collection } = config; const q = query( collection(db, products_collection), where("slug", "==", slug), limit(1) ); const snapshot = await getDocs(q); if (snapshot.empty) return null; const doc2 = snapshot.docs[0]; return { id: doc2.id, ...doc2.data() }; } async function fetchProductsFirebase(config, collectionId, sort, lastFetchedItemId, itemsPerPage) { const { db, products: products_collection } = config; let q = query(collection(db, products_collection), where("status", "==", "published"), limit(itemsPerPage || 12)); if (collectionId) { q = query(q, where("collections", "array-contains", collectionId)); } if (sort) { switch (sort) { case "created-descending": q = query(q, orderBy("createdAt", "desc")); break; case "created-ascending": q = query(q, orderBy("createdAt", "asc")); break; case "price-descending": q = query(q, orderBy("price", "desc")); break; case "price-ascending": q = query(q, orderBy("price", "asc")); break; case "title-descending": q = query(q, orderBy("title", "desc")); break; case "title-ascending": q = query(q, orderBy("title", "asc")); break; } if (lastFetchedItemId) { q = query(q, startAfter(lastFetchedItemId)); } } const snapshot = await getDocs(q); if (snapshot.empty) return null; const docs = snapshot.docs; return docs.map((doc2) => ({ id: doc2.id, ...doc2.data() })); } async function fetchCollectionsFirebase(config) { const { db, collections: collections_collection } = config; if (!collections_collection) return null; if (collections_collection.includes(":")) { const collectionParts = collections_collection.split(":"); const docSnap = await getDoc(doc(db, collectionParts[0])); const collections = docSnap.exists() ? docSnap.data()?.[collectionParts[1]] : null; return collections?.filter((collection2) => collection2.status === "published") || null; } else { const snapshot = await getDocs(query(collection(db, collections_collection))); return !snapshot.empty ? snapshot.docs.map((doc2) => ({ id: doc2.id, ...doc2.data() })).filter((collection2) => collection2.status === "published") : null; } } async function fetchShippingOptionsFirebase(config) { const { db, shippingOptions: shipping_options_collection } = config; if (!shipping_options_collection) return null; if (shipping_options_collection.includes(":")) { const collectionParts = shipping_options_collection.split(":"); const docSnap = await getDoc(doc(db, collectionParts[0])); const options = docSnap.exists() ? docSnap.data()?.[collectionParts[1]] : null; return options?.filter((opt) => opt.status === "published") || null; } else { const snapshot = await getDocs(query(collection(db, shipping_options_collection))); return !snapshot.empty ? snapshot.docs.map((doc2) => ({ id: doc2.id, ...doc2.data() })).filter((opt) => opt.status === "published") : null; } } function createProductConfigurator(product) { const selection = reactive( product.options?.reduce((acc, o) => { acc[o.name] = o.values[0]?.value || ""; return acc; }, {}) ); function getSelectedVariant() { if (product.variants?.length === 0) return _getMainProductAsVariant(); return getVariant(selection); } function getVariant(variant) { if (product.variants?.length === 0) return _getMainProductAsVariant(); return product.variants?.find( (v) => v.options.every((o) => o.value === variant[o.option]) ); } function _getMainProductAsVariant() { return { options: [], combinedOptions: "", stock: product.stock, price: product.price, image: product.media?.[0], disabled: false, name: product.title }; } return { selection, getSelectedVariant, getVariant }; } class Webshop { config; cart = ref([]); taxRate = ref(0); _shippingOptions = ref(null); _shippingOption = ref(null); _activePromoCode = ref(null); _collections = ref(null); _deliveryAddress = ref({ firstName: "", lastName: "", company: "", streetLine1: "", streetLine2: "", streetLine3: "", city: "", stateOrProvince: "", countyOrDistrict: "", postalCode: "", country: "", phoneNumber: "", email: "", instructions: "", latitude: 0, longitude: 0 }); constructor(config) { this.config = config; if (!this.config.defaultBrand) this.config.defaultBrand = ""; if (!this.config.defaultCurrency) this.config.defaultCurrency = "USD"; if (!this.config.defaultLocale) this.config.defaultLocale = "en-US"; this.cart.value = localStorage.getItem("webshop_cart") ? JSON.parse(localStorage.getItem("webshop_cart") || "[]") : []; this._activePromoCode.value = localStorage.getItem("webshop_promoCode") ? JSON.parse(localStorage.getItem("webshop_promoCode") || "null") : null; this._shippingOption.value = localStorage.getItem("webshop_shippingOption") ? JSON.parse(localStorage.getItem("webshop_shippingOption") || "null") : null; const saved = localStorage.getItem("webshop_deliveryAddress"); if (saved) { try { const data = JSON.parse(saved); Object.assign(this._deliveryAddress.value, data); } catch (e) { console.error("Could not parse saved address:", e); } } watch(this.cart, () => { localStorage.setItem("webshop_cart", JSON.stringify(this.cart.value)); }, { deep: true }); watch(this._activePromoCode, () => { localStorage.setItem("webshop_promoCode", JSON.stringify(this._activePromoCode.value)); }, { deep: true }); watch(this._deliveryAddress, () => { localStorage.setItem("webshop_deliveryAddress", JSON.stringify(this._deliveryAddress.value)); }, { deep: true }); watch(this._shippingOption, () => { localStorage.setItem("webshop_shippingOption", JSON.stringify(this._shippingOption.value)); }, { deep: true }); this.refreshCollections(); this.refreshShippingOptions(); } async refreshCollections() { if (this.config.provider === "firebase") { this._collections.value = await fetchCollectionsFirebase(this.config.firebase); return this._collections.value; } throw new Error(`Provider ${this.config.provider} not supported.`); } async refreshShippingOptions() { if (this.config.provider === "firebase") { this._shippingOptions.value = await fetchShippingOptionsFirebase(this.config.firebase); return this._shippingOptions.value; } throw new Error(`Provider ${this.config.provider} not supported.`); } async fetchProduct(slug) { if (this.config.provider === "firebase") { return fetchProductFirebase(this.config.firebase, slug); } throw new Error(`Provider ${this.config.provider} not supported.`); } /** * Sends a notification to a user. * * @param product: Product created in the Products module from whitelabel core modules package. */ setProductSeo(product) { if (!product) { throw new Error("Product is required"); } useHead({ title: product.title, meta: [ { name: "description", content: product.description }, { property: "og:title", content: product.title }, { property: "og:description", content: product.description }, { property: "og:image", content: product.media[0] }, { property: "og:url", content: `https://yourdomain.com/products/${product.slug}` }, { property: "og:type", content: "product" }, { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:title", content: product.title }, { name: "twitter:description", content: product.description }, { name: "twitter:image", content: product.media[0] } ], script: [ { type: "application/ld+json", innerHTML: JSON.stringify({ "@context": "https://schema.org/", "@type": "Product", name: product.title, image: product.media[0], description: product.description, sku: product.sku, mpn: product.mpn, gtin13: product.gtin, brand: { "@type": "Brand", name: product.brand || this.config.defaultBrand }, offers: { "@type": "Offer", url: `https://yourdomain.com/products/${product.slug}`, priceCurrency: product.currency || "EUR", price: product.price, availability: product.stock > 0 ? "https://schema.org/InStock" : "https://schema.org/OutOfStock", itemCondition: "https://schema.org/NewCondition" } }) } ] }); } createConfigurator(product) { return createProductConfigurator(product); } async addToCart(product, variant, quantity = 1) { const requiredKeys = ["id", "title", "slug", "price", "currency", "media"]; const missing = requiredKeys.filter((key) => !product[key]); if (missing.length) { throw new Error(`Product is missing required properties: ${missing.join(", ")}`); } const index = this.cart.value.findIndex( (i) => i.productId === product.id && deepEqual(i.variant, variant) ); const configurator = this.createConfigurator(product); const selectedVariant = configurator.getVariant(variant || {}); console.log("index", index); if (index !== -1) { this.cart.value[index].quantity += quantity; } else { let newProd = JSON.parse(JSON.stringify({ productId: product.id, title: product.title, slug: product.slug, thumbnail: selectedVariant?.image || product.media[0], price: selectedVariant?.price || product.price, currency: product.currency, stock: selectedVariant?.stock || product.stock, quantity, variant: JSON.parse(JSON.stringify(variant)) })); this.cart.value.push(newProd); } } removeFromCart(productId, variant) { this.cart.value = this.cart.value.filter( (i) => !(i.productId === productId && JSON.stringify(i.variant) === JSON.stringify(variant)) ); } decreaseItem(productId, variant) { const idx = this.cart.value.findIndex( (i) => i.productId === productId && JSON.stringify(i.variant) === JSON.stringify(variant) ); if (idx !== -1) { if (this.cart.value[idx].quantity > 1) { this.cart.value[idx].quantity -= 1; } else { this.cart.value.splice(idx, 1); } } } updateQuantity(productId, variant, newQty) { const idx = this.cart.value.findIndex( (i) => i.productId === productId && JSON.stringify(i.variant) === JSON.stringify(variant) ); if (idx !== -1) { if (newQty <= 0) { this.cart.value.splice(idx, 1); } else { this.cart.value[idx].quantity = newQty; } } } async validatePromoCode(code) { try { if (this.config.promoCodeValidationRequest) { return await this.config.promoCodeValidationRequest(code); } } catch (err) { return { valid: false, error: "Code is invalid" }; } } updateDeliveryAddress(updates) { Object.assign(this._deliveryAddress.value, updates); } // 💡 Accessors get cartItems() { return this.cart.value; } get activePromoCode() { return this._activePromoCode.value; } get collections() { return this._collections; } get itemCount() { return this.cart.value.reduce((sum, item) => sum + item.quantity, 0); } get subtotal() { return this.cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0); } get deliveryAddress() { return toRefs(this._deliveryAddress.value); } get shippingOptions() { if (!this._shippingOptions?.value || this._shippingOptions.value.length === 0) return []; return this._shippingOptions.value.reduce((options, option) => { if (!option) return options; if (option.status !== "published") return options; const validCountry = !option.countries || option.countries.length === 0 || option.countries.includes(this.country); if (!validCountry) return options; let validCondition = option.condition === "always"; if (!validCondition) { if (option.condition === "price_based") { const MIN = option.condition_min || 0; const MAX = option.condition_max || Infinity; validCondition = this.subtotal >= MIN && this.subtotal <= MAX; } else if (option.condition === "weight_based") ; } if (validCountry && validCondition) { options.push(option); } return options; }, []); } get shippingOption() { return this._shippingOption.value; } set shippingOption(option) { this._shippingOption.value = option; } get shippingFee() { if (this._activePromoCode.value?.type === "free_shipping") { return 0; } if (this._shippingOption.value) { return this._shippingOptions.value?.find((opt) => opt.id === this._shippingOption.value)?.price || 0; } return 0; } get discount() { if (!this._activePromoCode.value) return 0; if (this._activePromoCode.value.type === "percentage") { return this.subtotal * ((this._activePromoCode.value.value || 0) / 100) || 0; } if (this._activePromoCode.value.type === "fixed") { return this._activePromoCode.value.value || 0; } return 0; } get tax() { return this.taxRate.value * (this.subtotal - this.discount); } get total() { const total = this.subtotal + this.tax + this.shippingFee - this.discount; return total > 0 ? total : 0; } get country() { return this._deliveryAddress.value.country; } set country(val) { this._deliveryAddress.value.country = val; } // 💳 Controls async applyPromoCode(code) { code = code.trim().toUpperCase(); try { if (this.config.provider === "firebase") { const result = await this.validatePromoCode(code); if (!result || !result.valid || !result.promo) throw new Error("Code is invalid"); this._activePromoCode.value = result.promo; return result; } else { throw new Error(`Provider ${this.config.provider} not supported.`); } } catch (error) { throw error; } } async createPaymentLink() { try { if (!this.cart.value || this.cart.value.length === 0) throw new Error("No products in cart"); if (!this._shippingOption.value) throw new Error("No shipping option selected"); if (validateAddress(this._deliveryAddress.value).length > 0) throw new Error("Invalid address"); if (this.config.createPaymentLinkRequest) { let checkoutData = { cart: this.cart.value?.map((item) => ({ productId: item.productId, variant: item.variant, quantity: item.quantity })), shippingOption: this._shippingOption.value, activePromoCode: this._activePromoCode.value ? this._activePromoCode.value.code : null, deliveryAddress: this._deliveryAddress.value }; return await this.config.createPaymentLinkRequest(checkoutData); } } catch (err) { const inner = err.data?.message ?? (typeof err.data === "string" ? err.data : null) ?? err.message; throw new Error(inner); } } removePromoCode() { this._activePromoCode.value = null; } clearCart() { this.cart.value = []; this._activePromoCode.value = null; } // helpers formatPrice(price, currency, locale) { try { return new Intl.NumberFormat(locale || this.config.defaultLocale, { style: "currency", currency: currency || this.config.defaultCurrency }).format(price); } catch { return `${currency} ${price.toFixed(2)}`; } } // useProduct useProduct(slug) { const slugKey = computed(() => unref(slug)); const key = computed(() => `product-${slugKey.value}`); const selection = ref({}); const getSelectedVariant = ref(() => void 0); const { data, pending, error, refresh } = useAsyncData( key, async () => { const s = typeof slugKey.value === "string" ? slugKey.value : slugKey.value(); const prod = await fetchProductFirebase(this.config.firebase, s); if (!prod) { throw new Error(`Product "${s}" not found`); } const cfg = createProductConfigurator(prod); selection.value = cfg.selection; getSelectedVariant.value = cfg.getSelectedVariant; return prod; } ); const product = computed(() => data.value || null); const selectedVariant = computed(() => getSelectedVariant.value()); return { product, pending, error, refresh, selection, selectedVariant }; } // useCollection useCollection(slug) { const slugKey = computed(() => unref(slug)); const sort = ref("created-descending"); const _products = ref([]); const lastFetchedItemId = ref(""); const collection = computed(() => { const s = slugKey.value ? typeof slugKey.value === "string" ? slugKey.value : slugKey.value() : void 0; return this._collections.value?.find((c) => c.slug === s); }); const products = computed(() => _products.value); const pending = ref(false); const error = ref(null); const refresh = () => this.refreshCollections(); watch(slugKey, () => { reset(); loadProducts(); }); watch(sort, () => { reset(); loadProducts(); }); const reset = () => { _products.value = []; lastFetchedItemId.value = ""; pending.value = false; error.value = null; }; const loadProducts = async () => { pending.value = true; error.value = null; try { const prods = await fetchProductsFirebase(this.config.firebase, collection.value?.id, sort.value, lastFetchedItemId.value, this.config.itemsPerPage); _products.value = prods || []; } catch (err) { error.value = err; } finally { pending.value = false; } }; loadProducts(); return { collection, products, pending, error, refresh, sort }; } // useLocalized(product.title, 'nl') // returns Dutch title or fallback // useInventoryStatus(product) // useCheckout() // Stores cart items, Calculates total price, Prepares payload for order API // useProductFilters() // Maps query params (?color=red&size=m) into filters // useRelatedProducts() // Fetches related products based on tags, collections, manual } function deepEqual(a, b) { function isObject(o) { return o !== null && typeof o === "object"; } if (!isObject(a) || !isObject(b)) { return a === b; } const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) return false; return aKeys.every( (key) => b.hasOwnProperty(key) && deepEqual(a[key], b[key]) ); } async function fetchContentFirebase(config, settings) { try { const { db, entries: entries_collection } = config; const { slug, entryId, version, locale } = settings || {}; if (!db || !entries_collection || !slug || !locale) { console.warn("Missing Firebase configuration or settings", { db, entries_collection, slug, locale }); return null; } let content, liveByLocale; console.log("searching for live content with slug:", slug, "and locale:", locale, version, entries_collection, version); if (version === "latest") { const q = query( collection(db, entries_collection), where("liveKeys", "array-contains", slug + "_" + locale), limit(1) ); const snapshot = await getDocs(q); if (snapshot.empty) return null; content = snapshot.docs[0]; liveByLocale = content.data().liveByLocale || []; console.log("skrrr"); let versionId = content.data().liveByLocale?.find((v) => v.locale === locale)?.versionId; if (!versionId) return null; console.log("skrrrpapapa", versionId); content = await getDoc(doc(db, `${entries_collection}/${content.id}/versions/${versionId}`)); console.log("wawa", content); } else { console.log(`${entries_collection}/${entryId}/versions`, version || ""); content = await getDoc(doc(db, `${entries_collection}/${entryId}/versions`, version || "")); } if (!content.exists()) return null; content = { id: content.id, ...content.data() }; content.content = JSON.parse(content.content || "{}"); content.createdAt = content.created.at.toDate(); content.i18nParams = {}; delete content.created; liveByLocale?.forEach((v) => { content.i18nParams[v.locale] = { slug: v.slug }; }); console.log("Fetched content from Firebase:", content); return content; } catch (error) { console.error("Error fetching content from Firebase", error); return null; } } class ContentDelivery { config; constructor(config) { this.config = config; } useContent(settings) { settings.locale = settings.locale || this.config.defaultLocale || "en"; settings.version = new URLSearchParams(location.search).get("content-version") || "latest"; settings.entryId = new URLSearchParams(location.search).get("content-entry-id") || ""; const slugKey = computed(() => unref(settings.slug)); const key = computed(() => `content-${slugKey.value}_${settings.locale}_${settings.version}`); const showLivePreview = ref(false); const previewLayout = ref(null); const previewSettings = ref({}); const { data, status, pending, error, refresh } = useAsyncData( () => key.value, async () => { const s = typeof slugKey.value === "string" ? slugKey.value : slugKey.value(); const content2 = await fetchContentFirebase(this.config.firebase, settings); if (!content2) { throw new Error(`Content "${s}" not found`); } return content2; } ); const content = computed(() => data.value?.content || []); const entrySettings = computed(() => data.value?.settings || {}); const i18nParams = computed(() => data.value?.i18nParams || {}); const visibleContent = computed(() => { return showLivePreview.value ? previewLayout.value : content.value; }); const visibleSettings = computed(() => { return showLivePreview.value ? previewSettings.value : entrySettings.value; }); const onLivePreviewMessage = (ev) => { const data2 = ev.data; if (!data2 || typeof data2 !== "object") return; if (data2.type === "layout:update") { const payload = data2.payload; previewLayout.value = payload?.layout ? JSON.parse(payload?.layout) : previewLayout.value; } else if (data2.type === "settings:update") { const payload = data2.payload; previewSettings.value = payload?.settings ? JSON.parse(payload?.settings) : previewSettings.value; } }; onMounted(() => { showLivePreview.value = new URLSearchParams(location.search).get("live-editor") === "true"; if (showLivePreview.value) { window.parent.postMessage({ type: "page:loaded" }, "*"); window.addEventListener("message", onLivePreviewMessage); } }); onBeforeUnmount(() => { if (showLivePreview.value) { window.removeEventListener("message", onLivePreviewMessage); } }); return { content: visibleContent, settings: visibleSettings, i18nParams, status, pending, error, refresh }; } } async function fetchAgendaFirebase(config, id) { const { db, agendas: agendas_collection } = config; const res = await getDoc(doc(db, agendas_collection, id)); if (!res.exists()) return null; return { ...res.data() }; } async function fetchReservedSpotsFirebase(config, agendaId, dateString) { const { db, reserved_spots: reserved_spots_collection } = config; const res = await getDoc(doc(db, reserved_spots_collection, `${agendaId}-${dateString}`)); if (!res.exists()) return []; return res.data()?.reserved_spots || []; } class Booker { config; _activePromoCode = ref(null); constructor(config) { this.config = config; if (!this.config.defaultCurrency) this.config.defaultCurrency = "USD"; if (!this.config.defaultLocale) this.config.defaultLocale = "en-US"; this._activePromoCode.value = localStorage.getItem("booker_promoCode") ? JSON.parse(localStorage.getItem("booker_promoCode") || "null") : null; watch(this._activePromoCode, () => { localStorage.setItem("booker_promoCode", JSON.stringify(this._activePromoCode.value)); }, { deep: true }); } // useAppointmentBooker useAppointmentBooker(agendaId) { const agendaIdKey = computed(() => unref(agendaId)); const key = computed(() => `agenda-${agendaIdKey.value}`); const selectedDate = ref(toCalendarDate(today(getLocalTimeZone()))); const selectedHour = ref(null); const selectedAddOns = ref({}); const selectedSpots = ref(1); const _reservedSpotsForTheDay = ref([]); const { data, pending, error, refresh } = useAsyncData( key, async () => { const id = typeof agendaIdKey.value === "string" ? agendaIdKey.value : agendaIdKey.value(); const res = await fetchAgendaFirebase(this.config.firebase, id); if (!res) { throw new Error(`Agenda "${id}" not found`); } selectedDate.value = toCalendarDate(today(getLocalTimeZone())); return res; } ); watch(selectedDate, async (newDate, oldDate) => { if (!newDate || newDate == oldDate) return; fetchReservedSpotsForTheDay(); }); const agenda = computed(() => data.value || null); const customerInformationFields = computed(() => { return agenda.value?.customerInformationFields || []; }); const minimumBookingDate = computed(() => { let minDate = /* @__PURE__ */ new Date(); return new CalendarDate(minDate.getFullYear(), minDate.getMonth() + 1, minDate.getDate()); }); const maximumBookingDate = computed(() => { let daysToAdd = Math.max(Number(agenda.value?.serviceVisibility || 1), 1); let maxDate = new Date((/* @__PURE__ */ new Date()).getTime() + daysToAdd * 24 * 60 * 60 * 1e3); return new CalendarDate(maxDate.getFullYear(), maxDate.getMonth() + 1, maxDate.getDate()); }); const availableHours = computed(() => { const NORMAL_HOURS = agenda.value?.timeslots || { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: [] }; const EXCEPTIONS = agenda.value?.exceptions || []; const date = selectedDate.value; if (!date) return []; date.toString(); const TEST_TIMEZONE = getLocalTimeZone(); let timeslots = void 0; for (let i = 0; i < EXCEPTIONS.length; i++) { let ev = EXCEPTIONS[i]; if (ev.startDate == ev.endDate && ev.endDate == date?.toString() || ev.startDate != ev.endDate && new Date(ev.startDate) <= date.toDate(TEST_TIMEZONE) && date.toDate(TEST_TIMEZONE) <= new Date(ev.endDate)) { timeslots = [...ev.timeslots]; break; } } let res = timeslots ? timeslots : NORMAL_HOURS[date.toDate(TEST_TIMEZONE).getDay()]; return res?.sort((a, b) => parseFloat(a.startTime?.replace(":", ".") || "0") > parseFloat(b.startTime?.replace(":", ".") || "0") ? 1 : -1) || []; }); const availableSpots = computed(() => { if (!selectedHour.value) return 0; const MAX_SPOTS = agenda.value ? agenda.value.capacity : 0; const RESERVED = _reservedSpotsForTheDay.value?.find((r) => r.startTime === selectedHour.value.startTime && r.endTime === selectedHour.value.endTime)?.reserved || 0; return Math.max(MAX_SPOTS - RESERVED, 0); }); const availableAddOns = computed(() => { return agenda.value?.addOns || []; }); const totalPrice = computed(() => { return 0; }); const fetchReservedSpotsForTheDay = async () => { const date = selectedDate.value; if (!date) return []; _reservedSpotsForTheDay.value = await fetchReservedSpotsFirebase(this.config.firebase, agendaIdKey.value, date.toString()); console.log("Reserved spots for the day:", _reservedSpotsForTheDay.value); }; const isValidBookingDetails = computed(() => { if (!selectedDate.value) return false; if (!selectedHour.value || availableHours.value.length === 0 || !availableHours.value.find((h) => h.startTime === selectedHour.value.startTime && h.endTime === selectedHour.value.endTime)) return false; if (!availableSpots.value || !selectedSpots.value || selectedSpots.value <= 0 || selectedSpots.value > availableSpots.value) return false; return true; }); const isValidAddOns = computed(() => { return true; }); const isValidUserDetails = computed(() => { return true; }); const isValidConfirmation = computed(() => { return true; }); const toggleAddOn = (addOnId, value) => { if (selectedAddOns.value[addOnId]) { delete selectedAddOns.value[addOnId]; } else { selectedAddOns.value[addOnId] = value; } }; const clearAllAddOns = () => { selectedAddOns.value = {}; }; const selectAllAddOns = () => { const allAddOns = agenda.value?.addOns || []; const newSelectedAddOns = {}; allAddOns.forEach((addOn) => { newSelectedAddOns[addOn.id] = addOn; }); selectedAddOns.value = newSelectedAddOns; }; return { customerInformationFields, selectedDate, selectedHour, selectedAddOns, selectedSpots, minimumBookingDate, maximumBookingDate, availableHours, availableSpots, availableAddOns, totalPrice, isValidBookingDetails, isValidAddOns, isValidUserDetails, isValidConfirmation, toggleAddOn, clearAllAddOns, selectAllAddOns }; } } export { Booker, ContentDelivery, Webshop };