PageSourceSearch

https://www.junipersardinia.com/wp-content/plugins/jnp-tables/frontend/tables.js?ver=1.0

js junipersardinia.com collected 2026-10-09 23:14:15 UTC 19,581 bytes, 526 lines download raw bytes

1/* jnp-tables frontend widget */
2(function ($) {
3	'use strict';
4
5	const i18n    = jnpTables.i18n;
6	const restUrl = jnpTables.restUrl;
7	const nonce   = jnpTables.nonce;
8	const lang    = jnpTables.lang;
9
10	// =========================================================
11	// INIT — called when the Tables button is clicked
12	// =========================================================
13	window.jnpTablesOpen = function (eventId, occurrenceTs) {
14		const modal = document.getElementById('jnp-tables-modal');
15		if (!modal) return;
16
17		// Reset state
18		state = {
19			eventId,
20			occurrenceTs,
21			step: 1,
22			table: null,
23			drinks: [],    // { type, id, name, quantity, prebook_price, line_total }
24			form: {},
25		};
26
27		modal.style.display = 'flex';
28		document.body.style.overflow = 'hidden';
29		renderStep(1);
30		loadFloorPlan();
31	};
32
33	window.jnpTablesClose = function () {
34		const modal = document.getElementById('jnp-tables-modal');
35		if (modal) modal.style.display = 'none';
36		document.body.style.overflow = '';
37	};
38
39	let state = {};
40	let floorPlanData = null;
41	let drinksData    = null;
42
43	// =========================================================
44	// API
45	// =========================================================
46	function loadFloorPlan() {
47		setLoading(true);
48		fetch(`${restUrl}floor-plan?event_id=${state.eventId}&occurrence_ts=${state.occurrenceTs}&lang=${lang}`, {
49			headers: { 'X-WP-Nonce': nonce }
50		})
51			.then(r => r.json())
52			.then(data => {
53				if (data.code) { showError(data.message); return; }
54				floorPlanData = data;
55				setLoading(false);
56				renderFloorPlan();
57			})
58			.catch(() => { showError('Failed to load floor plan.'); setLoading(false); });
59	}
60
61	function loadDrinks(cb) {
62		if (drinksData) { cb(drinksData); return; }
63		fetch(`${restUrl}drinks?event_id=${state.eventId}&occurrence_ts=${state.occurrenceTs}&lang=${lang}`, {
64			headers: { 'X-WP-Nonce': nonce }
65		})
66			.then(r => r.json())
67			.then(data => { drinksData = data; cb(data); })
68			.catch(() => cb({ categories: [], packages: [] }));
69	}
70
71	// =========================================================
72	// STEP NAVIGATION
73	// =========================================================
74	function renderStep(step) {
75		state.step = step;
76		updateProgressBar(step);
77		const body = document.getElementById('jnp-modal-body');
78		if (!body) return;
79
80		if (step === 1) { body.innerHTML = renderStep1Skeleton(); if (floorPlanData) renderFloorPlan(); }
81		if (step === 2) { renderStep(3); return; } // step 2 removed — skip straight to checkout
82		if (step === 3) { body.innerHTML = renderStep3(); }
83	}
84
85	function updateProgressBar(step) {
86		// 2 visual steps: 1 = choose table, 2 = checkout (internal step 3)
87		const el1 = document.getElementById('jnp-step-1');
88		const el2 = document.getElementById('jnp-step-2');
89		if (el1) { el1.classList.toggle('active', step === 1); el1.classList.toggle('done', step > 1); }
90		if (el2) { el2.classList.toggle('active', step === 3); el2.classList.toggle('done', false); }
91	}
92
93	// =========================================================
94	// STEP 1 — FLOOR PLAN
95	// =========================================================
96	function renderStep1Skeleton() {
97		return `
98		<div class="jnp-step-header">
99			<h2 class="jnp-step-title">
99${i18n.step1_title}</h2>
100			<div class="jnp-legend">
101				<span class="jnp-legend-dot available"></span>${i18n.available}
102				<span class="jnp-legend-dot reserved"></span>${i18n.sold_out}
103				<span class="jnp-legend-dot unavailable"></span>${i18n.unavailable}
104			</div>
105		</div>
106		<div id="jnp-floor-wrap" class="jnp-floor-wrap">
107			<div class="jnp-loader">${i18n.processing}</div>
108		</div>
109		<!-- popover injected dynamically into canvas -->
110		`;
111	}
112
113	function renderFloorPlan() {
114		const wrap = document.getElementById('jnp-floor-wrap');
115		if (!wrap || !floorPlanData) return;
116
117		const { image_url, tables, canvas_width } = floorPlanData;
118
119		// Lock canvas to canvas_width if set — prevents dots drifting on resize
120		const canvasStyle = canvas_width > 0
121			? `max-width:${canvas_width}px;width:100%;margin:0 auto;`
122			: `width:100%;`;
123
124		wrap.innerHTML = `
125		<div class="jnp-floor-canvas" id="jnp-canvas" style="${canvasStyle}">
126			${image_url ? `<img id="jnp-canvas-img" src="${image_url}" style="display:block;width:100%;height:auto;pointer-events:none;">` : ''}
127			${tables.map(t => `
128			<button class="jnp-dot status-${t.status}"
129					style="left:${t.pos_x}%;top:${t.pos_y}%;background:${dotColor(t)}"
130					data-id="${t.id}"
131					${t.status !== 'available' ? 'disabled' : ''}
132					aria-label="${t.label}">
133				<span class="jnp-dot-pulse" style="background:${dotColor(t)}"></span>
134			</button>`).join('')}
135		</div>`;
136
137		// Click on canvas background closes popover
138		const canvasEl = wrap.querySelector('#jnp-canvas');
139		if (canvasEl) {
140			canvasEl.addEventListener('click', function(e) {
141				if (e.target === canvasEl || e.target.tagName === 'IMG') closePopover();
142			});
143		}
144
145		// Click handlers
146		wrap.querySelectorAll('.jnp-dot[data-id]').forEach(btn => {
147			btn.addEventListener('click', () => {
148				const tableId = parseInt(btn.dataset.id);
149				const t = tables.find(x => x.id === tableId);
150				if (t) showTableDetail(t);
151			});
152		});
153	}
154
155	function dotColor(t) {
156		if (t.status === 'available')   return '#22c55e';
157		if (t.status === 'reserved')    return '#ef4444';
158		return '#94a3b8';
159	}
160
161	function showTableDetail(t) {
162		state.table = t;
163		if (!state.partySize || state.lastTableId !== t.id) state.partySize = t.capacity;
164		state.lastTableId = t.id;
165
166		// Highlight dot
167		document.querySelectorAll('.jnp-dot').forEach(d => d.classList.remove('selected'));
168		const dot = document.querySelector(`.jnp-dot[data-id="${t.id}"]`);
169		if (dot) dot.classList.add('selected');
170
171		// Get or create the table screen — appended to modal-inner so it covers everything
172		const inner = document.querySelector('.jnp-modal-inner');
173		if (!inner) return;
174
175		let screen = document.getElementById('jnp-table-screen');
176		if (!screen) {
177			screen = document.createElement('div');
178			screen.id = 'jnp-table-screen';
179			screen.className = 'jnp-table-screen';
180			inner.appendChild(screen);
181		}
182
183		screen.innerHTML = renderTableCard(t);
184		// Slight delay so CSS transition fires
185		requestAnimationFrame(() => screen.classList.add('visible'));
186
187		// Back button → close screen, return to map
188		screen.querySelector('.jnp-close-detail')?.addEventListener('click', closeTableScreen);
189
190		// Party size stepper
191		screen.querySelector('#jnp-minus')?.addEventListener('click', () => {
192			if (state.partySize > 1) { state.partySize--; showTableDetail(t); }
193		});
194		screen.querySelector('#jnp-plus')?.addEventListener('click', () => {
195			if (state.partySize < t.capacity) { state.partySize++; showTableDetail(t); }
196		});
197
198		// Book now — close screen first, then advance
199		screen.querySelector('#jnp-select-table')?.addEventListener('click', () => {
200			const s = document.getElementById('jnp-table-screen');
201			if (s) s.remove();
202			renderStep(3);
203		});
204	}
205
206	function closeTableScreen() {
207		const screen = document.getElementById('jnp-table-screen');
208		if (screen) {
209			screen.classList.remove('visible');
210			screen.addEventListener('transitionend', () => screen.remove(), { once: true });
211		}
212		document.querySelectorAll('.jnp-dot').forEach(d => d.classList.remove('selected'));
213		state.table     = null;
214		state.partySize = null;
215	}
216
217	// Keep closePopover as alias so canvas-click handler still works
218	function closePopover() { closeTableScreen(); }
219
220
221	function renderTableCard(t) {
222		const ps = state.partySize || t.capacity;
223		// min_spend is fixed regardless of party size
224		const deposit = t.deposit_type === 'percentage'
225			? Math.round(t.min_spend * (t.deposit_value / 100) * 100) / 100
226			: t.deposit_value;
227
228		const isOnRequest = (t.booking_type || 'instant') === 'on_request';
229		const bookingBadge = isOnRequest
230			? `<span class="jnp-booking-badge on-request">⏳ ${i18n.on_request_badge}</span>`
231			: `<span class="jnp-booking-badge instant">⚡ ${i18n.instant_badge}</span>`;
232
233		return `
234		<div class="jnp-table-card">
235			<div class="jnp-table-card-header">
236				<h3>${esc(t.label)}</h3>
237				<button class="jnp-close-detail" aria-label="Back to map">✕ Back to map</button>
238			</div>
239			${bookingBadge}
240			${t.description ? `<p class="jnp-table-desc">${esc(t.description)}</p>` : ''}
241			
242			<div class="jnp-party-size-wrap">
243				<p class="jnp-party-label">${i18n.party_size}</p>
244				<div class="jnp-party-stepper">
245					<button id="jnp-minus" class="jnp-stepper-btn" ${ps <= 1 ? 'disabled' : ''}>−</button>
246					<span class="jnp-party-count">${ps} <small>${i18n.pax}</small></span>
247					<button id="jnp-plus" class="jnp-stepper-btn ${ps >= t.capacity ? 'disabled' : ''}" ${ps >= t.capacity ? 'disabled' : ''}>+</button>
248				</div>
249				<small class="jnp-capacity-label">${i18n.capacity}: ${t.capacity} ${i18n.pax}</small>
250			</div>
251
252			<div class="jnp-table-pricing">
253				<div class="jnp-pricing-row">
254					<span>${i18n.min_spend}</span>
255					<strong>€${fmt(t.min_spend)}</strong>
256				</div>
257				<div class="jnp-pricing-row pay-now">
258					<span>${i18n.pay_now}</span>
259					<strong>€${fmt(deposit)}</strong>
260				</div>
261			</div>
262
263			<div class="jnp-redeemable-box">
264				<div class="jnp-redeemable-title">✓ ${i18n.fully_redeemable}</div>
265				<div class="jnp-redeemable-body">${i18n.redeemable_conditions}</div>
266			</div>
267
268			${t.notes ? `<p class="jnp-table-notes">${esc(t.notes)}</p>` : ''}
269
270			${isOnRequest
271				? `<div class="jnp-on-request-info">${i18n.on_request_info}</div>`
272				: `<div class="jnp-pricing-row pay-now">
273					<span>${i18n.pay_now}</span>
274					<strong>€\${fmt(deposit)}</strong>
275				   </div>`
276			}
277
278			<button id="jnp-select-table" class="jnp-btn-primary jnp-btn-full">
279				${isOnRequest ? ('⏳ ' + i18n.request_now + ' →') : (i18n.book_now + ' →')}
280			</button>
281		</div>`;
282	}
283
284	// =========================================================
285	// STEP 2 — skipped (extras removed, goes straight to step 3)
286	// =========================================================
287	window.jnpTablesRenderStep = renderStep;
288	function calcExtras() {
289		return state.drinks.reduce((sum, d) => sum + d.line_total, 0);
290	}
291	function calcDeposit(t, drinks) {
292		if (!t) return 0;
293		const total = t.min_spend + calcExtras();
294		return t.deposit_type === 'percentage'
295			? Math.round(total * (t.deposit_value / 100) * 100) / 100
296			: Math.min(t.deposit_value, total);
297	}
298	function renderOrderSummary() {
299		const t = state.table;
300		if (!t) return '';
301		const extras  = calcExtras();
302		const total   = t.min_spend + extras;
303		const deposit = calcDeposit(t, state.drinks);
304		const balance = total - deposit;
305
306		let drinksHtml = '';
307		state.drinks.forEach(d => {
308			drinksHtml += `<div class="jnp-summary-row"><span>${esc(d.name)} ×${d.quantity}</span><span>€${fmt(d.line_total)}</span></div>`;
309		});
310
311		return `
312		<div class="jnp-order-summary" id="jnp-order-summary">
313			<p class="jnp-summary-title">${i18n.order_summary}</p>
314			<div class="jnp-summary-row"><span>${esc(t.label)}</span><span>€${fmt(t.min_spend)}</span></div>
315			${drinksHtml}
316			<div class="jnp-summary-divider"></div>
317			<div class="jnp-summary-row jnp-summary-total"><span>${i18n.grand_total}</span><span>€${fmt(total)}</span></div>
318			<div class="jnp-summary-row jnp-pay-now-row"><span>${i18n.pay_now}</span><strong>€${fmt(deposit)}</strong></div>
319			<div class="jnp-summary-row jnp-balance-row"><span>${i18n.balance_due}</span><span>€${fmt(balance)}</span></div>
320			<div class="jnp-redeemable-badge small">ℹ ${i18n.fully_redeemable}</div>
321		</div>`;
322	}
323	function refreshOrderSummary() {
324		const el = document.getElementById('jnp-order-summary');
325		if (el) el.outerHTML = renderOrderSummary();
326	}
327
328	// =========================================================
329	// STEP 3 — CONTACT FORM
330	// =========================================================
331	// =========================================================
332	// STEP 3 — CONTACT FORM
333	// =========================================================
334	function renderStep3() {
335		const t = state.table;
336		const deposit = calcDeposit(t, state.drinks);
337		const extras  = calcExtras();
338		const total   = t.min_spend + extras;
339
340		return `
341		<div class="jnp-step-header">
342			<button class="jnp-back-btn" onclick="jnpTablesRenderStep(1)">← ${i18n.back}</button>
343			<h2 class="jnp-step-title">${i18n.step3_title}</h2>
344		</div>
345
346		<div class="jnp-form-wrap" id="jnp-contact-form">
347			<div id="jnp-form-errors" class="jnp-errors" style="display:none;"></div>
348
349			<div class="jnp-form-row two-col">
350				<div class="jnp-field">
351					<label>${i18n.first_name} *</label>
352					<input type="text" id="jf-first-name" placeholder="${i18n.first_name}" value="${esc(state.form.first_name || '')}">
353				</div>
354				<div class="jnp-field">
355					<label>${i18n.last_name} *</label>
356					<input type="text" id="jf-last-name" placeholder="${i18n.last_name}" value="${esc(state.form.last_name || '')}">
357				</div>
358			</div>
359			<div class="jnp-field">
360				<label>${i18n.email} *</label>
361				<input type="email" id="jf-email" placeholder="${i18n.email}" value="${esc(state.form.email || '')}">
362			</div>
363			<div class="jnp-field">
364				<label>${i18n.phone} *</label>
365				<input type="tel" id="jf-phone" placeholder="+39..." value="${esc(state.form.phone || '')}">
366			</div>
367			<div class="jnp-field">
368				<label>${i18n.notes}</label>
369				<textarea id="jf-notes" rows="2" placeholder="${i18n.notes}">${esc(state.form.notes || '')}</textarea>
370			</div>
371			<div class="jnp-field">
372				<label>${i18n.celebrating}</label>
373				<input type="text" id="jf-celebrating" placeholder="${i18n.celebrating}" value="${esc(state.form.celebrating || '')}">
374			</div>
375
376			<!-- Info block -->
377			<div class="jnp-info-block">
378				<p class="jnp-info-title">ℹ ${i18n.info_title}</p>
379				<p class="jnp-info-text" id="jnp-info-text"></p>
380			</div>
381
382			<!-- Summary -->
383			${state.table && state.table.booking_type === 'on_request'
384				? `<div class="jnp-on-request-info" style="margin:12px 0;">${i18n.on_request_info}</div>`
385				: renderOrderSummary()
386			}
387
388			<label class="jnp-checkbox-label">
389				<input type="checkbox" id="jf-tc"> ${i18n.tc_accept} *
390			</label>
391			<label class="jnp-checkbox-label">
392				<input type="checkbox" id="jf-marketing"> ${i18n.marketing_consent}
393			</label>
394
395			<button class="jnp-btn-primary jnp-btn-full" id="jnp-pay-btn" style="margin-top:16px;">
396				${state.table && state.table.booking_type === 'on_request'
397					? ('⏳ ' + i18n.request_submit)
398					: ('💳 ' + i18n.pay + ' — €' + fmt(deposit))
399				}
400			</button>
401		</div>`;
402	}
403
404	// Load info text from WP options (async)
405	function loadInfoText() {
406		const el = document.getElementById('jnp-info-text');
407		if (!el) return;
408		// Use localized string if set, otherwise placeholder
409		el.textContent = jnpTables.infoText || (lang === 'it' ? '18+ · Dress code in vigore · Ingresso con documento' : '18+ · Dress code applies · Valid ID required');
410	}
411
412	// =========================================================
413	// PAYMENT SUBMISSION
414	// =========================================================
415	$(document).on('click', '#jnp-pay-btn', function () {
416		const btn = this;
417		const errors = [];
418
419		const first_name  = $('#jf-first-name').val().trim();
420		const last_name   = $('#jf-last-name').val().trim();
421		const email       = $('#jf-email').val().trim();
422		const phone       = $('#jf-phone').val().trim();
423		const notes       = $('#jf-notes').val().trim();
424		const celebrating = $('#jf-celebrating').val().trim();
425		const tc          = $('#jf-tc').is(':checked');
426		const marketing   = $('#jf-marketing').is(':checked');
427
428		if (!first_name || !last_name || !email || !phone) errors.push(i18n.error_required);
429		if (!tc) errors.push(i18n.error_tc);
430
431		const errEl = document.getElementById('jnp-form-errors');
432		if (errors.length) {
433			errEl.innerHTML = errors.map(e => `<p>${e}</p>`).join('');
434			errEl.style.display = 'block';
435			errEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
436			return;
437		}
438		errEl.style.display = 'none';
439
440		// Save form state
441		state.form = { first_name, last_name, email, phone, notes, celebrating };
442
443		$(btn).prop('disabled', true).text(i18n.processing);
444
445		const payload = {
446			table_id:      state.table.id,
447			event_id:      state.eventId,
448			occurrence_ts: state.occurrenceTs,
449			first_name, last_name, email, phone, notes, celebrating,
450			guests:        state.partySize || state.table.capacity,
451			drinks:        state.drinks.map(d => ({ type: d.type, id: d.id, quantity: d.quantity })),
452			marketing_consent: marketing ? 1 : 0,
453			lang,
454		};
455
456		const isOnRequest = state.table && state.table.booking_type === 'on_request';
457		const endpoint = isOnRequest ? `${restUrl}request-reserve` : `${restUrl}reserve`;
458
459		fetch(endpoint, {
460			method: 'POST',
461			headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
462			body: JSON.stringify(payload),
463		})
464			.then(r => r.json())
465			.then(data => {
466				if (isOnRequest && data.success) {
467					// Show success message inline
468					const formWrap = document.getElementById('jnp-contact-form');
469					if (formWrap) formWrap.innerHTML = `<div class="jnp-request-success"><p>✅ ${i18n.request_sent}</p></div>`;
470				} else if (data.checkout_url) {
471					window.location.href = data.checkout_url;
472				} else {
473					$(btn).prop('disabled', false).text(isOnRequest ? `⏳ ${i18n.request_submit}` : `💳 ${i18n.pay}`);
474					const errEl = document.getElementById('jnp-form-errors');
475					errEl.innerHTML = `<p>${data.message || 'An error occurred.'}</p>`;
476					errEl.style.display = 'block';
477				}
478			})
479			.catch(() => {
480				$(btn).prop('disabled', false).text(isOnRequest ? `⏳ ${i18n.request_submit}` : `💳 ${i18n.pay}`);
481			});
482	});
483
484	// =========================================================
485	// MODAL INIT — inject modal HTML on page load
486	// =========================================================
487	$(document).ready(function () {
488		if (!document.getElementById('jnp-tables-modal')) {
489			$('body').append(`
490			<div id="jnp-tables-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:99999;align-items:center;justify-content:center;padding:16px;">
491				<div class="jnp-modal-inner">
492					<div class="jnp-modal-header">
493						<div class="jnp-progress-bar">
494							<div class="jnp-progress-step active" id="jnp-step-1"><span>1</span></div>
495							<div class="jnp-progress-line"></div>
496							<div class="jnp-progress-step" id="jnp-step-2"><span>2</span></div>
497						</div>
498						<button class="jnp-modal-close" onclick="jnpTablesClose()">✕</button>
499					</div>
500					<div class="jnp-modal-body" id="jnp-modal-body"></div>
501				</div>
502			</div>`);
503		}
504
505		// Close on backdrop click
506		$('#jnp-tables-modal').on('click', function (e) {
507			if (e.target === this) jnpTablesClose();
508		});
509	});
510
511	// =========================================================
512	// HELPERS
513	// =========================================================
514	function fmt(n) { return parseFloat(n).toLocaleString('it-IT', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); }
515	function esc(s) { return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
516	function setLoading(on) {
517		const w = document.getElementById('jnp-floor-wrap');
518		if (!w) return;
519		if (on) w.innerHTML = `<div class="jnp-loader">${i18n.processing}</div>`;
520	}
521	function showError(msg) {
522		const b = document.getElementById('jnp-modal-body');
523		if (b) b.innerHTML = `<div class="jnp-error-msg">${msg}</div>`;
524	}
525
526})(jQuery);

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.