From 715e26a9eb219808d5c899d418e1d596f9318f61 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 22 Jun 2026 16:59:19 +0000
Subject: [PATCH] =Debugging and implementing the Checkout.js logic. Will be uploading to test integration with square next.

---
 assets/js/concise/Checkout.js |  422 ++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 307 insertions(+), 115 deletions(-)

diff --git a/assets/js/concise/Checkout.js b/assets/js/concise/Checkout.js
index e0990f0..594914e 100644
--- a/assets/js/concise/Checkout.js
+++ b/assets/js/concise/Checkout.js
@@ -21,7 +21,9 @@
 		this.checkout      = document.querySelector('aside#cart');
 		this.provider      = this.checkout?.querySelector('form')?.dataset.provider || '';
 
-		this.isOpen        = this.config.isOpen !== '1' || false;
+		this.isOpen        = this.config.isOpen === '1' || false;
+		this.isOpen = true;
+
 		this.isLoggedIn    = this.config.is_logged_in || false;
 		this.userEmail     = this.config.user_email || '';
 		this.savedCards    = [];
@@ -31,6 +33,7 @@
 
 		this.cache = new window.jvbCache('cart', { TTL: 8.64e+7 });
 		this.a11y  = window.jvbA11y;
+		this.T = window.jvbTemplates;
 
 		this.initCart();
 
@@ -38,20 +41,38 @@
 			this.initElements();
 			this.init();        // provider-specific
 			this.initListeners();
+			this.defineTemplates();
 
 			if (this.isLoggedIn) {
 				this.loadSavedCards();
 			}
 		}
 
+		if (!this.isOpen)
+		{
+			this.closeShop();
+		}
+
 		this.popup = window.jvbPopup.registerPopup({
 			popup:  this.checkout,
-			toggle: this.toggle,
+			toggle: this.ui.toggle.btn,
 			name:   'Cart',
 			onOpen: this.maybeAddEmptyState.bind(this),
+			onClose: this.toggleButton.bind(this),
 		});
 	}
 
+	closeShop() {
+		document.querySelectorAll('[data-add-to-cart]').forEach(item => {
+			item.querySelectorAll('button, input').forEach(i => {
+				i.disabled = true;
+				i.title = 'Sorry, we are currently closed.';
+			});
+
+		});
+	}
+
+
 	/*****************************************************************
 	 * ABSTRACT — subclasses must implement
 	 *****************************************************************/
