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>
This commit is contained in:
130
assets/js/woodoo-admin.js
Normal file
130
assets/js/woodoo-admin.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/* WooDoo Admin JS */
|
||||
jQuery( function ( $ ) {
|
||||
|
||||
const cfg = window.WooDooAdmin || {};
|
||||
|
||||
// ── Tab navigation ──────────────────────────────────────────────────
|
||||
$( '.woodoo-settings .nav-tab' ).on( 'click', function ( e ) {
|
||||
e.preventDefault();
|
||||
const target = $( this ).attr( 'href' );
|
||||
|
||||
$( '.woodoo-settings .nav-tab' ).removeClass( 'nav-tab-active' );
|
||||
$( this ).addClass( 'nav-tab-active' );
|
||||
|
||||
$( '.woodoo-tab' ).hide();
|
||||
$( target ).show();
|
||||
} );
|
||||
|
||||
// ── Test Connection ──────────────────────────────────────────────────
|
||||
$( '#woodoo-test-connection' ).on( 'click', function () {
|
||||
const $btn = $( this );
|
||||
const $result = $( '#woodoo-test-result' );
|
||||
|
||||
$btn.prop( 'disabled', true ).text( cfg.i18n.testing );
|
||||
$result.html( '' );
|
||||
|
||||
$.post( cfg.ajax_url, {
|
||||
action : 'woodoo_test_connection',
|
||||
nonce : cfg.nonce,
|
||||
} )
|
||||
.done( function ( res ) {
|
||||
if ( res.success ) {
|
||||
const d = res.data;
|
||||
const cls = d.success ? 'success' : 'error';
|
||||
$result.html(
|
||||
'<span class="woodoo-badge woodoo-badge--' + ( d.success ? 'green' : 'red' ) + '">' +
|
||||
escHtml( d.message ) + '</span>'
|
||||
);
|
||||
} else {
|
||||
$result.html( '<span style="color:red;">' + escHtml( res.data || cfg.i18n.error ) + '</span>' );
|
||||
}
|
||||
} )
|
||||
.fail( function () {
|
||||
$result.html( '<span style="color:red;">' + escHtml( cfg.i18n.error ) + '</span>' );
|
||||
} )
|
||||
.always( function () {
|
||||
$btn.prop( 'disabled', false ).text( 'Test Connection' );
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── Partner search on user profile ──────────────────────────────────
|
||||
$( '#woodoo-partner-search-btn' ).on( 'click', function () {
|
||||
const $input = $( '#woodoo-partner-search-input' );
|
||||
const $results = $( '#woodoo-partner-search-results' );
|
||||
const query = $input.val().trim();
|
||||
const userId = $input.data( 'user-id' );
|
||||
|
||||
if ( ! query ) return;
|
||||
|
||||
$results.html( '<em>' + escHtml( cfg.i18n.searching ) + '</em>' );
|
||||
|
||||
$.post( cfg.ajax_url, {
|
||||
action : 'woodoo_search_partners',
|
||||
nonce : cfg.nonce,
|
||||
query : query,
|
||||
} )
|
||||
.done( function ( res ) {
|
||||
if ( ! res.success || ! res.data.length ) {
|
||||
$results.html( '<em>No partners found.</em>' );
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<ul style="margin:0;padding:0;list-style:none;">';
|
||||
res.data.forEach( function ( p ) {
|
||||
html +=
|
||||
'<li style="padding:4px 0;border-bottom:1px solid #eee;">' +
|
||||
'<strong>' + escHtml( p.name ) + '</strong>' +
|
||||
( p.email ? ' <' + escHtml( p.email ) + '>' : '' ) +
|
||||
' <button type="button" class="button button-small woodoo-pick-partner"' +
|
||||
' data-id="' + parseInt( p.id, 10 ) + '"' +
|
||||
' data-name="' + escAttr( p.name ) + '"' +
|
||||
' data-user-id="' + parseInt( userId, 10 ) + '">' +
|
||||
'Select</button>' +
|
||||
'</li>';
|
||||
} );
|
||||
html += '</ul>';
|
||||
$results.html( html );
|
||||
} )
|
||||
.fail( function () {
|
||||
$results.html( '<em>' + escHtml( cfg.i18n.error ) + '</em>' );
|
||||
} );
|
||||
} );
|
||||
|
||||
// ── Pick partner from search results ────────────────────────────────
|
||||
$( document ).on( 'click', '.woodoo-pick-partner', function () {
|
||||
const $btn = $( this );
|
||||
const partnerId = $btn.data( 'id' );
|
||||
const name = $btn.data( 'name' );
|
||||
const userId = $btn.data( 'user-id' );
|
||||
|
||||
$( '#woodoo_odoo_partner_id' ).val( partnerId );
|
||||
$( '#woodoo-partner-name-display' ).text( name ).show();
|
||||
$( '#woodoo-partner-search-results' ).html(
|
||||
'<span style="color:green;">' + escHtml( cfg.i18n.link_done ) + ' → ' + escHtml( name ) + ' (#' + partnerId + ')</span>'
|
||||
);
|
||||
|
||||
// Persist immediately if we have the AJAX handler
|
||||
if ( userId ) {
|
||||
$.post( cfg.ajax_url, {
|
||||
action : 'woodoo_link_customer',
|
||||
nonce : cfg.nonce,
|
||||
user_id : userId,
|
||||
partner_id : partnerId,
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
// ── Utility ──────────────────────────────────────────────────────────
|
||||
function escHtml( str ) {
|
||||
return String( str )
|
||||
.replace( /&/g, '&' )
|
||||
.replace( /</g, '<' )
|
||||
.replace( />/g, '>' )
|
||||
.replace( /"/g, '"' );
|
||||
}
|
||||
|
||||
function escAttr( str ) {
|
||||
return escHtml( str ).replace( /'/g, ''' );
|
||||
}
|
||||
|
||||
} );
|
||||
248
assets/js/woodoo-frontend.js
Normal file
248
assets/js/woodoo-frontend.js
Normal file
@@ -0,0 +1,248 @@
|
||||
/* 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, '&' )
|
||||
.replace( /</g, '<' )
|
||||
.replace( />/g, '>' )
|
||||
.replace( /"/g, '"' );
|
||||
}
|
||||
|
||||
function showError( msg ) {
|
||||
errorMsg.textContent = msg;
|
||||
errorMsg.style.display = 'block';
|
||||
}
|
||||
|
||||
} )();
|
||||
Reference in New Issue
Block a user