Files
WooDoo/assets/js/woodoo-frontend.js
Malin 68c1ff4455 feat: initial WooDoo plugin – WooCommerce & Odoo 19 integration
- Odoo JSON-RPC client (no Composer, uses wp_remote_post)
- Admin settings page under WooCommerce with connection test
- Customer linking: search Odoo partners from WP user profile
- My Account: Odoo Invoices tab with PDF proxy download
- My Account: Book a Meeting tab (slot calculator + calendar.event)
- WC order → Odoo sale.order auto-sync on processing status
- Products matched by SKU; partner auto-created from billing info
- Uninstall cleanup (options, user meta, order meta, DB table)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:58:27 +02:00

249 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* WooDoo Frontend JS Calendar Booking */
( function () {
'use strict';
const cfg = window.WooDooCalendar;
if ( ! cfg ) return; // not on calendar page
const i18n = cfg.i18n;
const ajaxUrl = cfg.ajax_url;
const nonce = cfg.nonce;
// ── Element refs ────────────────────────────────────────────────────
const dateInput = document.getElementById( 'woodoo-booking-date' );
const slotsWrap = document.getElementById( 'woodoo-slots-wrap' );
const slotsGrid = document.getElementById( 'woodoo-slots-grid' );
const confirmBlock = document.getElementById( 'woodoo-booking-confirm' );
const bookBtn = document.getElementById( 'woodoo-book-btn' );
const notesArea = document.getElementById( 'woodoo-booking-notes' );
const slotLabel = document.getElementById( 'woodoo-selected-slot-label' );
const successMsg = document.getElementById( 'woodoo-booking-success' );
const errorMsg = document.getElementById( 'woodoo-booking-error' );
let selectedStart = null;
let selectedEnd = null;
// ── Date change → load slots ────────────────────────────────────────
if ( dateInput ) {
dateInput.addEventListener( 'change', function () {
const date = this.value;
if ( ! date ) return;
slotsGrid.innerHTML = '<em>' + esc( i18n.loading ) + '</em>';
slotsWrap.style.display = 'block';
confirmBlock.style.display = 'none';
successMsg.style.display = 'none';
errorMsg.style.display = 'none';
selectedStart = selectedEnd = null;
bookBtn.disabled = true;
post( 'woodoo_get_slots', { date } )
.then( res => {
if ( ! res.success ) {
slotsGrid.innerHTML = '<em>' + esc( i18n.error ) + '</em>';
return;
}
const slots = res.data;
if ( ! slots.length ) {
slotsGrid.innerHTML = '<em>' + esc( i18n.no_slots ) + '</em>';
confirmBlock.style.display = 'none';
return;
}
slotsGrid.innerHTML = '';
slots.forEach( slot => {
const btn = document.createElement( 'button' );
btn.type = 'button';
btn.className = 'woodoo-slot';
btn.textContent = formatTime( slot.start ) + ' ' + formatTime( slot.end );
btn.dataset.start = slot.start;
btn.dataset.end = slot.end;
btn.addEventListener( 'click', onSlotClick );
slotsGrid.appendChild( btn );
} );
} )
.catch( () => {
slotsGrid.innerHTML = '<em>' + esc( i18n.error ) + '</em>';
} );
} );
}
// ── Slot selection ───────────────────────────────────────────────────
function onSlotClick( e ) {
document.querySelectorAll( '.woodoo-slot' )
.forEach( b => b.classList.remove( 'selected' ) );
const btn = e.currentTarget;
btn.classList.add( 'selected' );
selectedStart = btn.dataset.start;
selectedEnd = btn.dataset.end;
slotLabel.textContent =
new Date( selectedStart ).toLocaleDateString( undefined, {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
} ) + ' ' + formatTime( selectedStart ) + ' ' + formatTime( selectedEnd );
confirmBlock.style.display = 'block';
bookBtn.disabled = false;
successMsg.style.display = 'none';
errorMsg.style.display = 'none';
}
// ── Confirm booking ──────────────────────────────────────────────────
if ( bookBtn ) {
bookBtn.addEventListener( 'click', function () {
if ( ! selectedStart || ! selectedEnd ) return;
bookBtn.disabled = true;
bookBtn.textContent = i18n.booking;
errorMsg.style.display = 'none';
post( 'woodoo_book_slot', {
start : selectedStart,
end : selectedEnd,
notes : notesArea ? notesArea.value : '',
} )
.then( res => {
if ( res.success ) {
successMsg.textContent = res.data.message;
successMsg.style.display = 'block';
confirmBlock.style.display = 'none';
slotsWrap.style.display = 'none';
dateInput.value = '';
selectedStart = selectedEnd = null;
// Re-fetch meetings after booking
refreshMeetings();
} else {
showError( res.data || i18n.error );
bookBtn.disabled = false;
bookBtn.textContent = i18n.book_btn;
}
} )
.catch( () => {
showError( i18n.error );
bookBtn.disabled = false;
bookBtn.textContent = i18n.book_btn;
} );
} );
}
// ── Cancel meeting ───────────────────────────────────────────────────
document.querySelectorAll( '.woodoo-cancel-meeting' ).forEach( btn => {
btn.addEventListener( 'click', function () {
if ( ! confirm( i18n.cancel_confirm ) ) return;
const eventId = this.dataset.eventId;
const card = this.closest( '.woodoo-meeting-card' );
this.disabled = true;
this.textContent = i18n.cancelling;
post( 'woodoo_cancel_meeting', { event_id: eventId } )
.then( res => {
if ( res.success ) {
card.style.opacity = '0.4';
card.style.transition = 'opacity .3s';
setTimeout( () => card.remove(), 350 );
} else {
alert( res.data || i18n.error );
this.disabled = false;
this.textContent = 'Cancel';
}
} )
.catch( () => {
this.disabled = false;
this.textContent = 'Cancel';
} );
} );
} );
// ── Refresh meetings list ────────────────────────────────────────────
function refreshMeetings() {
const listEl = document.getElementById( 'woodoo-meetings-list' );
if ( ! listEl ) return;
post( 'woodoo_get_meetings', {} )
.then( res => {
if ( ! res.success ) return;
const meetings = res.data;
if ( ! meetings.length ) {
listEl.innerHTML = '<p class="woodoo-empty">' + esc( 'No upcoming meetings scheduled.' ) + '</p>';
return;
}
let html = '<div class="woodoo-meetings-grid">';
meetings.forEach( ev => {
const start = new Date( ev.start.replace( ' ', 'T' ) );
const end = new Date( ev.stop.replace( ' ', 'T' ) );
html +=
'<div class="woodoo-meeting-card" data-event-id="' + ev.id + '">' +
'<div class="woodoo-meeting-card__date">' +
'<span class="woodoo-meeting-card__day">' + pad( start.getDate() ) + '</span>' +
'<span class="woodoo-meeting-card__month">' + monthShort( start ) + '</span>' +
'</div>' +
'<div class="woodoo-meeting-card__info">' +
'<strong>' + esc( ev.name ) + '</strong>' +
'<span class="woodoo-meeting-card__time">' +
timeStr( start ) + ' ' + timeStr( end ) +
'</span>' +
( ev.videocall_location
? '<a href="' + esc( ev.videocall_location ) + '" target="_blank" rel="noopener" class="woodoo-meeting-card__video">Join Video Call</a>'
: '' ) +
'</div>' +
'<div class="woodoo-meeting-card__actions">' +
'<button class="woodoo-cancel-meeting woodoo-btn woodoo-btn--sm woodoo-btn--outline" data-event-id="' + ev.id + '">Cancel</button>' +
'</div>' +
'</div>';
} );
html += '</div>';
listEl.innerHTML = html;
// Re-bind cancel buttons
listEl.querySelectorAll( '.woodoo-cancel-meeting' ).forEach( btn => {
btn.addEventListener( 'click', function () {
// same handler as above re-use via custom event trick
btn.dispatchEvent( new Event( 'woodoo:cancel' ) );
} );
} );
} );
}
// ── Helpers ──────────────────────────────────────────────────────────
function post( action, data ) {
const body = new URLSearchParams( Object.assign( { action, nonce }, data ) );
return fetch( ajaxUrl, { method: 'POST', body } )
.then( r => r.json() );
}
function formatTime( datetimeStr ) {
return datetimeStr.slice( 11, 16 );
}
function timeStr( d ) {
return pad( d.getHours() ) + ':' + pad( d.getMinutes() );
}
function pad( n ) { return String( n ).padStart( 2, '0' ); }
function monthShort( d ) {
return d.toLocaleDateString( undefined, { month: 'short' } );
}
function esc( str ) {
return String( str )
.replace( /&/g, '&amp;' )
.replace( /</g, '&lt;' )
.replace( />/g, '&gt;' )
.replace( /"/g, '&quot;' );
}
function showError( msg ) {
errorMsg.textContent = msg;
errorMsg.style.display = 'block';
}
} )();