@@ -81,20 +102,30 @@
 	 *****************************************************************/
 
 	async initCart() {
-		this.cartItems = await this.cache.get('cart') ?? new Map();
-		if (this.cartItems.size > 0) {
+		let stored = await this.cache.get('cart') ?? new Map();
+		if (stored.size > 0) {
+			this.cartItems = stored;
+			this.updateToggle();
 			this.notifyRestoredCart();
 		}
 	}
 
 	saveCart() {
 		this.updateTotal();
+		this.updateToggle();
 		this.cache.set('cart', this.cartItems);
 	}
 
 	clearCart() {
+		for (let [id, item] of this.cartItems.entries()) {
+			this.removeFromCart(id);
+		}
 		this.cartItems.clear();
-		window.removeChildren(this.table);
+		Array.from(this.ui.cart.table.children).forEach(child => {
+			if (child.tagName === 'TBODY') {
+				child.remove();
+			}
+		});
 		this.saveCart();
 	}
 
@@ -109,22 +140,87 @@
 	/*****************************************************************
 	 * ELEMENT SETUP
 	 *****************************************************************/
+	defineTemplates() {
+		// const checkout = this;
+		this.T.define('mainCartItem', {
+			refs: {
+				header: '.header th'
+			},
+			setup({el, refs, manyRefs, data}) {
+				refs.header.textContent = data.label;
+				el.dataset.label = data.label;
+				el.dataset.id = data.parentId;
+			}
+		});
 
+		this.T.define('childCartItem', {
+			refs: {
+				quantity: '.quantity',
+				label: 'label',
+				price: 'span.price',
+				button: '[data-remove-from-cart]'
+			},
+			manyRefs: {},
+			setup({el, refs, manyRefs, data}) {
+				[
+					refs.quantity.dataset.id,
+					refs.quantity.dataset.price,
+					refs.quantity.dataset.catalogId,
+					refs.label.textContent,
+					refs.price.textContent,
+					refs.button.dataset.id
+				] = [
+					data.id,
+					data.price,
+					data.catalogId || '',
+					data.name,
+					window.formatPrice(data.price),
+					data.id
+				];
+			}
+		});
+		this.T.define('emptyCart');
+	}
 	initElements() {
-		this.toggle       = document.querySelector('.toggle-cart');
+		this.restoreElement = false;
+		this.selectors = {
+			toggle: {
+				btn: '.toggle-cart',
+				count: '.toggle-cart .count',
+				icon: '.toggle-cart .icon',
+			},
+			panels: {
+				checkout: {
+					toggle: 'button[data-tab="checkout"]',
+					form: '#cart form'
+				}
+			},
+			cart: {
+				wrap: '#cart .cart-items',
+				empty: '#cart .cart-items .empty',
+				table: '#cart .cart-items table',
+				body: '#cart .cart-items tbody',
+				total: '#cart .cart-total',
+				foot: {
+					foot: '#cart tfoot',
+					total: '#cart tfoot .total',
+					tax: '#cart tfoot .tax',
+				},
+				grandTotal: '#cart .total span',
+				tax: '#cart .cart-total .tax span',
+			}
+		};
+		this.ui = window.uiFromSelectors(this.selectors);
+		this.ui.cart.items = new Map();
+
 		if (!this.isOpen) {
-			this.toggle.disabled = true;
-			this.toggle.title    = 'Currently closed for online ordering';
+			this.ui.toggle.btn.disabled = true;
+			this.ui.toggle.btn.title    = 'Currently closed for online ordering';
 		}
-		this.checkoutPanel = this.checkout.querySelector('button[data-tab="checkout"]');
-		this.itemsList     = this.checkout.querySelector('.cart-items');
-		this.table         = this.checkout.querySelector('.cart-items tbody');
-		this.total         = this.checkout.querySelector('.cart-total');
-		this.totalTax      = this.total.querySelector('.tax span');
-		this.grandTotal    = this.total.querySelector('.total span');
+
 		this.checkoutForm  = this.checkout.querySelector('form');
 
-		this.tabs = new window.jvbTabs(this.checkoutForm, {
+		this.tabs = window.jvbTabs.registerTab(this.checkoutForm, {
 			updateURL: false
 		});
 	}
@@ -137,6 +233,7 @@
 		this.checkoutForm.addEventListener('submit', (e) => this.handleFormSubmit(e));
 		document.addEventListener('click', this.clickHandler);
 		document.addEventListener('change', this.changeHandler);
+		// document.addEventListener('keydown', this.keyHandler);
 	}
 
 	/*****************************************************************
@@ -147,14 +244,15 @@
 		if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) {
 			let quantity = window.targetCheck(e, 'div.quantity');
 			this.handleNumberClick(e, quantity);
-		} else if (window.targetCheck(e, '[data-add-to-cart]')) {
+		} else if (window.targetCheck(e, '[data-add-to-cart]') && window.targetCheck(e, 'button')) {
 			let add = window.targetCheck(e, '[data-add-to-cart]');
 			this.handleAddToCart(add);
-		} else if (window.targetCheck(e, '[data-remove-from-cart]')) {
+		} else if (window.targetCheck(e, '[data-remove-from-cart]') ) {
 			let remove = window.targetCheck(e, '[data-remove-from-cart]');
 			this.handleRemoveFromCart(remove);
 		} else if (window.targetCheck(e, '[data-clear-cart]')) {
 			this.clearCart();
+			document.querySelector('.restored')?.remove();
 		} else if (window.targetCheck(e, '[data-dismiss]')) {
 			window.targetCheck(e, '[data-dismiss]').closest('.restored')?.remove();
 		}
@@ -164,12 +262,7 @@
 		let input = window.targetCheck(e, '.quantity-input');
 		if (input) {
 			let item  = e.target.closest('.quantity');
-			let value = input.value;
-
-			if (window.targetCheck(e, '.cart-items')) {
-				let main = document.querySelector(`.menu-section [data-id="${item.dataset.id}"] input`);
-				if (main) main.value = input.value;
-			}
+			let value = e.target.value;
 
 			if (value > 0) {
 				this.handleAddToCart(item);
@@ -181,6 +274,10 @@
 
 	handleEscape(e) {
 		if (e.key === 'Escape') {
+			if (this.restoreElement) {
+				this.restoreElement.classList.remove('show');
+				this.restoreElement = false;
+			}
 			this.stepMultiplier = 1;
 		} else if (e.ctrlKey && e.shiftKey) {
 			this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 100, 1000);
@@ -196,38 +293,62 @@
 	handleAddToCart(item) {
 		let id       = item.dataset.id;
 		let price    = parseFloat(item.dataset.price);
-		let quantity = parseInt(item.querySelector('.quantity-input')?.value) ?? 1;
+		let quantity = parseInt(item.querySelector('.quantity input')?.value) ?? 1;
 		let total    = parseFloat(price * quantity);
 
 		this.createItemElement(item);
 
-		this.cartItems.set(id, {
+		let parent = this.determineParent(item);
+
+		let stored = {};
+		if (this.cartItems.has(id)) {
+			stored = this.cartItems.get(id);
+		}
+		let newValue = {
 			post_id:    id,
-			name:       item.dataset.name,
+			name:       stored.name ?? item.dataset.name,
+			label:		stored.label ?? parent.dataset.label ??'',
+			parentId: 	stored.parentId ?? parent.dataset.id ??'',
 			price:      price,
 			quantity:   quantity,
 			total:      total,
-			catalog_id: item.dataset.catalogId || '',
-		});
+			catalog_id: stored.catalogId ?? item.dataset.catalogId ?? '',
+		};
+		this.cartItems.set(id, newValue);
 		this.saveCart();
 	}
 
 	handleRemoveFromCart(item) {
+		console.log(item);
 		if (confirm('This will remove this item from the cart. Continue?')) {
-			if (!item.querySelector('[data-id]')) {
-				item = item.closest('.item')?.querySelector('.quantity.field');
+			let id = item.dataset.id??item.id??false;
+			if (!id) {
+				console.error('No ID found..');
 			}
-			let id = item.dataset.id;
-			this.cartItems.delete(id);
-			this.table.querySelector(`[data-id="${id}"]`)?.closest('tr').remove();
-
-			let input = document.querySelector(`[data-id="${id}"] input`);
-			if (input) input.value = 0;
+			this.removeFromCart(id);
 
 			this.maybeAddEmptyState();
 			this.saveCart();
 		}
 	}
+		removeFromCart(id) {
+			let parent = this.findParentItem(id);
+			if (parent) {
+				let el = parent.items.get(id);
+				el.el.remove();
+				parent.items.delete(id);
+				if (el.reference){
+					el.reference.value = 0;
+				}
+				if (parent.items.size === 0) {
+					console.log('Removing parent item');
+					parent.el.remove();
+					this.ui.cart.items.delete(parent.id);
+				}
+			}
+
+			this.cartItems.delete(id);
+		}
 
 	handleNumberClick(e, container) {
 		e.preventDefault();
@@ -236,11 +357,15 @@
 		else if (e.target.closest('.decrease'))  change -= 1;
 
 		if (change !== 0) {
-			let step  = parseInt(container.dataset.step);
 			let input = container.querySelector('input');
+			let step  = parseInt(input.step);
 			let value = (input.value === '') ? 0 : parseInt(input.value);
+			if (value === 0 && change < 0) return;
 
-			input.value = value + (step * change * this.stepMultiplier);
+			let increaseBy = step * change * this.stepMultiplier;
+
+			let newValue = value + increaseBy;
+			input.value = Math.max(newValue, 0);
 			input.dispatchEvent(new Event('change', { bubbles: true }));
 			this.handleNumberLimits(container);
 		}
@@ -254,10 +379,12 @@
 		let decrease = container.querySelector('.decrease');
 		let value    = parseInt(input.value);
 
-		if (value <= min) {
+		if (min !== undefined && value <= min) {
+			console.log('Resetting to minimum value');
 			input.value      = min;
 			decrease.disabled = true;
-		} else if (value >= max) {
+		} else if (max !== undefined && value >= max) {
+			console.log('Resetting to maximum value');
 			input.value      = max;
 			increase.disabled = true;
 		} else {
@@ -271,100 +398,163 @@
 	 *****************************************************************/
 
 	createItemElement(item) {
-		let element = this.itemsList.querySelector(`[data-id="${item.dataset.id}"]`);
-		let add     = false;
-		let price   = item.dataset.price;
-		let quantity = item.querySelector('[name="quantity"]')?.value ?? 1;
+		let element = this.maybeCreateCartItem(item);
 
-		if (!element) {
-			add     = true;
-			element = window.getTemplate('cartItem');
-			let field = element.querySelector('.quantity');
-			[
-				field.dataset.id,
-				element.querySelector('label').textContent,
-				element.querySelector('.price').textContent,
-				field.dataset.price,
-				field.dataset.catalogId,
-			] = [
-				item.dataset.id,
-				item.dataset.name,
-				window.formatPrice(price),
-				price,
-				item.dataset.catalogId || '',
-			];
-		} else {
-			element = element.closest('tr');
-		}
+		let quantity = item.tagName === undefined ?
+			item.quantity :
+			item.querySelector('input').value ?? 1;
+		let price = item.tagName === undefined ?
+			item.price :
+			item.dataset.price;
 
-		[
-			element.querySelector('[name="quantity"]').value,
-			element.querySelector('.total').textContent,
-		] = [
-			quantity,
-			window.formatPrice(quantity * price),
-		];
-
-		if (add) {
-			element.classList.add('adding');
-			this.table.append(element);
-			setTimeout(() => element.classList.remove('adding'), 500);
+		element.quantity.value = quantity;
+		element.total.textContent = window.formatPrice(quantity * price);
+		if (element.reference) {
+			element.reference.value = quantity;
 		}
 	}
+		maybeCreateCartItem(item) {
+			let normalized = {};
+			if (item.tagName === undefined) {
+				//We're getting it from the cached cart
+				normalized = {
+					id: String(item.post_id),
+					name: item.name,
+					price: item.price,
+					catalogId: item.catalog_id || '',
+					quantity: item.quantity,
+					label: item.label,
+					parentId: String(item.parentId)
+				};
+			}  else if (item.closest('#cart')) {
+				// quantity field clicked from inside the cart itself — item already exists
+				let id = item.dataset.id;
+				let parent = this.findParentItem(id);
+				if (parent) {
+					return parent.items.get(id);
+				}
+				// fallback: shouldn't normally happen, but avoid crashing
+				console.warn('Cart quantity changed but no existing row found for id:', id);
+			}else {
+				//We're getting it direct from the quantity field
+				normalized = {
+					id: item.dataset.id,
+					name: item.dataset.name,
+					price: item.dataset.price,
+					catalogId: item.dataset.catalogId || '',
+				}
+				let parent = this.determineParent(item);
+				normalized.parentId = parent.dataset.id??parent.id;
+				normalized.label = parent.dataset.label??parent.label;
+			}
+
+			if (!this.ui.cart.items.has(normalized.parentId)) {
+				let parentEl = this.T.create('mainCartItem', normalized);
+				this.ui.cart.items.set(normalized.parentId, {
+					el: parentEl,
+					id: normalized.parentId,
+					label: normalized.label,
+					items: new Map()
+				});
+				this.ui.cart.table.insertBefore(parentEl, this.ui.cart.foot.foot);
+			}
+
+			let parent = this.ui.cart.items.get(normalized.parentId);
+			if (!parent.items.has(normalized.id)) {
+				let el = this.T.create('childCartItem', normalized);
+				parent.items.set(normalized.id, {
+					el: el,
+					quantity: el.querySelector('[name="quantity"]'),
+					total: el.querySelector('.total'),
+					reference: document.querySelector(`.menu-items [data-id="${normalized.id}"] input`),
+				});
+				parent.el.append(el);
+			}
+
+			return parent.items.get(normalized.id);
+		}
+			determineParent(item) {
+				if (item.tagName !== undefined) {
+					return this.parentFromHTML(item);
+				}
+			}
+				parentFromHTML(item) {
+					let parent = item.closest('.menu-item');
+					if (parent) return parent;
+					let id = item.dataset.id;
+					return this.findParentItem(id).el??false;
+				}
+			findParentItem(searchId) {
+				let parentItem = false;
+				for (let [id, data] of this.ui.cart.items.entries()) {
+					if (data.items.has(searchId)) {
+						parentItem = this.ui.cart.items.get(id);
+					}
+				}
+				return parentItem;
+			}
+
 
 	/*****************************************************************
 	 * CART STATE
 	 *****************************************************************/
-
+	toggleButton(open = false)
+	{
+		this.ui.toggle.icon.hidden = open;
+	}
 	maybeAddEmptyState() {
-		let empty = this.itemsList.querySelector('.empty');
-		if (empty) empty.remove();
+		this.toggleButton(true);
+		this.findEmpty();
 
 		if (this.cartItems.size === 0) {
-			this.checkoutPanel.disabled = true;
-			this.checkoutPanel.title    = 'Add some things to your cart first!';
-			let emptyEl = window.getTemplate('emptyCart');
-			this.itemsList.append(emptyEl);
-			this.table.closest('table').hidden = true;
-			this.total.hidden = true;
+			this.ui.panels.checkout.toggle.disabled = true;
+			this.ui.panels.checkout.toggle.title    = 'Add some things to your cart first!';
+			this.ui.cart.empty.hidden = false;
+			this.ui.cart.table.hidden = true;
+			this.ui.cart.total.hidden = true;
 			this.a11y.announce('Nothing in Cart');
 		} else {
-			this.checkoutPanel.disabled       = false;
-			this.table.closest('table').hidden = false;
-			this.total.hidden                  = false;
-			this.checkoutPanel.title           = 'Checkout';
+			this.ui.cart.empty.hidden = true;
+			this.ui.panels.checkout.toggle.disabled       = false;
+			this.ui.cart.table.hidden = false;
+			this.ui.cart.total.hidden                  = false;
+			this.ui.panels.checkout.toggle.title           = 'Checkout';
 		}
 	}
+		findEmpty() {
+			if (!this.ui.cart.empty) {
+				this.ui.cart.empty = this.T.create('emptyCart');
+				this.ui.cart.wrap.append(this.ui.cart.empty);
+			}
+		}
 
 	notifyRestoredCart() {
 		let restored = window.getTemplate('restoredCart');
-		this.checkout.querySelector('.tab-content[data-tab=cartItems]').insertBefore(restored, this.itemsList);
+		document.body.append(restored);
+		restored.classList.toggle('show');
+		this.restoreElement = restored;
 
 		this.cartItems.forEach(item => {
-			let element = window.getTemplate('cartItem');
-			let field   = element.querySelector('.quantity');
-			[
-				field.dataset.id,
-				element.querySelector('label').textContent,
-				element.querySelector('.price').textContent,
-				field.dataset.price,
-				field.dataset.catalogId,
-				element.querySelector('[name="quantity"]').value,
-				element.querySelector('.total').textContent,
-			] = [
-				item.post_id,
-				item.name,
-				window.formatPrice(item.price),
-				item.price,
-				item.catalog_id || '',
-				item.quantity,
-				window.formatPrice(item.quantity * item.price),
-			];
-			this.table.append(element);
+			this.createItemElement(item);
 		});
 		this.updateTotal();
 	}
 
+	updateToggle() {
+		if (this.cartItems.size > 0 && this.ui.toggle.btn.classList.contains('empty')) {
+			this.ui.toggle.btn.classList.remove('empty');
+		}
+		if (this.cartItems.size === 0 && !this.ui.toggle.btn.classList.contains('empty')) {
+			this.ui.toggle.btn.classList.add('empty');
+		}
+		let total = 0;
+		for (let [id, item] of this.cartItems.entries()) {
+			total += item.quantity;
+		}
+
+		this.ui.toggle.count.textContent = this.cartItems.size === 0 ? '' : total;
+	}
+
 	/*****************************************************************
 	 * TOTALS
 	 *****************************************************************/
@@ -373,11 +563,13 @@
 		let total = 0;
 		this.cartItems.forEach(item => total += item.total);
 		let tax = total * 0.05;
-		window.eraseText(this.totalTax);
-		window.eraseText(this.grandTotal);
-		window.typeText(this.totalTax, window.formatPrice(tax));
-		window.typeText(this.grandTotal, window.formatPrice(total + tax));
-		this.totalTax.classList.remove('typeText');
+		window.eraseText(this.ui.cart.tax);
+		window.eraseText(this.ui.cart.foot.tax);
+		window.eraseText(this.ui.cart.foot.total);
+		window.typeText(this.ui.cart.foot.tax, window.formatPrice(tax));
+		window.typeText(this.ui.cart.grandTotal, window.formatPrice(total + tax));
+		window.typeText(this.ui.cart.foot.total, window.formatPrice(total + tax));
+		this.ui.cart.tax.classList.remove('typeText');
 	}
 
 	/*****************************************************************

--
Gitblit v1.10.0