@wallfar/ocd-studio-core-sdk
Version:
Helper SDK for our OneClick Studio modules
844 lines (835 loc) • 30.9 kB
JavaScript
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 };