Files
WooDoo/assets/js/woodoo-frontend.js
Malin c05433689e feat: invoice email resend, smaller table, column cleanup
Email resend (replaces broken PDF download):
- New AJAX action woodoo_send_invoice_email
- Finds Odoo invoice email template via ir.model.data (cached 24h)
  with fallback search on mail.template by model
- Calls mail.template.send_mail([template_id], invoice_id, force_send=True)
  via authenticated JSON-RPC — triggers Odoo to email the invoice
- Inline success/error row appears below the invoice row, auto-hides 6s
- Button shows "Enviando…" spinner state, resets on failure

Table columns:
- Remove "Saldo pendiente" column (was amount_residual)
- Rename "Total" → "Importe"
- min-width reduced to 700px now that there are 6 columns
- Font size reduced to .8rem for denser display

JS restructured so invoice email handler always runs (no early exit),
calendar handler only runs when WooDooCalendar data is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:43:39 +02:00

287 lines
14 KiB
JavaScript
Raw Permalink 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 */
( function () {
'use strict';
// ══════════════════════════════════════════════════════════════════════
// INVOICE EMAIL
// ══════════════════════════════════════════════════════════════════════
const inv = window.WooDooInvoices;
if ( inv ) {
document.querySelectorAll( '.woodoo-send-invoice' ).forEach( function ( btn ) {
btn.addEventListener( 'click', function () {
const invoiceId = this.dataset.id;
const $btn = this;
const row = $btn.closest( 'tr' );
// Remove any previous feedback in this row
const prev = row.querySelector( '.woodoo-inline-msg' );
if ( prev ) prev.remove();
$btn.disabled = true;
$btn.textContent = 'Enviando…';
const body = new URLSearchParams( {
action : 'woodoo_send_invoice_email',
nonce : inv.nonce,
invoice_id : invoiceId,
} );
fetch( inv.ajax_url, { method: 'POST', body } )
.then( r => r.json() )
.then( function ( res ) {
if ( res.success ) {
$btn.textContent = '✓ Enviado';
$btn.style.color = '#065f46';
// Show success message below the row
const td = document.createElement( 'td' );
td.colSpan = 6;
td.className = 'woodoo-inline-msg woodoo-inline-msg--ok';
td.textContent = res.data.message;
const msgRow = document.createElement( 'tr' );
msgRow.className = 'woodoo-inline-msg';
msgRow.appendChild( td );
row.insertAdjacentElement( 'afterend', msgRow );
// Auto-hide after 6s
setTimeout( function () {
msgRow.remove();
$btn.disabled = false;
$btn.textContent = '✉ Reenviar';
$btn.style.color = '';
}, 6000 );
} else {
showInvoiceError( row, res.data || 'Error desconocido.' );
$btn.disabled = false;
$btn.textContent = '✉ Reenviar';
}
} )
.catch( function () {
showInvoiceError( row, 'Error de conexión. Inténtalo de nuevo.' );
$btn.disabled = false;
$btn.textContent = '✉ Reenviar';
} );
} );
} );
function showInvoiceError( row, msg ) {
const prev = row.querySelector( '.woodoo-inline-msg' );
if ( prev ) prev.remove();
const td = document.createElement( 'td' );
td.colSpan = 6;
td.className = 'woodoo-inline-msg woodoo-inline-msg--err';
td.textContent = msg;
const msgRow = document.createElement( 'tr' );
msgRow.className = 'woodoo-inline-msg';
msgRow.appendChild( td );
row.insertAdjacentElement( 'afterend', msgRow );
}
}
// ══════════════════════════════════════════════════════════════════════
// CALENDAR BOOKING
// ══════════════════════════════════════════════════════════════════════
const cfg = window.WooDooCalendar;
if ( ! cfg ) return;
const i18n = cfg.i18n;
const ajaxUrl = cfg.ajax_url;
const nonce = cfg.nonce;
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>'; } );
} );
}
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( 'es-ES', {
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;
refreshMeetings();
} else {
showCalError( res.data || i18n.error );
bookBtn.disabled = false;
bookBtn.textContent = i18n.book_btn;
}
} )
.catch( () => {
showCalError( 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 = 'Cancelar';
}
} )
.catch( () => {
this.disabled = false;
this.textContent = 'Cancelar';
} );
} );
} );
// ── Refresh meetings list ────────────────────────────────────────────
function refreshMeetings() {
const listEl = document.getElementById( 'woodoo-meetings-list' );
if ( ! listEl ) return;
post( 'woodoo_get_meetings', {} )
.then( res => {
if ( ! res.success || ! res.data.length ) {
listEl.innerHTML = '<p class="woodoo-empty">No tienes reuniones programadas.</p>';
return;
}
const meses = [ '', 'ene', 'feb', 'mar', 'abr', 'may', 'jun',
'jul', 'ago', 'sep', 'oct', 'nov', 'dic' ];
let html = '<div class="woodoo-meetings-grid">';
res.data.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">' + ( meses[ start.getMonth() + 1 ] || '' ) + '</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">Unirse a la videollamada</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 + '">Cancelar</button>' +
'</div>' +
'</div>';
} );
html += '</div>';
listEl.innerHTML = html;
} );
}
// ── 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( dt ) { return dt.slice( 11, 16 ); }
function timeStr( d ) { return pad( d.getHours() ) + ':' + pad( d.getMinutes() ); }
function pad( n ) { return String( n ).padStart( 2, '0' ); }
function esc( str ) {
return String( str )
.replace( /&/g, '&amp;' ).replace( /</g, '&lt;' )
.replace( />/g, '&gt;' ).replace( /"/g, '&quot;' );
}
function showCalError( msg ) {
errorMsg.textContent = msg;
errorMsg.style.display = 'block';
}
} )();