Files
WooDoo/includes/class-woodoo-calendar.php
Malin 02c8fee174 fix: Spanish frontend, wider columns, currency symbols, PDF download
PDF fix:
- Replace session-cookie auth with HTTP Basic Auth (username:api_key)
  which is natively supported by Odoo 17+ report endpoints
- Validate response is actually a PDF (%PDF header check) before serving
- Return a descriptive Spanish error if HTTP code != 200 or body is not PDF

Frontend → Spanish:
- All invoice template text in Spanish (Nº Factura, Vencimiento, etc.)
- All calendar/booking template text in Spanish
- Payment state labels: Pendiente / Parcial / En cobro / Pagado / Anulado
- "Vencida" badge for overdue invoices
- Error/notice messages in Spanish across both pages

Currency symbols:
- Add currency_symbol() helper mapping ISO codes to symbols
- EUR → €, USD → $, GBP → £, etc. (25 currencies mapped)

Column widths:
- Switch invoice table to table-layout:fixed with explicit column widths
- col-number: 180px nowrap so reference never wraps across lines
- Date/due/amount/status columns all fixed-width and nowrap
- Add .woodoo-nowrap utility class

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

319 lines
12 KiB
PHP
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.
<?php
/**
* My Account Book a Meeting tab.
* Reads booked slots from Odoo calendar.event and creates new bookings.
*/
defined( 'ABSPATH' ) || exit;
class WooDoo_Calendar {
const ENDPOINT = 'book-meeting';
public static function init(): void {
add_filter( 'woocommerce_account_menu_items', [ __CLASS__, 'add_menu_item' ], 30 );
add_action( 'woocommerce_account_' . self::ENDPOINT . '_endpoint', [ __CLASS__, 'render' ] );
add_filter( 'woocommerce_get_query_vars', [ __CLASS__, 'add_query_var' ] );
add_action( 'wp_ajax_woodoo_get_slots', [ __CLASS__, 'ajax_get_slots' ] );
add_action( 'wp_ajax_woodoo_book_slot', [ __CLASS__, 'ajax_book_slot' ] );
add_action( 'wp_ajax_woodoo_get_meetings',[ __CLASS__, 'ajax_get_meetings' ] );
add_action( 'wp_ajax_woodoo_cancel_meeting', [ __CLASS__, 'ajax_cancel_meeting' ] );
}
public static function add_query_var( array $vars ): array {
$vars[ self::ENDPOINT ] = self::ENDPOINT;
return $vars;
}
public static function add_menu_item( array $items ): array {
$items[ self::ENDPOINT ] = __( 'Book a Meeting', 'woodoo' );
return $items;
}
// ── Render ────────────────────────────────────────────────────────────
public static function render(): void {
$user_id = get_current_user_id();
$partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
$user = get_userdata( $user_id );
$api = woodoo_api();
if ( ! $api ) {
echo '<p class="woodoo-notice woodoo-error">' .
'Las reservas no están disponibles en este momento. Por favor, contacta con soporte.' .
'</p>';
return;
}
// Upcoming bookings for this user
$upcoming = [];
if ( $partner_id ) {
$now = gmdate( 'Y-m-d H:i:s' );
$upcoming = $api->search_read(
'calendar.event',
[
[ 'partner_ids', 'in', [ $partner_id ] ],
[ 'start', '>=', $now ],
[ 'active', '=', true ],
],
[ 'id', 'name', 'start', 'stop', 'location', 'description', 'videocall_location' ],
20,
0,
'start asc'
);
}
include WOODOO_DIR . 'templates/myaccount-calendar.php';
}
// ── Slot Calculation ──────────────────────────────────────────────────
/**
* Returns available booking slots for a given date (Y-m-d).
* Filters out already-booked times from Odoo calendar.
*
* @return array List of ['start' => 'Y-m-d H:i', 'end' => 'Y-m-d H:i']
*/
public static function get_available_slots( string $date ): array {
$available_days = (array) get_option( 'woodoo_available_days', [ 1, 2, 3, 4, 5 ] );
$from = get_option( 'woodoo_available_from', '09:00' );
$to = get_option( 'woodoo_available_to', '17:00' );
$duration = (int) get_option( 'woodoo_meeting_duration', 30 );
$ts = strtotime( $date );
$day_of_w = (int) gmdate( 'N', $ts ); // 1=Mon … 7=Sun
if ( ! in_array( $day_of_w, array_map( 'intval', $available_days ), true ) ) {
return [];
}
// Build all slots for the day
$slots = [];
$from_ts = strtotime( $date . ' ' . $from . ':00' );
$to_ts = strtotime( $date . ' ' . $to . ':00' );
$step = $duration * 60;
for ( $start = $from_ts; $start + $step <= $to_ts; $start += $step ) {
$slots[] = [
'start' => gmdate( 'Y-m-d H:i', $start ),
'end' => gmdate( 'Y-m-d H:i', $start + $step ),
'start_ts' => $start,
'end_ts' => $start + $step,
];
}
// Fetch existing events from Odoo for this day
$api = woodoo_api();
if ( ! $api ) return $slots;
$day_start = $date . ' 00:00:00';
$day_end = $date . ' 23:59:59';
$cache_key = 'woodoo_booked_' . $date;
$booked = get_transient( $cache_key );
if ( false === $booked ) {
$booked = $api->search_read(
'calendar.event',
[
[ 'start', '>=', $day_start ],
[ 'start', '<=', $day_end ],
[ 'active', '=', true ],
],
[ 'id', 'start', 'stop' ],
100
);
set_transient( $cache_key, $booked, 120 );
}
// Remove slots that overlap with existing events
$available = array_filter( $slots, function ( $slot ) use ( $booked ) {
foreach ( $booked as $event ) {
$e_start = strtotime( $event['start'] );
$e_end = strtotime( $event['stop'] );
// Overlap: slot_start < e_end AND slot_end > e_start
if ( $slot['start_ts'] < $e_end && $slot['end_ts'] > $e_start ) {
return false;
}
}
return true;
} );
// Strip internal timestamps before returning
return array_values( array_map( fn($s) => [
'start' => $s['start'],
'end' => $s['end'],
], $available ) );
}
// ── AJAX: Get Slots ───────────────────────────────────────────────────
public static function ajax_get_slots(): void {
check_ajax_referer( 'woodoo_calendar', 'nonce' );
if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 );
$date = sanitize_text_field( wp_unslash( $_POST['date'] ?? '' ) );
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
wp_send_json_error( 'Invalid date format' );
}
// Don't allow past dates
if ( strtotime( $date ) < strtotime( 'today' ) ) {
wp_send_json_success( [] );
}
wp_send_json_success( self::get_available_slots( $date ) );
}
// ── AJAX: Book Slot ───────────────────────────────────────────────────
public static function ajax_book_slot(): void {
check_ajax_referer( 'woodoo_calendar', 'nonce' );
if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 );
$start = sanitize_text_field( wp_unslash( $_POST['start'] ?? '' ) );
$end = sanitize_text_field( wp_unslash( $_POST['end'] ?? '' ) );
$notes = sanitize_textarea_field( wp_unslash( $_POST['notes'] ?? '' ) );
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $start )
|| ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $end ) ) {
wp_send_json_error( 'Invalid date/time format' );
}
if ( strtotime( $start ) < time() ) {
wp_send_json_error( __( 'Cannot book a slot in the past.', 'woodoo' ) );
}
$user_id = get_current_user_id();
$user = get_userdata( $user_id );
$api = woodoo_api();
if ( ! $api ) wp_send_json_error( 'API not configured' );
// Find or create Odoo partner for the customer
$partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
if ( ! $partner_id ) {
$partner_id = $api->find_or_create_partner(
$user->user_email,
$user->display_name
);
if ( $partner_id ) {
update_user_meta( $user_id, 'woodoo_odoo_partner_id', $partner_id );
}
}
if ( ! $partner_id ) {
wp_send_json_error( __( 'Could not find your Odoo record.', 'woodoo' ) );
}
// Get the Odoo user (organizer) the API user
$me_uid = $api->authenticate();
// Find res.users for this uid to get partner_id of organizer
$organizer_users = $api->search_read(
'res.users',
[ [ 'id', '=', $me_uid ] ],
[ 'partner_id' ],
1
);
$organizer_partner_id = $organizer_users[0]['partner_id'][0] ?? null;
$title = get_option( 'woodoo_meeting_title_prefix', 'Meeting via WooDoo' )
. ' ' . $user->display_name;
$partner_ids = array_filter( [ $partner_id, $organizer_partner_id ] );
$event_vals = [
'name' => $title,
'start' => $start . ':00',
'stop' => $end . ':00',
'description' => $notes ?: false,
'partner_ids' => array_map( fn($id) => [ 4, $id, 0 ], $partner_ids ),
'privacy' => 'confidential',
];
$event_id = $api->create( 'calendar.event', $event_vals );
if ( ! $event_id ) {
wp_send_json_error( __( 'Could not create meeting in Odoo. Please try again.', 'woodoo' ) );
}
// Invalidate booked slots cache for this date
$date = substr( $start, 0, 10 );
delete_transient( 'woodoo_booked_' . $date );
wp_send_json_success( [
'event_id' => $event_id,
'start' => $start,
'end' => $end,
'message' => __( 'Meeting booked! You will receive a confirmation from Odoo.', 'woodoo' ),
] );
}
// ── AJAX: Get User's Meetings ─────────────────────────────────────────
public static function ajax_get_meetings(): void {
check_ajax_referer( 'woodoo_calendar', 'nonce' );
if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 );
$user_id = get_current_user_id();
$partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
if ( ! $partner_id ) wp_send_json_success( [] );
$api = woodoo_api();
if ( ! $api ) wp_send_json_error( 'API not configured' );
$now = gmdate( 'Y-m-d H:i:s' );
$meetings = $api->search_read(
'calendar.event',
[
[ 'partner_ids', 'in', [ $partner_id ] ],
[ 'start', '>=', $now ],
[ 'active', '=', true ],
],
[ 'id', 'name', 'start', 'stop', 'location', 'videocall_location' ],
20,
0,
'start asc'
);
wp_send_json_success( $meetings );
}
// ── AJAX: Cancel Meeting ──────────────────────────────────────────────
public static function ajax_cancel_meeting(): void {
check_ajax_referer( 'woodoo_calendar', 'nonce' );
if ( ! is_user_logged_in() ) wp_send_json_error( 'Not logged in', 401 );
$event_id = absint( $_POST['event_id'] ?? 0 );
$user_id = get_current_user_id();
$partner_id = (int) get_user_meta( $user_id, 'woodoo_odoo_partner_id', true );
if ( ! $event_id || ! $partner_id ) wp_send_json_error( 'Invalid request' );
$api = woodoo_api();
if ( ! $api ) wp_send_json_error( 'API not configured' );
// Verify the event belongs to this user
$events = $api->search(
'calendar.event',
[ [ 'id', '=', $event_id ], [ 'partner_ids', 'in', [ $partner_id ] ] ],
1
);
if ( empty( $events ) ) {
wp_send_json_error( __( 'Meeting not found.', 'woodoo' ) );
}
// Unlink the customer partner from the event (don't delete the event)
$api->write(
'calendar.event',
[ $event_id ],
[ 'partner_ids' => [ [ 3, $partner_id, 0 ] ] ]
);
wp_send_json_success( [ 'message' => __( 'Meeting cancelled.', 'woodoo' ) ] );
}
}