PageSourceSearch

https://wizfort.com/assets/cslshoes_erp/js/wishlist_page.js?v=20260517c

js wizfort.com collected 2026-09-27 13:40:31 UTC 17,615 bytes, 435 lines download raw bytes

1/**
2 * wishlist_page.js — /wishlist page remove-button handler (BUG-W1 fix, 2026-05-17)
3 *
4 * Replaces webshop's broken .remove-wish handler which 403-silent-fails because
5 * webshop's Wishlist DocType has no Website User role permission, and the bound
6 * callback only checks r.exc (unset on PermissionError success-callback path).
7 *
8 * Uses the CSL custom endpoint cslshoes_erp.api.wishlist.toggle_wishlist_item
9 * (same one Session C + PDP main heart use). Reads website_item from a new
10 * data attribute on the .remove-wish element (template edit pairs with this).
11 *
12 * Loading: defer-loaded via hooks.py:web_include_js. Per ERP-083, NO frappe.ready
13 * wrapper (callback would never dispatch on defer-loaded files). IIFE runs at
14 * script-load time, binds single document-level CAPTURE-phase delegate so it
15 * fires BEFORE webshop's jQuery bubble delegate at $('.page_content').on(...).
16 * stopImmediatePropagation prevents webshop's handler from also firing → no
17 * spurious 403 in network tab.
18 *
19 * Auditor C1: button disabled on click to defeat double-click → toggle endpoint
20 *   would otherwise re-ADD on the second click (non-idempotent).
21 * Auditor C2: empty-state mirrors webshop's render_empty_state — appends to
22 *   .container after clearing the wishlist row, NOT nested inside .row.
23 * Auditor C3: uses window.cslTileWishlist.updateAllCountBadges (exposed in
24 *   tile_wishlist.js v20260517b) for header badge sync.
25 * Auditor C4: trusts data.message.count from response, NOT wish_count cookie
26 *   (webshop sets cookie via its endpoint; ours doesn't, so cookie is stale).
27 * Auditor C6: capture-phase delegate verified to beat webshop's jQuery bubble.
28 */
29(function () {
30	if (typeof window === 'undefined') return;
31	if (window.cslWishlistPage && window.cslWishlistPage.__bound__) return;
32
33	var ENDPOINT_TOGGLE = '/api/method/cslshoes_erp.api.wishlist.toggle_wishlist_item';
34
35	function getCsrfHeader() {
36		var h = { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' };
37		if (typeof frappe !== 'undefined' && frappe.csrf_token) {
38			h['X-Frappe-CSRF-Token'] = frappe.csrf_token;
39		}
40		return h;
41	}
42
43	function isGuest() {
44		return (typeof frappe !== 'undefined' &&
45			frappe.session && frappe.session.user === 'Guest');
46	}
47
48	function showToast(message, type) {
49		// Reuse Session C's toast helper if loaded; otherwise minimal inline fallback.
50		if (window.cslTileWishlist && typeof cslTileWishlist.showToast === 'function') {
51			cslTileWishlist.showToast(message, type);
52			return;
53		}
54		var t = document.createElement('div');
55		t.className = 'csl-toast ' + (type || 'info');
56		t.setAttribute('role', 'status');
57		t.setAttribute('aria-live', 'polite');
58		t.textContent = message;
59		t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);' +
60			'background:#111827;color:#fff;padding:10px 18px;border-radius:6px;font-size:14px;' +
61			'box-shadow:0 4px 12px rgba(0,0,0,0.2);z-index:9999;';
62		document.body.appendChild(t);
63		setTimeout(function () { t.remove(); }, 3000);
64	}
65
66	function updateHeaderBadges(count) {
67		// Prefer Session C's helper (exposed in tile_wishlist.js v20260517b).
68		// Falls back to local update for cold-load edge cases (script ordering).
69		if (window.cslTileWishlist && typeof cslTileWishlist.updateAllCountBadges === 'function') {
70			cslTileWishlist.updateAllCountBadges(count);
71			return;
72		}
73		// Match the selector union in tile_wishlist.js v20260517c.
74		var badges = document.querySelectorAll(
75			'.csl-wishlist-count, #csl-wishlist-count, ' +
76			'.wishlist-count, #header-wishlist-count, #wishlist-count'
77		);
78		badges.forEach(function (b) {
79			b.textContent = count > 0 ? count : '';
80			b.style.display = count > 0 ? 'flex' : 'none';
81		});
82	}
83
84	function renderEmptyState() {
85		// Mirrors webshop's render_empty_state pattern (wishlist.js:182-191). The
86		// existing wishlist.html structure puts the items <div class="row"> at top
87		// of .container; replace its parent <div class="row"> chain with empty-state.
88		var container = document.querySelector('.container');
89		if (!container) return;
90		// Remove the items row (the only .row inside .container on this page when items present).
91		var itemsRow = container.querySelector('.row');
92		if (itemsRow) itemsRow.remove();
93		var empty = document.createElement('div');
94		empty.className = 'cart-empty frappe-card';
95		empty.innerHTML =
96			'<div class="cart-empty-state">' +
97				'<img src="/assets/webshop/images/cart-empty-state.png" alt="Empty Wishlist">' +
98			'</div>' +
99			'<div class="cart-empty-message mt-4">Wishlist is empty!</div>';
100		container.appendChild(empty);
101	}
102
103	function removeWishlistItem(triggerEl) {
104		var websiteItem = triggerEl.getAttribute('data-website-item');
105		if (!websiteItem) {
106			showToast('Could not identify item to remove (missing website-item ID)', 'error');
107			return;
108		}
109		if (triggerEl.dataset.cslBusy === '1') return;  // Auditor C1: defeat double-click race
110
111		if (isGuest()) {
112			showToast('Please log in to manage your wishlist', 'info');
113			setTimeout(function () {
114				window.location.href = '/login?redirect-to=' + encodeURIComponent(window.location.pathname);
115			}, 1500);
116			return;
117		}
118
119		triggerEl.dataset.cslBusy = '1';
120		triggerEl.style.opacity = '0.5';
121		triggerEl.style.cursor = 'progress';
122		var card = triggerEl.closest('.wishlist-card');
123
124		fetch(ENDPOINT_TOGGLE, {
125			method: 'POST',
126			headers: getCsrfHeader(),
127			credentials: 'same-origin',
128			body: JSON.stringify({ website_item: websiteItem })
129		}).then(function (r) {
130			if (r.status === 403) {
131				delete triggerEl.dataset.cslBusy;
132				triggerEl.style.opacity = '';
133				triggerEl.style.cursor = '';
134				showToast('Session expired. Please log in again.', 'info');
135				setTimeout(function () {
136					window.location.href = '/login?redirect-to=' + encodeURIComponent(window.location.pathname);
137				}, 1500);
138				return null;
139			}
140			if (!r.ok) {
141				delete triggerEl.dataset.cslBusy;
142				triggerEl.style.opacity = '';
143				triggerEl.style.cursor = '';
144				showToast('Could not remove (HTTP ' + r.status + ')', 'error');
145				return null;
146			}
147			return r.json();
148		}).then(function (data) {
149			if (!data) return;
150			var msg = data.message || {};
151			if (msg.success && msg.action === 'removed') {
152				// Fade out card, then remove from DOM.
153				if (card) {
154					card.style.transition = 'opacity 250ms ease, transform 250ms ease';
155					card.style.opacity = '0';
156					card.style.transform = 'scale(0.95)';
157					setTimeout(function () {
158						card.remove();
159						// If no cards left, render empty state.
160						var remaining = document.querySelectorAll('.wishlist-card').length;
161						if (remaining === 0) renderEmptyState();
162					}, 260);
163				}
164				updateHeaderBadges(msg.count);
165				showToast('Removed from wishlist', 'success');
166			} else if (msg.success && msg.action === 'added') {
167				// Unexpected — toggle endpoint re-ADDED an item. Should be impossible
168				// since the page only shows items already in the wishlist, but if it
169				// happens (e.g. concurrent removal in another tab), just leave the UI
170				// stable and reset the button.
171				delete triggerEl.dataset.cslBusy;
172				triggerEl.style.opacity = '';
173				triggerEl.style.cursor = '';
174				showToast('Wishlist state was out of sync — refreshed', 'info');
175				updateHeaderBadges(msg.count);
176			} else if (msg.login_required) {
177				delete triggerEl.dataset.cslBusy;
178				triggerEl.style.opacity = '';
179				triggerEl.style.cursor = '';
180				showToast('Please log in to manage your wishlist', 'info');
181				setTimeout(function () {
182					window.location.href = '/login?redirect-to=' + encodeURIComponent(window.location.pathname);
183				}, 1500);
184			} else {
185				delete triggerEl.dataset.cslBusy;
186				triggerEl.style.opacity = '';
187				triggerEl.style.cursor = '';
188				showToast('Could not remove (unexpected response)', 'error');
189			}
190		}).catch(function () {
191			delete triggerEl.dataset.cslBusy;
192			triggerEl.style.opacity = '';
193			triggerEl.style.cursor = '';
194			showToast('Could not remove (network error)', 'error');
195		});
196	}
197
198	// =========================================================================
199	// BUG-W1b — Move to Cart silent wishlist-remove fix (2026-05-17)
200	//
201	// Webshop's bind_move_to_cart_action (apps/webshop/webshop/public/js/wishlist.js:40-58)
202	// fires shopping_cart_update (cart-add OK for logged-in) then add_remove_from_wishlist
203	// ("remove", ...) which hits the same broken endpoint as BUG-W1 (403-silent-fail).
204	// Item ends up in cart AND still in wishlist after refresh.
205	//
206	// Fix mirrors BUG-W1 + auditor C1-C7:
207	// - Same IIFE so __bound__ guard covers both listeners (auditor C1).
208	// - Sequenced: cart-add first; toggle ONLY if cart-add returns message.success===true
209	//   (auditor C2 — webshop's update_cart can 200 with internal failure).
210	// - action==='added' edge case: one-shot retry, then hard-fail (auditor C3).
211	// - Guest guard at top of handler (auditor C5 — page is logged-in-only but
212	//   cached HTML + mid-session expiry are the edge case).
213	// - Partial-failure UX: cart badge MUST refresh even when wishlist-remove fails
214	//   because cart DID change (auditor C7). Otherwise badge looks stale and user
215	//   thinks the whole action failed.
216	// =========================================================================
217	var ENDPOINT_CART = '/api/method/cslshoes_erp.api.guest_cart.update_guest_cart';
218
219	function getGuestCartId() {
220		var m = document.cookie.match(/guest_cart_id=([^;]+)/);
221		return m ? m[1] : null;
222	}
223
224	function restoreMoveBtn(btn, originalLabel) {
225		delete btn.dataset.cslBusy;
226		btn.disabled = false;
227		btn.style.opacity = '';
228		btn.style.cursor = '';
229		var label = btn.querySelector('.csl-move-to-cart-label');
230		if (label && originalLabel) label.textContent = originalLabel;
231	}
232
233	function fadeCardAndRemove(card) {
234		if (!card) return;
235		card.style.transition = 'opacity 250ms ease, transform 250ms ease';
236		card.style.opacity = '0';
237		card.style.transform = 'scale(0.95)';
238		setTimeout(function () {
239			card.remove();
240			var remaining = document.querySelectorAll('.wishlist-card').length;
241			if (remaining === 0) renderEmptyState();
242		}, 260);
243	}
244
245	function refreshHeaderCounts() {
246		if (typeof window.updateCslCartCount === 'function') window.updateCslCartCount();
247		if (typeof window.updateCslWishlistCount === 'function') window.updateCslWishlistCount();
248	}
249
250	// Wishlist-remove step — used after cart-add success. Returns Promise.
251	function postWishlistToggle(websiteItem) {
252		return fetch(ENDPOINT_TOGGLE, {
253			method: 'POST',
254			headers: getCsrfHeader(),
255			credentials: 'same-origin',
256			body: JSON.stringify({ website_item: websiteItem })
257		}).then(function (r) {
258			if (r.status === 403) return { __auth_fail: true };
259			if (!r.ok) return { __http_fail: r.status };
260			return r.json().then(function (data) { return data; });
261		}).catch(function () {
262			return { __network_fail: true };
263		});
264	}
265
266	function moveToCart(btn) {
267		var itemCode = btn.getAttribute('data-item-code');
268		var websiteItem = btn.getAttribute('data-website-item');
269		if (!itemCode || !websiteItem) {
270			showToast('Could not identify item (missing data attributes)', 'error');
271			return;
272		}
273		if (btn.dataset.cslBusy === '1') return;  // auditor C1 — double-click guard
274
275		// Auditor C5: guest guard at top — page is logged-in-only via wishlist.py,
276		// but cached HTML + mid-session expiry create the edge case.
277		if (isGuest()) {
278			showToast('Please log in to manage your wishlist', 'info');
279			setTimeout(function () {
280				window.location.href = '/login?redirect-to=' + encodeURIComponent(window.location.pathname);
281			}, 1500);
282			return;
283		}
284
285		btn.dataset.cslBusy = '1';
286		btn.disabled = true;
287		btn.style.opacity = '0.7';
288		btn.style.cursor = 'progress';
289		var label = btn.querySelector('.csl-move-to-cart-label');
290		var originalLabel = label ? label.textContent : null;
291		if (label) label.textContent = 'Moving...';
292		var card = btn.closest('.wishlist-card');
293
294		// Step 1: cart-add via cslshoes_erp.api.guest_cart.update_guest_cart
295		// (delegates to webshop's update_cart for logged-in users per
296		// api/guest_cart.py:184).
297		fetch(ENDPOINT_CART, {
298			method: 'POST',
299			headers: getCsrfHeader(),
300			credentials: 'same-origin',
301			body: JSON.stringify({ item_code: itemCode, qty: 1, cart_id: getGuestCartId() })
302		}).then(function (r) {
303			if (r.status === 403) {
304				restoreMoveBtn(btn, originalLabel);
305				showToast('Session expired. Please log in again.', 'info');
306				setTimeout(function () {
307					window.location.href = '/login?redirect-to=' + encodeURIComponent(window.location.pathname);
308				}, 1500);
309				return null;
310			}
311			if (!r.ok) {
312				restoreMoveBtn(btn, originalLabel);
313				showToast('Could not move to cart (HTTP ' + r.status + ')', 'error');
314				return null;
315			}
316			return r.json();
317		}).then(function (data) {
318			if (!data) return null;
319			var msg = data.message || {};
320			// Auditor C2 — explicit success-payload check. webshop's update_cart
321			// can return 200 with success:false (out-of-stock, item disabled, etc).
322			if (msg.success === false) {
323				restoreMoveBtn(btn, originalLabel);
324				showToast(msg.error || 'Could not add to cart', 'error');
325				return null;
326			}
327			// Cart-add succeeded. Step 2: remove from wishlist.
328			return postWishlistToggle(websiteItem).then(function (wlData) {
329				// Auditor C7: cart count refreshes even on wishlist-remove failure
330				// — cart DID change, badge MUST reflect that.
331				if (typeof window.updateCslCartCount === 'function') window.updateCslCartCount();
332
333				if (!wlData || wlData.__auth_fail) {
334					restoreMoveBtn(btn, originalLabel);
335					showToast('Added to cart, but session expired during wishlist update', 'info');
336					setTimeout(function () {
337						window.location.href = '/login?redirect-to=' + encodeURIComponent(window.location.pathname);
338					}, 1500);
339					return;
340				}
341				if (wlData.__http_fail || wlData.__network_fail) {
342					restoreMoveBtn(btn, originalLabel);
343					showToast('Added to cart but could not remove from wishlist — click X to remove manually', 'error');
344					return;
345				}
346				var wlMsg = wlData.message || {};
347				if (wlMsg.success && wlMsg.action === 'removed') {
348					fadeCardAndRemove(card);
349					if (typeof window.updateCslWishlistCount === 'function') window.updateCslWishlistCount();
350					showToast('Moved to cart', 'success');
351					return;
352				}
353				// Auditor C3: action==='added' means wishlist was empty when toggle hit
354				// (cross-tab removal race). One-shot retry to remove again.
355				if (wlMsg.success && wlMsg.action === 'added') {
356					postWishlistToggle(websiteItem).then(function (retryData) {
357						var retryMsg = (retryData && retryData.message) || {};
358						if (retryMsg.success && retryMsg.action === 'removed') {
359							fadeCardAndRemove(card);
360							if (typeof window.updateCslWishlistCount === 'function') window.updateCslWishlistCount();
361							showToast('Moved to cart', 'success');
362						} else {
363							restoreMoveBtn(btn, originalLabel);
364							showToast('Added to cart but wishlist state is out of sync — please refresh', 'error');
365						}
366					});
367					return;
368				}
369				if (wlMsg.login_required) {
370					restoreMoveBtn(btn, originalLabel);
371					showToast('Added to cart. Please log in to update your wishlist', 'info');
372					setTimeout(function () {
373						window.location.href = '/login?redirect-to=' + encodeURIComponent(window.location.pathname);
374					}, 1500);
375					return;
376				}
377				restoreMoveBtn(btn, originalLabel);
378				showToast('Added to cart but wishlist-remove had an unexpected response', 'error');
379			});
380		}).catch(function () {
381			restoreMoveBtn(btn, originalLabel);
382			showToast('Could not move to cart (network error)', 'error');
383		});
384	}
385
386	function onDocumentClick(e) {
387		if (!e.target || !e.target.closest) return;
388		// .remove-wish (BUG-W1) — X delete button.
389		var removeTrigger = e.target.closest('.remove-wish[data-website-item]');
390		if (removeTrigger) {
391			// Capture phase + stopImmediatePropagation — beats webshop's jQuery bubble
392			// delegate at $('.page_content').on('click', '.remove-wish', ...).
393			e.preventDefault();
394			e.stopPropagation();
395			if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
396			removeWishlistItem(removeTrigger);
397			return;
398		}
399		// .btn-add-to-cart inside .wishlist-card (BUG-W1b) — Move to Cart button.
400		// Scoped to .wishlist-card so we don't intercept .btn-add-to-cart on /all-products
401		// or other pages where webshop's stock binding is the correct handler.
402		var moveTrigger = e.target.closest('.wishlist-card .btn-add-to-cart[data-website-item]');
403		if (moveTrigger) {
404			e.preventDefault();
405			e.stopPropagation();
406			if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
407			moveToCart(moveTrigger);
408			return;
409		}
410	}
411
412	function onKeydown(e) {
413		if (e.key !== 'Enter' && e.key !== ' ') return;
414		if (!e.target || !e.target.classList) return;
415		if (e.target.classList.contains('remove-wish') && e.target.getAttribute('data-website-item')) {
416			e.preventDefault();
417			removeWishlistItem(e.target);
418		} else if (e.target.classList.contains('btn-add-to-cart') &&
419		           e.target.getAttribute('data-website-item') &&
420		           e.target.closest('.wishlist-card')) {
421			e.preventDefault();
422			moveToCart(e.target);
423		}
424	}
425
426	document.addEventListener('click', onDocumentClick, true);
427	document.addEventListener('keydown', onKeydown, false);
428
429	window.cslWishlistPage = {
430		__bound__: true,
431		__version__: '20260517c',
432		removeWishlistItem: removeWishlistItem,
433		moveToCart: moveToCart  // exported for future programmatic use
434	};
435})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.