Show calendar tab content
August 7, 2025 Β· AI Conversations
<style>
/* Toggle Button */
.toggle-btn {
transition: background 0.2s, color 0.2s;
}
.toggle-btn.active {
background: #00c48c !important;
color: #fff !important;
}
/* Calendar Containers */
.weekly-calendar,
.monthly-calendar {
margin: 30px 0;
font-family: Arial, sans-serif;
}
/* Table Styling */
.weekly-calendar table,
.monthly-calendar table {
width: 100%;
border-collapse: collapse;
text-align: center;
min-width: 220px;
margin-bottom: 0;
}
/* Table Headers and Cells */
.weekly-calendar th,
.weekly-calendar td,
.monthly-calendar th,
.monthly-calendar td {
border: none;
}
.weekly-calendar th {
text-align: center;
vertical-align: middle;
font-size: 12px;
}
.weekly-calendar td {
font-size: 14px !important;
font-weight: 600 !important;
}
.weekly-calendar th small {
font-size: 10px;
color: #666;
display: block;
margin-top: 2px;
}
.weekly-calendar th,
.monthly-calendar th {
}
.yith-wcbk-booking-form button#prev-week-btn ,.yith-wcbk-booking-form button#next-week-btn{
background: none;
border:none;
cursor: pointer;
}
.yith-wcbk-form-section-dates-wrapper .toggle-btn {
padding: 10px !important;
text-align: center;
font-size: 14px;
font-weight: 600;
}
.weekly-calendar .selected,
.monthly-calendar .selected {
color: #fff;
}
/* Monthly Calendar Layout */
.monthly-calendar {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.prev_month ,.next_month {
background: none;
border: none;
cursor: pointer;
}
.monthly-calendar caption {
font-weight: bold;
margin-bottom: 5px;
}
.monthly-calendar .day-checkbox {
display: none;
}
/* 2. Style the label as a box */
label[for^="day-"] {
display: inline-block;
text-align: center;
font-weight: bold;
position: relative;
cursor: pointer;
}
/* 3. Show tick icon when checked */
.day-checkbox:checked + label[for^="day-"]::after {
content: "β";
color: green;
font-size: 19px;
position: absolute;
top: -5px;
right: 0px;
background: #ccffcc;
width: 100%;
}
.monthly-calendar td:has(input.unavailable) {
background-color: #ffcccc !important;
border-radius: 5px;
padding: 10px !important;
}
.monthly-calendar td.selected{
background: #00c48c !important;
border-radius: 5px;
padding: 10px !important;
}
.monthly-calendar td:not(:first-child) {
background: #ccffcc;
border-radius: 5px;
}
/* Scroll Container */
.monthly-calendar-scroll {
width: 100%;
overflow-y: auto;
}
.monthly-calendar-scroll.scrollable {
max-height: 350px;
}
/* Calendar Block */
.strek-calendar-block {
margin-bottom: 24px;
background: #fff;
padding: 8px 0 16px 0;
}
.weekly-calendar .strek-calendar-block:last-child {
border-bottom: none;
}
.weekly-calendar td:has(input.unavailable) {
background-color: #ffcccc !important;
}
.weekly-calendar td.selected{
background: #00c48c !important;
}
/* Custom Checkbox */
.weekly-calendar .strek-checkbox {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
vertical-align: middle; /* default: available (green) */
transition: background 0.2s;
}
.weekly-calendar .strek-checkbox:checked::after {
content: "β";
display: inline-block;
color: #ffffff;
font-size: 19px;
position: relative;
left: 2px;
top: 0px;
}
.weekly-calendar .week-checkbox.strek-checkbox.unavailable::after {
content: "β"; /* Unicode cross */
color: red;
font-size: 19px;
position: absolute;
top: 0;
left: 2px;
}
.weekly-calendar .strek-checkbox.unavailable {
background: #ffcccc !important; /* unavailable (red) */
border-color: #ff8888;
border:none;
}
.weekly-calendar .strek-checkbox:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.weekly-calendar .strek-checkbox:checked {
background: #00c48c !important;
border-color: #00c48c;
border:none;
}
/* Weekly calendar specific styles */
.weekly-calendar td {
text-align: center;
vertical-align: middle;
position: relative;
padding: 0px !important;
}
.weekly-calendar td:not(:first-child){
background: #ccffcc;
border-radius: 5px;
}
.weekly-calendar tr{
border:none !important;
}
td small {
font-size: 10px;
line-height: 1.2;
margin-top: 4px;
display: none;
}
.woocommerce div.product form.cart table{
border-collapse: separate;
border-spacing: 12px;
}
</style>
)
)
<script>
let displayWeekly='<?php echo $displayWeekly; ?>';
let displayPerday='<?php echo $displayPerday; ?>';
const strekAvail = <?php echo json_encode($strek_avail); ?>;
const strekMonthIdx = {};
Object.keys(strekAvail).forEach(stekId => {
strekMonthIdx[stekId] = 0; // Start at first month for each strek
});
const allowedDays = <?php echo json_encode($product->get_allowed_start_days()); ?>; // 1=Monday, 5=Friday, etc.
console.log('Streak Availability Data:', strekAvail);
console.log('Allowed Days:', allowedDays);
document.addEventListener('DOMContentLoaded', function() {
const weekList = <?php echo json_encode($week_list); ?>;
const weeksToShow = <?php echo $weeks_to_show; ?>;
const loadedMonths = <?php echo json_encode($loaded_months); ?>;
let weekOffset = 0;
let currentMonthIdx = 0;
const notAvailableDates = <?php echo json_encode($not_available_dates); ?>;
// Helper function to check if any day in a week is unavailable
const weekHasUnavailableDates = (weekDates, notAvailableDates) => {
for (let i = 0; i < weekDates.length; i++) {
if (notAvailableDates.includes(weekDates[i])) {
return true;
}
}
return false;
};
// WEEKLY CALENDAR
const renderCalendar = () => {
let html = `<table><thead><tr><th>Stek</th>`;
// Table headers (weeks)
for (let i = 0; i < weeksToShow; i++) {
const weekIdx = weekOffset + i;
if (weekIdx >= weekList.length) break;
const w = weekList[weekIdx];
// Find allowed weekday (e.g. Friday) in this week
let allowedDate = null;
for (let d = 0; d < w.dates.length; d++) {
const dateObj = new Date(w.dates[d]);
// getDay: 0=Sunday, 1=Monday, ..., 5=Friday, 6=Saturday
// allowedDays uses 1=Monday, 7=Sunday (ISO)
// Convert to ISO format: Saturday=6 becomes 6, Sunday=0 becomes 7
const isoDay = dateObj.getDay() === 0 ? 7 : dateObj.getDay();
if (allowedDays.includes(isoDay)) {
allowedDate = w.dates[d];
break;
}
}
let thLabel = `Week ${w.week}`;
if (allowedDate) {
const dateObj = new Date(allowedDate);
const startObj = new Date(w.dates[0]);
const endObj = new Date(w.dates[w.dates.length - 1]);
const startStr = startObj.toLocaleDateString('nl-NL', { day: '2-digit', month: '2-digit' });
const endStr = endObj.toLocaleDateString('nl-NL', { day: '2-digit', month: '2-digit' });
thLabel += `<br><small>${startStr} to ${endStr}</small>`;
}
html += `<th>${thLabel}</th>`;
}
html += `</tr></thead><tbody>`;
// For each stek, create a row
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
html += `<tr><td>${stekData.label}</td>`;
for (let i = 0; i < weeksToShow; i++) {
const weekIdx = weekOffset + i;
if (weekIdx >= weekList.length) break;
const w = weekList[weekIdx];
// Find allowed weekday in this week
let allowedDate = null;
for (let d = 0; d < w.dates.length; d++) {
const dateObj = new Date(w.dates[d]);
// Convert to ISO format: Saturday=6 becomes 6, Sunday=0 becomes 7
const isoDay = dateObj.getDay() === 0 ? 7 : dateObj.getDay();
if (allowedDays.includes(isoDay)) {
allowedDate = w.dates[d];
break;
}
}
// Check if this specific date is available for this specific stek
const isAvailable = allowedDate && stekData.available_dates.includes(allowedDate);
const unavailableClass = isAvailable ? '' : 'unavailable';
// Debug logging for this specific check
if (allowedDate) {
console.log(`Week ${w.week}: ${stekData.label} - Date: ${allowedDate}, Available: ${isAvailable}`);
// Show specific example for August 2nd
if (allowedDate === '2024-08-02') {
console.log(`π― August 2nd found! Week: ${w.week}, Resource: ${stekData.label}, Available: ${isAvailable}`);
}
}
html += `<td style="padding: 20px 10px;">
<input type="checkbox" class="week-checkbox strek-checkbox ${unavailableClass}"
id="week-check-${stekId}-${weekIdx}"
data-stek="${stekId}"
data-week="${w.week}"
data-year="${w.year}"
data-date="${allowedDate ? allowedDate : ''}"
${isAvailable ? '' : 'disabled'}>
${allowedDate
? `<br><small>${new Date(allowedDate).toLocaleDateString('nl-NL', { weekday: 'short', day: '2-digit', month: '2-digit' })}</small>`
: '<br><small>n.v.t.</small>'}
${isAvailable ? '<br><small style="color: green;">β Available</small>' : '<br><small style="color: red;">β Unavailable</small>'}
</td>`;
}
html += `</tr>`;
});
html += `</tbody></table>`;
document.getElementById('calendar-table-wrapper').innerHTML = html;
// Update navigation label
const navLabel = document.getElementById('week-navigation-label');
if (navLabel && weekList.length > 0) {
const firstWeek = weekList[weekOffset];
const lastWeek = weekList[Math.min(weekOffset + weeksToShow - 1, weekList.length - 1)];
if (firstWeek && lastWeek) {
const firstDate = new Date(firstWeek.start);
const lastDate = new Date(lastWeek.start);
lastDate.setDate(lastDate.getDate() + 6);
navLabel.textContent = `Weeks ${firstWeek.week} - ${lastWeek.week} (${firstDate.toLocaleDateString('nl-NL')} - ${lastDate.toLocaleDateString('nl-NL')})`;
}
}
// Selection highlight for checkboxes and event handlers
document.querySelectorAll('.weekly-calendar .week-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => {
const td = checkbox.closest('td');
if (checkbox.checked) {
// Uncheck all other week checkboxes across all streks
document.querySelectorAll('.weekly-calendar .week-checkbox').forEach(cb => {
if (cb !== checkbox) {
cb.checked = false;
cb.closest('td').classList.remove('selected');
}
});
td.classList.add('selected');
// Update the booking form fields
const strekId = checkbox.getAttribute('data-stek');
const productId = "<?php echo $product->get_id(); ?>";
const selectedDate = checkbox.getAttribute('data-date');
// Set the check-in (start date) field
const startDateInput = document.getElementById(`yith-wcbk-booking-start-date-${productId}`);
if (startDateInput) {
startDateInput.value = selectedDate;
startDateInput.dispatchEvent(new Event('input', { bubbles: true }));
startDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(startDateInput).datepicker) {
jQuery(startDateInput).datepicker('setDate', selectedDate);
}
}
// Set the check-out (end date) field to 7 days after check-in
const endDateInput = document.getElementById(`yith-wcbk-booking-end-date-${productId}`);
if (endDateInput) {
const dateObj = new Date(selectedDate);
dateObj.setDate(dateObj.getDate() + 7); // 7 days (week)
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
const dd = String(dateObj.getDate()).padStart(2, '0');
const endDate = `${yyyy}-${mm}-${dd}`;
endDateInput.value = endDate;
endDateInput.dispatchEvent(new Event('input', { bubbles: true }));
endDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(endDateInput).datepicker) {
jQuery(endDateInput).datepicker('setDate', endDate);
}
}
// Update strek dropdown
const select = document.querySelector(`#yith-wcbk-booking-resources-${productId} select.yith-wcbk-selector__field`);
if (select) {
select.value = strekId;
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
const customItem = document.querySelector(`#yith-wcbk-booking-resources-${productId} .yith-wcbk-selector__item[data-key="${strekId}"]`);
if (customItem) {
customItem.click();
}
}
} else {
td.classList.remove('selected');
}
});
});
};
document.getElementById('prev-week-btn')?.remove();
document.getElementById('next-week-btn')?.remove();
const navDiv = document.createElement('div');
navDiv.style.display = "flex";
navDiv.style.justifyContent = "center";
navDiv.style.marginBottom = "10px";
navDiv.innerHTML = `
<button type="button" id="prev-week-btn" style="display:<?php echo $displayWeekly; ?>;"> <img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/left-chevron-svgrepo-com.svg" alt="Previous" width="25" height="25"> </button>
<div style="flex: 1; text-align: center; font-weight: bold;display:<?php echo $displayWeekly; ?>;" id="week-navigation-label">Select a week</div>
<button type="button" id="next-week-btn" style="display:<?php echo $displayWeekly; ?>;"> <img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/right-chevron-svgrepo-com.svg" alt="Previous" width="25" height="25"> </button>
`;
document.getElementById('calendar-table-wrapper').before(navDiv);
document.getElementById('prev-week-btn').addEventListener('click', () => {
if (weekOffset - weeksToShow >= 0) {
weekOffset -= weeksToShow;
} else {
weekOffset = 0;
}
renderCalendar();
});
document.getElementById('next-week-btn').addEventListener('click', () => {
if (weekOffset + weeksToShow < weekList.length) {
weekOffset += weeksToShow;
renderCalendar();
}
});
// MONTHLY CALENDAR (one month at a time)
const renderMonthlyCalendar = () => {
if (loadedMonths.length === 0) {
document.getElementById('monthly-calendar-scroll').innerHTML = '<p>No months available.</p>';
document.getElementById('monthly-calendar-controls').style.display = 'none';
return;
}
document.getElementById('monthly-calendar-controls').style.display = 'none'; // Hide global controls
let html = '';
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
html += renderStrekMonthBlock(stekId, stekData);
});
document.getElementById('monthly-calendar-scroll').innerHTML = html;
// Add event listeners for each strek's navigation
Object.keys(strekAvail).forEach(stekId => {
document.getElementById(`prev-month-btn-${stekId}`).addEventListener('click', () => {
if (strekMonthIdx[stekId] > 0) {
strekMonthIdx[stekId]--;
renderMonthlyCalendar();
}
});
document.getElementById(`next-month-btn-${stekId}`).addEventListener('click', () => {
if (strekMonthIdx[stekId] < loadedMonths.length - 1) {
strekMonthIdx[stekId]++;
renderMonthlyCalendar();
}
});
});
// Add scrollable class if needed
const strekCount = Object.keys(strekAvail).length;
const scrollDiv = document.getElementById('monthly-calendar-scroll');
if (strekCount > 2) {
scrollDiv.classList.add('scrollable');
} else {
scrollDiv.classList.remove('scrollable');
}
};
const renderStrekMonthBlock = (stekId, stekData) => {
const idx = strekMonthIdx[stekId];
const [year, month] = loadedMonths[idx].split('-').map(Number);
const availableDates = Array.isArray(stekData.available_dates) ? stekData.available_dates : [];
// Get first and last day of month
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const daysInMonth = lastDay.getDate();
let html = `<div class="strek-calendar-block">
<div style="font-weight:bold; margin-bottom:20px;">${stekData.label}</div>
<div style="text-align:center; margin-bottom:10px; display:flex; align-items: center; justify-content: space-between;">
<button type="button" id="prev-month-btn-${stekId}" class="prev_month"><img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/left-chevron-svgrepo-com.svg" alt="Next" width="25" height="25"> </button>
<span style="font-weight:bold; margin:0 10px;">${firstDay.toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
<button type="button" id="next-month-btn-${stekId}" class="next_month"><img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/right-chevron-svgrepo-com.svg" alt="Next" width="25" height="25"></button>
</div>
<table>
<thead>
<tr>
<th>Sa</th><th>Su</th><th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th>
</tr>
</thead>
<tbody>
<tr>`;
// Calculate the day of week for the 1st (Saturday=0)
let dayOfWeek = (firstDay.getDay() + 1) % 7;
for (let i = 0; i < dayOfWeek; i++) html += '<td></td>';
for (let day = 1; day <= daysInMonth; day++) {
if ((dayOfWeek % 7 === 0) && day !== 1) html += '</tr><tr>';
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const isAvailable = availableDates.includes(dateStr);
const unavailableClass = isAvailable ? '' : 'unavailable';
const disabledAttr = isAvailable ? '' : 'disabled';
html += `<td>
<input type="checkbox" class="day-checkbox strek-checkbox ${unavailableClass}"
id="day-${stekId}-${year}-${month}-${day}"
data-date="${dateStr}"
data-stek="${stekId}"
${disabledAttr}>
<label for="day-${stekId}-${year}-${month}-${day}">${day}</label>
</td>`;
dayOfWeek++;
}
while (dayOfWeek % 7 !== 0) { html += '<td></td>'; dayOfWeek++; }
html += '</tr></tbody></table></div>';
return html;
};
const renderSingleMonth = (year, month) => {
// For each strek, render a classic calendar
let html = '';
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
// Get first and last day of month
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const daysInMonth = lastDay.getDate();
html += `<div class="strek-calendar-block">
<div style="font-weight:bold; margin-bottom:4px;">${stekData.label}</div>
<table>
<caption>${firstDay.toLocaleString('default', { month: 'long' })} ${year}</caption>
<thead>
<tr>
<th>Sa</th><th>Su</th><th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th>
</tr>
</thead>
<tbody>
<tr>`;
// Calculate the day of week for the 1st (Saturday=0)
let dayOfWeek = (firstDay.getDay() + 1) % 7;
for (let i = 0; i < dayOfWeek; i++) html += '<td></td>';
for (let day = 1; day <= daysInMonth; day++) {
if ((dayOfWeek % 7 === 0) && day !== 1) html += '</tr><tr>';
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
html += `<td>
<input type="checkbox" class="day-checkbox strek-checkbox" id="day-${stekId}-${year}-${month}-${day}" data-date="${dateStr}" data-stek="${stekId}">
<label for="day-${stekId}-${year}-${month}-${day}">${day}</label>
</td>`;
dayOfWeek++;
}
while (dayOfWeek % 7 !== 0) { html += '<td></td>'; dayOfWeek++; }
html += '</tr></tbody></table></div>';
});
return html;
};
// TOGGLE LOGIC
document.getElementById('toggle-week').addEventListener('click', function() {
document.getElementById('toggle-week').classList.add('active');
document.getElementById('toggle-day').classList.remove('active');
document.getElementById('calendar-table-wrapper').style.display = '';
document.getElementById('monthly-calendar-wrapper').style.display = 'none';
document.getElementById('monthly-calendar-controls').style.display = 'none';
navDiv.style.display = '';
renderCalendar();
});
document.getElementById('toggle-day').addEventListener('click', function() {
document.getElementById('toggle-day').classList.add('active');
document.getElementById('toggle-week').classList.remove('active');
document.getElementById('calendar-table-wrapper').style.display = 'none';
document.getElementById('monthly-calendar-wrapper').style.display = '';
document.getElementById('monthly-calendar-controls').style.display = '';
navDiv.style.display = 'none';
renderMonthlyCalendar();
});
// Initial render
if(displayWeekly=='block'){
renderCalendar();
}else{
renderMonthlyCalendar();
}
document.getElementById('prev-month-btn').addEventListener('click', () => {
if (currentMonthIdx > 0) {
currentMonthIdx--;
renderMonthlyCalendar();
}
});
document.getElementById('next-month-btn').addEventListener('click', () => {
if (currentMonthIdx < loadedMonths.length - 1) {
currentMonthIdx++;
renderMonthlyCalendar();
}
});
document.addEventListener('change', function(e) {
if (
e.target.classList.contains('day-checkbox') &&
e.target.checked &&
!e.target.disabled
) {
// Uncheck all other day-checkboxes
document.querySelectorAll('.day-checkbox').forEach(cb => {
if (cb !== e.target) cb.checked = false;
});
const strekId = e.target.getAttribute('data-stek');
const productId = "<?php echo $product->get_id(); ?>";
const selectedDate = e.target.getAttribute('data-date');
// Set the check-in (start date) field and fire change event
const startDateInput = document.getElementById(`yith-wcbk-booking-start-date-${productId}`);
startDateInput.value = selectedDate;
// Fire both input and change events
startDateInput.dispatchEvent(new Event('input', { bubbles: true }));
startDateInput.dispatchEvent(new Event('change', { bubbles: true }));
// If a jQuery datepicker is attached, update it as well
if (typeof jQuery !== 'undefined' && jQuery(startDateInput).datepicker) {
jQuery(startDateInput).datepicker('setDate', selectedDate);
}
// Set the check-out (end date) field to one day after check-in
const endDateInput = document.getElementById(`yith-wcbk-booking-end-date-${productId}`);
if (endDateInput) {
// Calculate next day
const dateObj = new Date(selectedDate);
dateObj.setDate(dateObj.getDate() + 1);
// Format as yyyy-mm-dd
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
const dd = String(dateObj.getDate()).padStart(2, '0');
const nextDay = `${yyyy}-${mm}-${dd}`;
endDateInput.value = nextDay;
endDateInput.dispatchEvent(new Event('input', { bubbles: true }));
endDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(endDateInput).datepicker) {
jQuery(endDateInput).datepicker('setDate', nextDay);
}
}
// Strek dropdown logic (as before)
const select = document.querySelector(`#yith-wcbk-booking-resources-${productId} select.yith-wcbk-selector__field`);
if (select) {
select.value = strekId;
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
const customItem = document.querySelector(`#yith-wcbk-booking-resources-${productId} .yith-wcbk-selector__item[data-key="${strekId}"]`);
if (customItem) {
customItem.click();
}
}
}
});
});
</script>
—
### β Wrap the `add_calendar_tab_content()` output in a container with `display:none`, and only show it when the tab is active.
But WordPress **automatically shows the tab content container if it has the ID defined in the `target` key**. So we just need to:
—
### π οΈ Step-by-step Fix
#### 1. **Wrap your content output with a proper container** and set its `id` to `calendar_tab_data`, which matches `'target' => 'calendar_tab_data'` in your tab definition.
Replace this:
public function add_calendar_tab_content() {
With this:
public function add_calendar_tab_content() {
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
And **at the end** of your function (before the closing `}`), add:
echo '</div>'; // Close the panel div
}
—
### π§ Why it works
WooCommerce uses jQuery to show the content of only the selected tab (matching the `.panel` with the same ID as `'target'`). So WooCommerce will handle `display: block` only when the tab is clicked.
—
### β Final Result
At the beginning of `add_calendar_tab_content()`:
public function add_calendar_tab_content() {
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
// ... all your existing code ...
At the end:
echo '</div>';
}
—
Now your calendar tab data will **only be rendered when the Calendar tab is opened**, improving performance and preventing UI clutter on initial page load.
Let me know if you also want to **lazy-load content via AJAX** instead of rendering it immediately inside the tab.
I have wrapped the content inside:
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
// ... your full calendar logic ...
echo '</div>';
—
### β Full Code With Fix Applied
You only need to update the `add_calendar_tab_content()` method inside your class:
public function add_calendar_tab_content() {
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
global $post;
$product = wc_get_product( get_the_ID() );
$slug = $product->get_slug();
// [ .. keep all your existing calendar logic, variables, HTML, styles, and JS .. ]
// From: $perday_product_url = 'javascript:void(0)';
// All the way down to your JavaScript render logic, etc.
// Important: everything from your original function should remain the same.
// Just ensure it's enclosed inside this panel wrapper.
echo '</div>';
}
—
### π§ Why This Works
WooCommerce expects each tab content to be in a container with:
<div id="{target}" class="panel woocommerce_options_panel" style="display:none;">
Where `{target}` matches the `'target'` key you registered in your `add_calendar_tab()` function (`'calendar_tab_data'` in your case).
WooCommerce will then show this panel only when the tab is clicked.
—
Let me know if you want to **lazy-load the calendar using AJAX** when the tab is clicked (which helps performance if rendering is heavy).
—
### β How to Check if YITH Booking Plugin is Active
The main class for the premium plugin is usually:
class_exists( 'YITH_WCBK' )
So, apply this logic in the constructor before adding filters.
—
### π§ Full Modified Plugin Code (with both `style="display:none;"` and class check)
Update your plugin like this:
<?php
/**
* Plugin Name: Booking Addon
* Description: A plugin to add a calendar to the booking product.
* Version: 1.0.0
* Author: Carplakes
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class Booking_Addon {
public function __construct() {
// Only proceed if YITH Booking Premium plugin is active
if ( class_exists( 'YITH_WCBK' ) ) {
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_calendar_tab' ) );
add_action( 'woocommerce_product_data_panels', array( $this, 'add_calendar_tab_content' ) );
}
}
public function add_calendar_tab( $tabs ) {
$tabs['calendar_tab'] = array(
'label' => __( 'Calendar', 'your-textdomain' ),
'target' => 'calendar_tab_data',
'class' => array(),
'priority' => 99,
);
return $tabs;
}
public function add_calendar_tab_content() {
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
// Your full existing logic goes here...
// [ Leave everything inside this function exactly as it was ]
// Just make sure the entire content is enclosed in the opening and closing `echo` lines
echo '</div>'; // close panel
}
}
// Initialize plugin
new Booking_Addon();
—
### β Summary of Changes
| Feature | Implementation |
|—————————-|————————————————–|
| Only run if YITH Booking is active | `if ( class_exists( 'YITH_WCBK' ) )` |
| Tab content hidden until clicked | `style="display:none;"` in the wrapper div |
This makes your plugin safe and optimized.
Let me know if you also want to display an **admin notice** if the YITH plugin is missing.
<style>
/* Toggle Button */
.toggle-btn {
transition: background 0.2s, color 0.2s;
}
.toggle-btn.active {
background: #00c48c !important;
color: #fff !important;
}
/* Calendar Containers */
.weekly-calendar,
.monthly-calendar {
margin: 30px 0;
font-family: Arial, sans-serif;
}
/* Table Styling */
.weekly-calendar table,
.monthly-calendar table {
width: 100%;
border-collapse: collapse;
text-align: center;
min-width: 220px;
margin-bottom: 0;
}
/* Table Headers and Cells */
.weekly-calendar th,
.weekly-calendar td,
.monthly-calendar th,
.monthly-calendar td {
border: none;
}
.weekly-calendar th {
text-align: center;
vertical-align: middle;
font-size: 12px;
}
.weekly-calendar td {
font-size: 14px !important;
font-weight: 600 !important;
}
.weekly-calendar th small {
font-size: 10px;
color: #666;
display: block;
margin-top: 2px;
}
.weekly-calendar th,
.monthly-calendar th {
}
.yith-wcbk-booking-form button#prev-week-btn ,.yith-wcbk-booking-form button#next-week-btn{
background: none;
border:none;
cursor: pointer;
}
.yith-wcbk-form-section-dates-wrapper .toggle-btn {
padding: 10px !important;
text-align: center;
font-size: 14px;
font-weight: 600;
}
.weekly-calendar .selected,
.monthly-calendar .selected {
color: #fff;
}
/* Monthly Calendar Layout */
.monthly-calendar {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.prev_month ,.next_month {
background: none;
border: none;
cursor: pointer;
}
.monthly-calendar caption {
font-weight: bold;
margin-bottom: 5px;
}
.monthly-calendar .day-checkbox {
display: none;
}
/* 2. Style the label as a box */
label[for^="day-"] {
display: inline-block;
text-align: center;
font-weight: bold;
position: relative;
cursor: pointer;
}
/* 3. Show tick icon when checked */
.day-checkbox:checked + label[for^="day-"]::after {
content: "β";
color: green;
font-size: 19px;
position: absolute;
top: -5px;
right: 0px;
background: #ccffcc;
width: 100%;
}
.monthly-calendar td:has(input.unavailable) {
background-color: #ffcccc !important;
border-radius: 5px;
padding: 10px !important;
}
.monthly-calendar td.selected{
background: #00c48c !important;
border-radius: 5px;
padding: 10px !important;
}
.monthly-calendar td:not(:first-child) {
background: #ccffcc;
border-radius: 5px;
}
/* Scroll Container */
.monthly-calendar-scroll {
width: 100%;
overflow-y: auto;
}
.monthly-calendar-scroll.scrollable {
max-height: 350px;
}
/* Calendar Block */
.strek-calendar-block {
margin-bottom: 24px;
background: #fff;
padding: 8px 0 16px 0;
}
.weekly-calendar .strek-calendar-block:last-child {
border-bottom: none;
}
.weekly-calendar td:has(input.unavailable) {
background-color: #ffcccc !important;
}
.weekly-calendar td.selected{
background: #00c48c !important;
}
/* Custom Checkbox */
.weekly-calendar .strek-checkbox {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
vertical-align: middle; /* default: available (green) */
transition: background 0.2s;
}
.weekly-calendar .strek-checkbox:checked::after {
content: "β";
display: inline-block;
color: #ffffff;
font-size: 19px;
position: relative;
left: 2px;
top: 0px;
}
.weekly-calendar .week-checkbox.strek-checkbox.unavailable::after {
content: "β"; /* Unicode cross */
color: red;
font-size: 19px;
position: absolute;
top: 0;
left: 2px;
}
.weekly-calendar .strek-checkbox.unavailable {
background: #ffcccc !importantadd_calendar_tab_content; /* unavailable (red) */
border-color: #ff8888;
border:none;
}
.weekly-calendar .strek-checkbox:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.weekly-calendar .strek-checkbox:checked {
background: #00c48c !important;
border-color: #00c48c;
border:none;
}
/* Weekly calendar specific styles */
.weekly-calendar td {
text-align: center;
vertical-align: middle;
position: relative;
padding: 0px !important;
}
.weekly-calendar td:not(:first-child){
background: #ccffcc;
border-radius: 5px;
}
.weekly-calendar tr{
border:none !important;
}
td small {
font-size: 10px;
line-height: 1.2;
margin-top: 4px;
display: none;
}
.woocommerce div.product form.cart table{
border-collapse: separate;
border-spacing: 12px;
}
</style>
)
)
<script>
let displayWeekly='<?php echo $displayWeekly; ?>';
let displayPerday='<?php echo $displayPerday; ?>';
const strekAvail = <?php echo json_encode($strek_avail); ?>;
const strekMonthIdx = {};
Object.keys(strekAvail).forEach(stekId => {
strekMonthIdx[stekId] = 0; // Start at first month for each strek
});
const allowedDays = <?php echo json_encode($product->get_allowed_start_days()); ?>; // 1=Monday, 5=Friday, etc.
console.log('Streak Availability Data:', strekAvail);
console.log('Allowed Days:', allowedDays);
document.addEventListener('DOMContentLoaded', function() {
const weekList = <?php echo json_encode($week_list); ?>;
const weeksToShow = <?php echo $weeks_to_show; ?>;
const loadedMonths = <?php echo json_encode($loaded_months); ?>;
let weekOffset = 0;
let currentMonthIdx = 0;
const notAvailableDates = <?php echo json_encode($not_available_dates); ?>;
// Helper function to check if any day in a week is unavailable
const weekHasUnavailableDates = (weekDates, notAvailableDates) => {
for (let i = 0; i < weekDates.length; i++) {
if (notAvailableDates.includes(weekDates[i])) {
return true;
}
}
return false;
};
// WEEKLY CALENDAR
const renderCalendar = () => {
let html = `<table><thead><tr><th>Stek</th>`;
// Table headers (weeks)
for (let i = 0; i < weeksToShow; i++) {
const weekIdx = weekOffset + i;
if (weekIdx >= weekList.length) break;
const w = weekList[weekIdx];
// Find allowed weekday (e.g. Friday) in this week
let allowedDate = null;
for (let d = 0; d < w.dates.length; d++) {
const dateObj = new Date(w.dates[d]);
// getDay: 0=Sunday, 1=Monday, ..., 5=Friday, 6=Saturday
// allowedDays uses 1=Monday, 7=Sunday (ISO)
// Convert to ISO format: Saturday=6 becomes 6, Sunday=0 becomes 7
const isoDay = dateObj.getDay() === 0 ? 7 : dateObj.getDay();
if (allowedDays.includes(isoDay)) {
allowedDate = w.dates[d];
break;
}
}
let thLabel = `Week ${w.week}`;
if (allowedDate) {
const dateObj = new Date(allowedDate);
const startObj = new Date(w.dates[0]);
const endObj = new Date(w.dates[w.dates.length - 1]);
const startStr = startObj.toLocaleDateString('nl-NL', { day: '2-digit', month: '2-digit' });
const endStr = endObj.toLocaleDateString('nl-NL', { day: '2-digit', month: '2-digit' });
thLabel += `<br><small>${startStr} to ${endStr}</small>`;
}
html += `<th>${thLabel}</th>`;
}
html += `</tr></thead><tbody>`;
// For each stek, create a row
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
html += `<tr><td>${stekData.label}</td>`;
for (let i = 0; i < weeksToShow; i++) {
const weekIdx = weekOffset + i;
if (weekIdx >= weekList.length) break;
const w = weekList[weekIdx];
// Find allowed weekday in this week
let allowedDate = null;
for (let d = 0; d < w.dates.length; d++) {
const dateObj = new Date(w.dates[d]);
// Convert to ISO format: Saturday=6 becomes 6, Sunday=0 becomes 7
const isoDay = dateObj.getDay() === 0 ? 7 : dateObj.getDay();
if (allowedDays.includes(isoDay)) {
allowedDate = w.dates[d];
break;
}
}
// Check if this specific date is available for this specific stek
const isAvailable = allowedDate && stekData.available_dates.includes(allowedDate);
const unavailableClass = isAvailable ? '' : 'unavailable';
// Debug logging for this specific check
if (allowedDate) {
console.log(`Week ${w.week}: ${stekData.label} - Date: ${allowedDate}, Available: ${isAvailable}`);
// Show specific example for August 2nd
if (allowedDate === '2024-08-02') {
console.log(`π― August 2nd found! Week: ${w.week}, Resource: ${stekData.label}, Available: ${isAvailable}`);
}
}
html += `<td style="padding: 20px 10px;">
<input type="checkbox" class="week-checkbox strek-checkbox ${unavailableClass}"
id="week-check-${stekId}-${weekIdx}"
data-stek="${stekId}"
data-week="${w.week}"
data-year="${w.year}"
data-date="${allowedDate ? allowedDate : ''}"
${isAvailable ? '' : 'disabled'}>
${allowedDate
? `<br><small>${new Date(allowedDate).toLocaleDateString('nl-NL', { weekday: 'short', day: '2-digit', month: '2-digit' })}</small>`
: '<br><small>n.v.t.</small>'}
${isAvailable ? '<br><small style="color: green;">β Available</small>' : '<br><small style="color: red;">β Unavailable</small>'}
</td>`;
}
html += `</tr>`;
});
html += `</tbody></table>`;
document.getElementById('calendar-table-wrapper').innerHTML = html;
// Update navigation label
const navLabel = document.getElementById('week-navigation-label');
if (navLabel && weekList.length > 0) {
const firstWeek = weekList[weekOffset];
const lastWeek = weekList[Math.min(weekOffset + weeksToShow - 1, weekList.length - 1)];
if (firstWeek && lastWeek) {
const firstDate = new Date(firstWeek.start);
const lastDate = new Date(lastWeek.start);
lastDate.setDate(lastDate.getDate() + 6);
navLabel.textContent = `Weeks ${firstWeek.week} - ${lastWeek.week} (${firstDate.toLocaleDateString('nl-NL')} - ${lastDate.toLocaleDateString('nl-NL')})`;
}
}
// Selection highlight for checkboxes and event handlers
document.querySelectorAll('.weekly-calendar .week-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => {
const td = checkbox.closest('td');
if (checkbox.checked) {
// Uncheck all other week checkboxes across all streks
document.querySelectorAll('.weekly-calendar .week-checkbox').forEach(cb => {
if (cb !== checkbox) {
cb.checked = false;
cb.closest('td').classList.remove('selected');
}
});
td.classList.add('selected');
// Update the booking form fields
const strekId = checkbox.getAttribute('data-stek');
const productId = "<?php echo $product->get_id(); ?>";
const selectedDate = checkbox.getAttribute('data-date');
// Set the check-in (start date) field
const startDateInput = document.getElementById(`yith-wcbk-booking-start-date-${productId}`);
if (startDateInput) {
startDateInput.value = selectedDate;
startDateInput.dispatchEvent(new Event('input', { bubbles: true }));
startDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(startDateInput).datepicker) {
jQuery(startDateInput).datepicker('setDate', selectedDate);
}
}
// Set the check-out (end date) field to 7 days after check-in
const endDateInput = document.getElementById(`yith-wcbk-booking-end-date-${productId}`);
if (endDateInput) {
const dateObj = new Date(selectedDate);
dateObj.setDate(dateObj.getDate() + 7); // 7 days (week)
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
const dd = String(dateObj.getDate()).padStart(2, '0');
const endDate = `${yyyy}-${mm}-${dd}`;
endDateInput.value = endDate;
endDateInput.dispatchEvent(new Event('input', { bubbles: true }));
endDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(endDateInput).datepicker) {
jQuery(endDateInput).datepicker('setDate', endDate);
}
}
// Update strek dropdown
const select = document.querySelector(`#yith-wcbk-booking-resources-${productId} select.yith-wcbk-selector__field`);
if (select) {
select.value = strekId;
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
const customItem = document.querySelector(`#yith-wcbk-booking-resources-${productId} .yith-wcbk-selector__item[data-key="${strekId}"]`);
if (customItem) {
customItem.click();
}
}
} else {
td.classList.remove('selected');
}
});
});
};
document.getElementById('prev-week-btn')?.remove();
document.getElementById('next-week-btn')?.remove();
const navDiv = document.createElement('div');
navDiv.style.display = "flex";
navDiv.style.justifyContent = "center";
navDiv.style.marginBottom = "10px";
navDiv.innerHTML = `
<button type="button" id="prev-week-btn" style="display:<?php echo $displayWeekly; ?>;"> <img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/left-chevron-svgrepo-com.svg" alt="Previous" width="25" height="25"> </button>
<div style="flex: 1; text-align: center; font-weight: bold;display:<?php echo $displayWeekly; ?>;" id="week-navigation-label">Select a week</div>
<button type="button" id="next-week-btn" style="display:<?php echo $displayWeekly; ?>;"> <img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/right-chevron-svgrepo-com.svg" alt="Previous" width="25" height="25"> </button>
`;
document.getElementById('calendar-table-wrapper').before(navDiv);
document.getElementById('prev-week-btn').addEventListener('click', () => {
if (weekOffset - weeksToShow >= 0) {
weekOffset -= weeksToShow;
} else {
weekOffset = 0;
}
renderCalendar();
});
document.getElementById('next-week-btn').addEventListener('click', () => {
if (weekOffset + weeksToShow < weekList.length) {
weekOffset += weeksToShow;
renderCalendar();
}
});
// MONTHLY CALENDAR (one month at a time)
const renderMonthlyCalendar = () => {
if (loadedMonths.length === 0) {
document.getElementById('monthly-calendar-scroll').innerHTML = '<p>No months available.</p>';
document.getElementById('monthly-calendar-controls').style.display = 'none';
return;
}
document.getElementById('monthly-calendar-controls').style.display = 'none'; // Hide global controls
let html = '';
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
html += renderStrekMonthBlock(stekId, stekData);
});
document.getElementById('monthly-calendar-scroll').innerHTML = html;
// Add event listeners for each strek's navigation
Object.keys(strekAvail).forEach(stekId => {
document.getElementById(`prev-month-btn-${stekId}`).addEventListener('click', () => {
if (strekMonthIdx[stekId] > 0) {
strekMonthIdx[stekId]--;
renderMonthlyCalendar();
}
});
document.getElementById(`next-month-btn-${stekId}`).addEventListener('click', () => {
if (strekMonthIdx[stekId] < loadedMonths.length - 1) {
strekMonthIdx[stekId]++;
renderMonthlyCalendar();
}
});
});
// Add scrollable class if needed
const strekCount = Object.keys(strekAvail).length;
const scrollDiv = document.getElementById('monthly-calendar-scroll');
if (strekCount > 2) {
scrollDiv.classList.add('scrollable');
} else {
scrollDiv.classList.remove('scrollable');
}
};
const renderStrekMonthBlock = (stekId, stekData) => {
const idx = strekMonthIdx[stekId];
const [year, month] = loadedMonths[idx].split('-').map(Number);
const availableDates = Array.isArray(stekData.available_dates) ? stekData.available_dates : [];
// Get first and last day of month
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const daysInMonth = lastDay.getDate();
let html = `<div class="strek-calendar-block">
<div style="font-weight:bold; margin-bottom:20px;">${stekData.label}</div>
<div style="text-align:center; margin-bottom:10px; display:flex; align-items: center; justify-content: space-between;">
<button type="button" id="prev-month-btn-${stekId}" class="prev_month"><img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/left-chevron-svgrepo-com.svg" alt="Next" width="25" height="25"> </button>
<span style="font-weight:bold; margin:0 10px;">${firstDay.toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
<button type="button" id="next-month-btn-${stekId}" class="next_month"><img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/right-chevron-svgrepo-com.svg" alt="Next" width="25" height="25"></button>
</div>
<table>
<thead>
<tr>
<th>Sa</th><th>Su</th><th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th>
</tr>
</thead>
<tbody>
<tr>`;
// Calculate the day of week for the 1st (Saturday=0)
let dayOfWeek = (firstDay.getDay() + 1) % 7;
for (let i = 0; i < dayOfWeek; i++) html += '<td></td>';
for (let day = 1; day <= daysInMonth; day++) {
if ((dayOfWeek % 7 === 0) && day !== 1) html += '</tr><tr>';
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const isAvailable = availableDates.includes(dateStr);
const unavailableClass = isAvailable ? '' : 'unavailable';
const disabledAttr = isAvailable ? '' : 'disabled';
html += `<td>
<input type="checkbox" class="day-checkbox strek-checkbox ${unavailableClass}"
id="day-${stekId}-${year}-${month}-${day}"
data-date="${dateStr}"
data-stek="${stekId}"
${disabledAttr}>
<label for="day-${stekId}-${year}-${month}-${day}">${day}</label>
</td>`;
dayOfWeek++;
}
while (dayOfWeek % 7 !== 0) { html += '<td></td>'; dayOfWeek++; }
html += '</tr></tbody></table></div>';
return html;
};
const renderSingleMonth = (year, month) => {
// For each strek, render a classic calendar
let html = '';
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
// Get first and last day of month
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const daysInMonth = lastDay.getDate();
html += `<div class="strek-calendar-block">
<div style="font-weight:bold; margin-bottom:4px;">${stekData.label}</div>
<table>
<caption>${firstDay.toLocaleString('default', { month: 'long' })} ${year}</caption>
<thead>
<tr>
<th>Sa</th><th>Su</th><th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th>
</tr>
</thead>
<tbody>
<tr>`;
// Calculate the day of week for the 1st (Saturday=0)
let dayOfWeek = (firstDay.getDay() + 1) % 7;
for (let i = 0; i < dayOfWeek; i++) html += '<td></td>';
for (let day = 1; day <= daysInMonth; day++) {
if ((dayOfWeek % 7 === 0) && day !== 1) html += '</tr><tr>';
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
html += `<td>
<input type="checkbox" class="day-checkbox strek-checkbox" id="day-${stekId}-${year}-${month}-${day}" data-date="${dateStr}" data-stek="${stekId}">
<label for="day-${stekId}-${year}-${month}-${day}">${day}</label>
</td>`;
dayOfWeek++;
}
while (dayOfWeek % 7 !== 0) { html += '<td></td>'; dayOfWeek++; }
html += '</tr></tbody></table></div>';
});
return html;
};
// TOGGLE LOGIC
document.getElementById('toggle-week').addEventListener('click', function() {
document.getElementById('toggle-week').classList.add('active');
document.getElementById('toggle-day').classList.remove('active');
document.getElementById('calendar-table-wrapper').style.display = '';
document.getElementById('monthly-calendar-wrapper').style.display = 'none';
document.getElementById('monthly-calendar-controls').style.display = 'none';
navDiv.style.display = '';
renderCalendar();
});
document.getElementById('toggle-day').addEventListener('click', function() {
document.getElementById('toggle-day').classList.add('active');
document.getElementById('toggle-week').classList.remove('active');
document.getElementById('calendar-table-wrapper').style.display = 'none';
document.getElementById('monthly-calendar-wrapper').style.display = '';
document.getElementById('monthly-calendar-controls').style.display = '';
navDiv.style.display = 'none';
renderMonthlyCalendar();
});
// Initial render
if(displayWeekly=='block'){
renderCalendar();
}else{
renderMonthlyCalendar();
}
document.getElementById('prev-month-btn').addEventListener('click', () => {
if (currentMonthIdx > 0) {
currentMonthIdx--;
renderMonthlyCalendar();
}
});
document.getElementById('next-month-btn').addEventListener('click', () => {
if (currentMonthIdx < loadedMonths.length - 1) {
currentMonthIdx++;
renderMonthlyCalendar();
}
});
document.addEventListener('change', function(e) {
if (
e.target.classList.contains('day-checkbox') &&
e.target.checked &&
!e.target.disabled
) {
// Uncheck all other day-checkboxes
document.querySelectorAll('.day-checkbox').forEach(cb => {
if (cb !== e.target) cb.checked = false;
});
const strekId = e.target.getAttribute('data-stek');
const productId = "<?php echo $product->get_id(); ?>";
const selectedDate = e.target.getAttribute('data-date');
// Set the check-in (start date) field and fire change event
const startDateInput = document.getElementById(`yith-wcbk-booking-start-date-${productId}`);
startDateInput.value = selectedDate;
// Fire both input and change events
startDateInput.dispatchEvent(new Event('input', { bubbles: true }));
startDateInput.dispatchEvent(new Event('change', { bubbles: true }));
// If a jQuery datepicker is attached, update it as well
if (typeof jQuery !== 'undefined' && jQuery(startDateInput).datepicker) {
jQuery(startDateInput).datepicker('setDate', selectedDate);
}
// Set the check-out (end date) field to one day after check-in
const endDateInput = document.getElementById(`yith-wcbk-booking-end-date-${productId}`);
if (endDateInput) {
// Calculate next day
const dateObj = new Date(selectedDate);
dateObj.setDate(dateObj.getDate() + 1);
// Format as yyyy-mm-dd
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
const dd = String(dateObj.getDate()).padStart(2, '0');
const nextDay = `${yyyy}-${mm}-${dd}`;
endDateInput.value = nextDay;
endDateInput.dispatchEvent(new Event('input', { bubbles: true }));
endDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(endDateInput).datepicker) {
jQuery(endDateInput).datepicker('setDate', nextDay);
}
}
// Strek dropdown logic (as before)
const select = document.querySelector(`#yith-wcbk-booking-resources-${productId} select.yith-wcbk-selector__field`);
if (select) {
select.value = strekId;
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
const customItem = document.querySelector(`#yith-wcbk-booking-resources-${productId} .yith-wcbk-selector__item[data-key="${strekId}"]`);
if (customItem) {
customItem.click();add_calendar_tab_content
}
}
}
});
});add_calendar_tab_content
</script>
1. Loads the **Calendar** tab and content in the **WooCommerce Product Edit** screen.
2. Runs **only when YITH Booking Premium plugin is active**.
3. Does **not affect other wp-admin functionality**.
—
### β Final Efficient Code β with All Conditions Applied
Replace your constructor like this:
public function __construct() {
if (
is_admin() && // Only run in wp-admin
function_exists('get_current_screen') && // Avoid error on early admin init
class_exists('YITH_WCBK') // Ensure YITH Booking Premium is active
) {
add_action('current_screen', function ($screen) {
if (
$screen->base === 'post' && // We're editing a post
$screen->post_type === 'product' // And it's a WooCommerce product
) {
add_filter('woocommerce_product_data_tabs', array($this, 'add_calendar_tab'));
add_action('woocommerce_product_data_panels', array($this, 'add_calendar_tab_content'));
}
});
}
}
—
### π Why This Works
| Check | Purpose |
|————————–|————————————————————————-|
| `is_admin()` | Ensures the code doesn't run on frontend |
| `get_current_screen()` | Runs only when WordPress is ready to give screen context |
| `$screen->base === 'post'` | Ensures you're inside the edit screen |
| `$screen->post_type === 'product'` | Restricts logic to WooCommerce product edit page only |
| `class_exists('YITH_WCBK')` | Ensures plugin only loads if YITH Booking Premium is active |
—
### β Bonus: Show admin notice if plugin missing
If you want to alert admins when YITH Booking Premium is not active:
if (is_admin() && !class_exists('YITH_WCBK')) {
add_action('admin_notices', function () {
echo '<div class="notice notice-error"><p><strong>Booking Addon:</strong> YITH WooCommerce Booking Premium is not active. Please activate it to use the calendar tab.</p></div>';
});
}
Let me know if youβd like to defer the calendar rendering until the tab is actually clicked (via AJAX) to reduce memory usage.
– PHP structure and isolation
– JavaScript scope
– Avoiding global pollution or unintended conflicts
—
## β PHP CODE REVIEW β Recommendations
### 1. β
**Wrap Output Only Inside Product Edit Page**
You're already doing this:
$screen->base === 'post' && $screen->post_type === 'product'
Perfect. No changes needed.
—
### 2. β **Check for Product Existence Before Proceeding**
Before using `$product->get_slug()` or accessing methods like `$product->get_resources_data()`, add a guard:
if ( ! $product || ! method_exists($product, 'get_slug') ) {
echo '</div>';
return;
}
β Already partially done in your code β just ensure itβs added early inside `add_calendar_tab_content()`.
—
### 3. β **Avoid Function Conflicts**
You are using:
function get_week_start_date($week, $year) { ... }
To avoid polluting the global function namespace in WordPress, wrap your functions in a class method or make them static helpers:
#### Recommended:
private function get_week_start_date($week, $year) { ... }
private function get_week_dates($week, $year) { ... }
And call them like:
$this->get_week_start_date(...);
—
### 4. β **Avoid Inline “ When Possible**
Move the styles to an enqueued CSS file (optional but better), or at minimum scope it like this:
echo '<style id="booking-addon-style">';
This makes it easier to debug and avoids duplicate inline styles.
—
## β JAVASCRIPT CODE REVIEW β Recommendations
### 1. β **Scope All JS with IIFE (Immediately Invoked Function Expression)**
Wrap the whole block like this to avoid polluting global JS:
<script>
(function($){
// Your entire JS here
})(jQuery);
</script>
You already use `document.addEventListener('DOMContentLoaded', …)` which is safe, but this additional wrap makes sure no globals are leaked.
—
### 2. β **Use Unique Class or ID Prefixes**
You're using classes like `.day-checkbox`, `.toggle-btn`, etc. These are too generic.
π Rename them to something more unique, e.g.:
.booking-addon-toggle-btn
.booking-addon-day-checkbox
And in HTML/JS, update accordingly:
<input class="booking-addon-day-checkbox">
document.querySelectorAll('.booking-addon-day-checkbox').forEach(...)
This **avoids conflicts with themes or plugins using similar class names**.
—
### 3. β **Don't Overwrite DOM Elements Without Check**
Avoid assumptions like:
document.getElementById('prev-week-btn').remove();
Instead, do:
const prevBtn = document.getElementById('prev-week-btn');
if (prevBtn) prevBtn.remove();
β You're already doing this with optional chaining (`?.`) β looks good.
—
### 4. β **Namespace JS Variables (Optional But Ideal)**
You're defining global JS variables like:
const strekAvail = ...
let displayWeekly = ...
To avoid global leaks, use a single global object if needed:
window.BookingAddon = {
displayWeekly: '<?php echo $displayWeekly; ?>',
strekAvail: <?php echo json_encode($strek_avail); ?>,
...
};
Or scope them inside the IIFE.
—
## β OPTIONAL: Separate JS & CSS Files
You can enqueue your JS/CSS conditionally like:
add_action('admin_enqueue_scripts', function($hook){
if ($hook === 'post.php' || $hook === 'post-new.php') {
if (get_post_type() === 'product') {
wp_enqueue_script('booking-addon-js', plugin_dir_url(__FILE__) . 'assets/admin.js', ['jquery'], '1.0', true);
wp_enqueue_style('booking-addon-css', plugin_dir_url(__FILE__) . 'assets/admin.css');
}
}
});
This keeps code modular, easier to maintain and debug.
—
## β Conclusion
### Your plugin is safe to run if you:
– β Wrap all logic inside product edit screen (β
already done)
– β Scope PHP helper functions into class
– β Rename JS/CSS classes to avoid theme/plugin conflicts
– β Use IIFE for JavaScript or namespace your JS
– β Optionally move JS/CSS to separate files
—
Let me know if you want help converting your inline JS and CSS into separate enqueued files.
give me code here
—
### β Update this part at the **top of** `add_calendar_tab_content()`:
Replace:
public function add_calendar_tab_content() {
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
global $post;
$product = wc_get_product( get_the_ID() );
With this **safe version**:
public function add_calendar_tab_content() {
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
global $post;
$product = wc_get_product( get_the_ID() );
if ( ! $product || ! method_exists( $product, 'get_slug' ) ) {
echo '<p>' . esc_html__( 'This product is not compatible with the Booking Addon.', 'your-textdomain' ) . '</p>';
echo '</div>';
return;
}
—
### β Why this matters
This ensures your code:
– Doesn't throw a fatal error on `$product->get_slug()` if `$product` is `null`
– Displays a fallback message instead of breaking the UI
– Exits early, keeping admin smooth
Let me know if you want this to log an error to debug.log as well.
if ( ! $product || ! method_exists( $product, 'get_slug' ) ) {
echo '' . esc_html__( 'This product is not compatible with the Booking Addon.', 'your-textdomain' ) . '';
echo '';
return;
}
Hereβs the **updated and improved** check to place at the beginning of `add_calendar_tab_content()`:
—
### β Full Safe Guard Code Block:
public function add_calendar_tab_content() {
echo '<div id="calendar_tab_data" class="panel woocommerce_options_panel" style="display:none;">';
global $post;
$product = wc_get_product( get_the_ID() );
if (
! $product ||
! method_exists( $product, 'get_slug' ) ||
! $product instanceof WC_Product_Booking
) {
echo '<p>' . esc_html__( 'This product is not compatible with the Booking Addon. Please make sure it is a valid YITH Booking product.', 'your-textdomain' ) . '</p>';
echo '</div>';
return;
}
—
### β What this check ensures:
| Condition | Explanation |
|———-|————-|
| `! $product` | Prevents null errors if the product doesnβt exist |
| `! method_exists( $product, 'get_slug' )` | Ensures itβs a valid WooCommerce product object |
| `! $product instanceof WC_Product_Booking` | Ensures it's a YITH Booking product |
This prevents unintended behavior on non-booking products or during early loading stages.
Let me know if you want to **hide the Calendar tab altogether** for non-booking products instead of showing a message.
—
## β 1. Wrap everything inside an IIFE with jQuery scope
Wrap your entire JS inside this to avoid polluting the global namespace:
<script>
(function($) {
// your existing code here
})(jQuery);
</script>
This ensures `$` is always jQuery, and it wonβt clash with other plugins or themes.
—
## β 2. Prefix all class names and IDs to avoid conflicts
Change generic class names like:
| Original | Replace with |
|——————|——————————-|
| `.toggle-btn` | `.booking-addon-toggle-btn` |
| `.day-checkbox` | `.booking-addon-day-checkbox` |
| `.week-checkbox` | `.booking-addon-week-checkbox` |
| `#calendar-table-wrapper` | `#booking-addon-weekly-calendar-wrapper` |
β Also update usage in JS and CSS accordingly.
—
## β 3. Avoid global variables (use one namespace object)
Instead of:
let displayWeekly = 'block';
const strekAvail = {...};
Wrap all your config/vars like this:
const BookingAddon = {
displayWeekly: '<?php echo $displayWeekly; ?>',
displayPerday: '<?php echo $displayPerday; ?>',
strekAvail: <?php echo json_encode($strek_avail); ?>,
allowedDays: <?php echo json_encode($product->get_allowed_start_days()); ?>,
...
};
Use `BookingAddon.displayWeekly` instead of plain `displayWeekly`.
—
## β 4. Check existence of DOM elements before modifying
You're doing this safely:
const btn = document.getElementById('prev-week-btn');
if (btn) btn.remove();
β Keep this practice everywhere you manipulate DOM.
—
## β 5. Add `data-*` scoped attributes if needed
Example:
<div data-booking-addon="calendar-wrapper"></div>
Then in JS:
const wrapper = document.querySelector('[data-booking-addon="calendar-wrapper"]');
β Reduces risk of clashing with other plugins.
—
## β 6. Only run on product edit screen
To be extra safe, you can add a `body` check:
if (!document.body.classList.contains('post-type-product')) return;
Or WordPress gives you page info like:
if (typeof pagenow !== 'undefined' && pagenow !== 'product') return;
—
## β 7. Defer heavy rendering until needed (optional performance boost)
Instead of rendering calendars on `DOMContentLoaded`, defer to tab click:
document.getElementById('toggle-week')?.addEventListener('click', () => {
if (!window.bookingAddonWeekRendered) {
renderCalendar();
window.bookingAddonWeekRendered = true;
}
});
Same for `.toggle-day` to render monthly on demand.
—
## β Summary of JavaScript Improvements
| Area | Action |
|————–|——–|
| Global scope | Wrap in IIFE `(function($){…})(jQuery);` |
| Variable leaks | Use a `BookingAddon` object to store all shared variables |
| Class/ID naming | Use `booking-addon-*` prefix |
| DOM handling | Always check element existence |
| When to run | Check `pagenow` or `post-type-product` before running |
| Performance | Delay rendering until tab is clicked |
—
Let me know if you want the **updated JS with all these improvements applied** in your code directly.
is_admin() && // Only run in wp-admin
function_exists('get_current_screen') && // Avoid error on early admin init
class_exists('YITH_WCBK') // Ensure YITH Booking Premium is active
) {
add_action('current_screen', function ($screen) {
if (
$screen->base === 'post' && // We're editing a post
$screen->post_type === 'product' // And it's a WooCommerce product
) {
add_filter('woocommerce_product_data_tabs', array($this, 'add_calendar_tab'));
add_action('woocommerce_product_data_panels', array($this, 'add_calendar_tab_content'));
}
});
}
after adding this claender tab not showing
—
### β Solution: Move everything inside `current_screen` hook
Hereβs the **correct and working version** of your constructor:
public function __construct() {
if ( is_admin() && class_exists('YITH_WCBK') ) {
add_action('current_screen', function ($screen) {
if (
$screen &&
$screen->base === 'post' &&
$screen->post_type === 'product'
) {
add_filter('woocommerce_product_data_tabs', array($this, 'add_calendar_tab'));
add_action('woocommerce_product_data_panels', array($this, 'add_calendar_tab_content'));
}
});
}
}
—
### β Key Fixes
| Issue | Fix |
|———————————-|—————————————|
| `get_current_screen()` used too early | Moved check inside `current_screen` action |
| `$screen` not checked for null | Added `if ($screen && …)` guard |
—
### π‘ Optional: Log or test output
To debug, you can add:
error_log('Screen Base: ' . $screen->base);
Or show in admin bar using `add_action('admin_notices', …)` if needed.
—
Let me know if you'd like the full working plugin file updated with this fix and best practices bundled in.
—
### β Root Cause
You're using an **anonymous function** inside `add_action('current_screen', function() {…})` **within a class**, and trying to call `$this->add_calendar_tab` inside it. But `$this` won't be available inside that closure unless it's explicitly captured.
—
### β Final Working Fix for Your `__construct()`:
Replace your constructor with this:
public function __construct() {
if ( is_admin() && class_exists( 'YITH_WCBK' ) ) {
add_action( 'current_screen', array( $this, 'maybe_add_booking_tab' ) );
}
}
Then **add this new method** inside your class:
public function maybe_add_booking_tab( $screen ) {
if (
$screen &&
$screen->base === 'post' &&
$screen->post_type === 'product'
) {
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_calendar_tab' ) );
add_action( 'woocommerce_product_data_panels', array( $this, 'add_calendar_tab_content' ) );
}
}
—
### β Summary
| Fix | Why it's needed |
|—–|—————–|
| Use a separate method instead of anonymous closure | So `$this` works properly |
| Hook into `current_screen` correctly | Only runs on admin screens |
| Check `post_type === 'product'` | Limits to WooCommerce product edit |
—
Let me know if you want to conditionally **hide the tab for non-booking products** as well.
<style>
/* Toggle Button */
.toggle-btn {
transition: background 0.2s, color 0.2s;
}
.toggle-btn.active {
background: #00c48c !important;
color: #fff !important;
}
/* Calendar Containers */
.weekly-calendar,
.monthly-calendar {
margin: 30px 0;
font-family: Arial, sans-serif;
}
/* Table Styling */
.weekly-calendar table,
.monthly-calendar table {
width: 100%;
border-collapse: collapse;
text-align: center;
min-width: 220px;
margin-bottom: 0;
}
/* Table Headers and Cells */
.weekly-calendar th,
.weekly-calendar td,
.monthly-calendar th,
.monthly-calendar td {
border: none;
}
.weekly-calendar th {
text-align: center;
vertical-align: middle;
font-size: 12px;
}
.weekly-calendar td {
font-size: 14px !important;
font-weight: 600 !important;
}
.weekly-calendar th small {
font-size: 10px;
color: #666;
display: block;
margin-top: 2px;
}
.weekly-calendar th,
.monthly-calendar th {
}
.yith-wcbk-booking-form button#prev-week-btn ,.yith-wcbk-booking-form button#next-week-btn{
background: none;
border:none;
cursor: pointer;
}
.yith-wcbk-form-section-dates-wrapper .toggle-btn {
padding: 10px !important;
text-align: center;
font-size: 14px;
font-weight: 600;
}
.weekly-calendar .selected,
.monthly-calendar .selected {
color: #fff;
}
/* Monthly Calendar Layout */
.monthly-calendar {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.prev_month ,.next_month {
background: none;
border: none;
cursor: pointer;
}
.monthly-calendar caption {
font-weight: bold;
margin-bottom: 5px;
}
.monthly-calendar .day-checkbox {
display: none;
}
/* 2. Style the label as a box */
label[for^="day-"] {
display: inline-block;
text-align: center;
font-weight: bold;
position: relative;
cursor: pointer;
}
/* 3. Show tick icon when checked */
.day-checkbox:checked + label[for^="day-"]::after {
content: "β";
color: green;
font-size: 19px;
position: absolute;
top: -5px;
right: 0px;
background: #ccffcc;
width: 100%;
}
.monthly-calendar td:has(input.unavailable) {
background-color: #ffcccc !important;
border-radius: 5px;
padding: 10px !important;
}
.monthly-calendar td.selected{
background: #00c48c !important;
border-radius: 5px;
padding: 10px !important;
}
.monthly-calendar td:not(:first-child) {
background: #ccffcc;
border-radius: 5px;
}
/* Scroll Container */
.monthly-calendar-scroll {
width: 100%;
overflow-y: auto;
}
.monthly-calendar-scroll.scrollable {
max-height: 350px;
}
/* Calendar Block */
.strek-calendar-block {
margin-bottom: 24px;
background: #fff;
padding: 8px 0 16px 0;
}
.weekly-calendar .strek-calendar-block:last-child {
border-bottom: none;
}
.weekly-calendar td:has(input.unavailable) {
background-color: #ffcccc !important;
}
.weekly-calendar td.selected{
background: #00c48c !important;
}
/* Custom Checkbox */
.weekly-calendar .strek-checkbox {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
vertical-align: middle; /* default: available (green) */
transition: background 0.2s;
}
.weekly-calendar .strek-checkbox:checked::after {
content: "β";
display: inline-block;
color: #ffffff;
font-size: 19px;
position: relative;
left: 2px;
top: 0px;
}
.weekly-calendar .week-checkbox.strek-checkbox.unavailable::after {
content: "β"; /* Unicode cross */
color: red;
font-size: 19px;
position: absolute;
top: 0;
left: 2px;
}
.weekly-calendar .strek-checkbox.unavailable {
background: #ffcccc !important; /* unavailable (red) */
border-color: #ff8888;
border:none;
}
.weekly-calendar .strek-checkbox:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.weekly-calendar .strek-checkbox:checked {
background: #00c48c !important;
border-color: #00c48c;
border:none;
}
/* Weekly calendar specific styles */
.weekly-calendar td {
text-align: center;
vertical-align: middle;
position: relative;
padding: 0px !important;
}
.weekly-calendar td:not(:first-child){
background: #ccffcc;
border-radius: 5px;
}
.weekly-calendar tr{
border:none !important;
}
td small {
font-size: 10px;
line-height: 1.2;
margin-top: 4px;
display: none;
}
.woocommerce div.product form.cart table{
border-collapse: separate;
border-spacing: 12px;
}
</style>
)
)
<script>
(function($) {
let displayWeekly='<?php echo $displayWeekly; ?>';
let displayPerday='<?php echo $displayPerday; ?>';
const strekAvail = <?php echo json_encode($strek_avail); ?>;
const strekMonthIdx = {};
Object.keys(strekAvail).forEach(stekId => {
strekMonthIdx[stekId] = 0; // Start at first month for each strek
});
const allowedDays = <?php echo json_encode($product->get_allowed_start_days()); ?>; // 1=Monday, 5=Friday, etc.
console.log('Streak Availability Data:', strekAvail);
console.log('Allowed Days:', allowedDays);
document.addEventListener('DOMContentLoaded', function() {
const weekList = <?php echo json_encode($week_list); ?>;
const weeksToShow = <?php echo $weeks_to_show; ?>;
const loadedMonths = <?php echo json_encode($loaded_months); ?>;
let weekOffset = 0;
let currentMonthIdx = 0;
const notAvailableDates = <?php echo json_encode($not_available_dates); ?>;
// Helper function to check if any day in a week is unavailable
const weekHasUnavailableDates = (weekDates, notAvailableDates) => {
for (let i = 0; i < weekDates.length; i++) {
if (notAvailableDates.includes(weekDates[i])) {
return true;
}
}
return false;
};
// WEEKLY CALENDAR
const renderCalendar = () => {
let html = `<table><thead><tr><th>Stek</th>`;
// Table headers (weeks)
for (let i = 0; i < weeksToShow; i++) {
const weekIdx = weekOffset + i;
if (weekIdx >= weekList.length) break;
const w = weekList[weekIdx];
// Find allowed weekday (e.g. Friday) in this week
let allowedDate = null;
for (let d = 0; d < w.dates.length; d++) {
const dateObj = new Date(w.dates[d]);
// getDay: 0=Sunday, 1=Monday, ..., 5=Friday, 6=Saturday
// allowedDays uses 1=Monday, 7=Sunday (ISO)
// Convert to ISO format: Saturday=6 becomes 6, Sunday=0 becomes 7
const isoDay = dateObj.getDay() === 0 ? 7 : dateObj.getDay();
if (allowedDays.includes(isoDay)) {
allowedDate = w.dates[d];
break;
}
}
let thLabel = `Week ${w.week}`;
if (allowedDate) {
const dateObj = new Date(allowedDate);
const startObj = new Date(w.dates[0]);
const endObj = new Date(w.dates[w.dates.length - 1]);
const startStr = startObj.toLocaleDateString('nl-NL', { day: '2-digit', month: '2-digit' });
const endStr = endObj.toLocaleDateString('nl-NL', { day: '2-digit', month: '2-digit' });
thLabel += `<br><small>${startStr} to ${endStr}</small>`;
}
html += `<th>${thLabel}</th>`;
}
html += `</tr></thead><tbody>`;
// For each stek, create a row
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
html += `<tr><td>${stekData.label}</td>`;
for (let i = 0; i < weeksToShow; i++) {
const weekIdx = weekOffset + i;
if (weekIdx >= weekList.length) break;
const w = weekList[weekIdx];
// Find allowed weekday in this week
let allowedDate = null;
for (let d = 0; d < w.dates.length; d++) {
const dateObj = new Date(w.dates[d]);
// Convert to ISO format: Saturday=6 becomes 6, Sunday=0 becomes 7
const isoDay = dateObj.getDay() === 0 ? 7 : dateObj.getDay();
if (allowedDays.includes(isoDay)) {
allowedDate = w.dates[d];
break;
}
}
// Check if this specific date is available for this specific stek
const isAvailable = allowedDate && stekData.available_dates.includes(allowedDate);
const unavailableClass = isAvailable ? '' : 'unavailable';
// Debug logging for this specific check
if (allowedDate) {
console.log(`Week ${w.week}: ${stekData.label} - Date: ${allowedDate}, Available: ${isAvailable}`);
// Show specific example for August 2nd
if (allowedDate === '2024-08-02') {
console.log(`π― August 2nd found! Week: ${w.week}, Resource: ${stekData.label}, Available: ${isAvailable}`);
}
}
html += `<td style="padding: 20px 10px;">
<input type="checkbox" class="week-checkbox strek-checkbox ${unavailableClass}"
id="week-check-${stekId}-${weekIdx}"
data-stek="${stekId}"
data-week="${w.week}"
data-year="${w.year}"
data-date="${allowedDate ? allowedDate : ''}"
${isAvailable ? '' : 'disabled'}>
${allowedDate
? `<br><small>${new Date(allowedDate).toLocaleDateString('nl-NL', { weekday: 'short', day: '2-digit', month: '2-digit' })}</small>`
: '<br><small>n.v.t.</small>'}
${isAvailable ? '<br><small style="color: green;">β Available</small>' : '<br><small style="color: red;">β Unavailable</small>'}
</td>`;
}
html += `</tr>`;
});
html += `</tbody></table>`;
document.getElementById('calendar-table-wrapper').innerHTML = html;
// Update navigation label
const navLabel = document.getElementById('week-navigation-label');
if (navLabel && weekList.length > 0) {
const firstWeek = weekList[weekOffset];
const lastWeek = weekList[Math.min(weekOffset + weeksToShow - 1, weekList.length - 1)];
if (firstWeek && lastWeek) {
const firstDate = new Date(firstWeek.start);
const lastDate = new Date(lastWeek.start);
lastDate.setDate(lastDate.getDate() + 6);
navLabel.textContent = `Weeks ${firstWeek.week} - ${lastWeek.week} (${firstDate.toLocaleDateString('nl-NL')} - ${lastDate.toLocaleDateString('nl-NL')})`;
}
}
// Selection highlight for checkboxes and event handlers
document.querySelectorAll('.weekly-calendar .week-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', () => {
const td = checkbox.closest('td');
if (checkbox.checked) {
// Uncheck all other week checkboxes across all streks
document.querySelectorAll('.weekly-calendar .week-checkbox').forEach(cb => {
if (cb !== checkbox) {
cb.checked = false;
cb.closest('td').classList.remove('selected');
}
});
td.classList.add('selected');
// Update the booking form fields
const strekId = checkbox.getAttribute('data-stek');
const productId = "<?php echo $product->get_id(); ?>";
const selectedDate = checkbox.getAttribute('data-date');
// Set the check-in (start date) field
const startDateInput = document.getElementById(`yith-wcbk-booking-start-date-${productId}`);
if (startDateInput) {
startDateInput.value = selectedDate;
startDateInput.dispatchEvent(new Event('input', { bubbles: true }));
startDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(startDateInput).datepicker) {
jQuery(startDateInput).datepicker('setDate', selectedDate);
}
}
// Set the check-out (end date) field to 7 days after check-in
const endDateInput = document.getElementById(`yith-wcbk-booking-end-date-${productId}`);
if (endDateInput) {
const dateObj = new Date(selectedDate);
dateObj.setDate(dateObj.getDate() + 7); // 7 days (week)
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
const dd = String(dateObj.getDate()).padStart(2, '0');
const endDate = `${yyyy}-${mm}-${dd}`;
endDateInput.value = endDate;
endDateInput.dispatchEvent(new Event('input', { bubbles: true }));
endDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(endDateInput).datepicker) {
jQuery(endDateInput).datepicker('setDate', endDate);
}
}
// Update strek dropdown
const select = document.querySelector(`#yith-wcbk-booking-resources-${productId} select.yith-wcbk-selector__field`);
if (select) {
select.value = strekId;
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
const customItem = document.querySelector(`#yith-wcbk-booking-resources-${productId} .yith-wcbk-selector__item[data-key="${strekId}"]`);
if (customItem) {
customItem.click();
}
}
} else {
td.classList.remove('selected');
}
});
});
};
document.getElementById('prev-week-btn')?.remove();
document.getElementById('next-week-btn')?.remove();
const navDiv = document.createElement('div');
navDiv.style.display = "flex";
navDiv.style.justifyContent = "center";
navDiv.style.marginBottom = "10px";
navDiv.innerHTML = `
<button type="button" id="prev-week-btn" style="display:<?php echo $displayWeekly; ?>;"> <img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/left-chevron-svgrepo-com.svg" alt="Previous" width="25" height="25"> </button>
<div style="flex: 1; text-align: center; font-weight: bold;display:<?php echo $displayWeekly; ?>;" id="week-navigation-label">Select a week</div>
<button type="button" id="next-week-btn" style="display:<?php echo $displayWeekly; ?>;"> <img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/right-chevron-svgrepo-com.svg" alt="Previous" width="25" height="25"> </button>
`;
document.getElementById('calendar-table-wrapper').before(navDiv);
document.getElementById('prev-week-btn').addEventListener('click', () => {
if (weekOffset - weeksToShow >= 0) {
weekOffset -= weeksToShow;
} else {
weekOffset = 0;
}
renderCalendar();
});
document.getElementById('next-week-btn').addEventListener('click', () => {
if (weekOffset + weeksToShow < weekList.length) {
weekOffset += weeksToShow;
renderCalendar();
}
});
// MONTHLY CALENDAR (one month at a time)
const renderMonthlyCalendar = () => {
if (loadedMonths.length === 0) {
document.getElementById('monthly-calendar-scroll').innerHTML = '<p>No months available.</p>';
document.getElementById('monthly-calendar-controls').style.display = 'none';
return;
}
document.getElementById('monthly-calendar-controls').style.display = 'none'; // Hide global controls
let html = '';
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
html += renderStrekMonthBlock(stekId, stekData);
});
document.getElementById('monthly-calendar-scroll').innerHTML = html;
// Add event listeners for each strek's navigation
Object.keys(strekAvail).forEach(stekId => {
document.getElementById(`prev-month-btn-${stekId}`).addEventListener('click', () => {
if (strekMonthIdx[stekId] > 0) {
strekMonthIdx[stekId]--;
renderMonthlyCalendar();
}
});
document.getElementById(`next-month-btn-${stekId}`).addEventListener('click', () => {
if (strekMonthIdx[stekId] < loadedMonths.length - 1) {
strekMonthIdx[stekId]++;
renderMonthlyCalendar();
}
});
});
// Add scrollable class if needed
const strekCount = Object.keys(strekAvail).length;
const scrollDiv = document.getElementById('monthly-calendar-scroll');
if (strekCount > 2) {
scrollDiv.classList.add('scrollable');
} else {
scrollDiv.classList.remove('scrollable');
}
};
const renderStrekMonthBlock = (stekId, stekData) => {
const idx = strekMonthIdx[stekId];
const [year, month] = loadedMonths[idx].split('-').map(Number);
const availableDates = Array.isArray(stekData.available_dates) ? stekData.available_dates : [];
// Get first and last day of month
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const daysInMonth = lastDay.getDate();
let html = `<div class="strek-calendar-block">
<div style="font-weight:bold; margin-bottom:20px;">${stekData.label}</div>
<div style="text-align:center; margin-bottom:10px; display:flex; align-items: center; justify-content: space-between;">
<button type="button" id="prev-month-btn-${stekId}" class="prev_month"><img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/left-chevron-svgrepo-com.svg" alt="Next" width="25" height="25"> </button>
<span style="font-weight:bold; margin:0 10px;">${firstDay.toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
<button type="button" id="next-month-btn-${stekId}" class="next_month"><img src="https://carplakes.newsoftdemo.info/wp-content/uploads/2025/07/right-chevron-svgrepo-com.svg" alt="Next" width="25" height="25"></button>
</div>
<table>
<thead>
<tr>
<th>Sa</th><th>Su</th><th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th>
</tr>
</thead>
<tbody>
<tr>`;
// Calculate the day of week for the 1st (Saturday=0)
let dayOfWeek = (firstDay.getDay() + 1) % 7;
for (let i = 0; i < dayOfWeek; i++) html += '<td></td>';
for (let day = 1; day <= daysInMonth; day++) {
if ((dayOfWeek % 7 === 0) && day !== 1) html += '</tr><tr>';
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const isAvailable = availableDates.includes(dateStr);
const unavailableClass = isAvailable ? '' : 'unavailable';
const disabledAttr = isAvailable ? '' : 'disabled';
html += `<td>
<input type="checkbox" class="day-checkbox strek-checkbox ${unavailableClass}"
id="day-${stekId}-${year}-${month}-${day}"
data-date="${dateStr}"
data-stek="${stekId}"
${disabledAttr}>
<label for="day-${stekId}-${year}-${month}-${day}">${day}</label>
</td>`;
dayOfWeek++;
}
while (dayOfWeek % 7 !== 0) { html += '<td></td>'; dayOfWeek++; }
html += '</tr></tbody></table></div>';
return html;
};
const renderSingleMonth = (year, month) => {
// For each strek, render a classic calendar
let html = '';
Object.entries(strekAvail).forEach(([stekId, stekData]) => {
// Get first and last day of month
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const daysInMonth = lastDay.getDate();
html += `<div class="strek-calendar-block">
<div style="font-weight:bold; margin-bottom:4px;">${stekData.label}</div>
<table>
<caption>${firstDay.toLocaleString('default', { month: 'long' })} ${year}</caption>
<thead>
<tr>
<th>Sa</th><th>Su</th><th>Mo</th><th>Tu</th><th>We</th><th>Th</th><th>Fr</th>
</tr>
</thead>
<tbody>
<tr>`;
// Calculate the day of week for the 1st (Saturday=0)
let dayOfWeek = (firstDay.getDay() + 1) % 7;
for (let i = 0; i < dayOfWeek; i++) html += '<td></td>';
for (let day = 1; day <= daysInMonth; day++) {
if ((dayOfWeek % 7 === 0) && day !== 1) html += '</tr><tr>';
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
html += `<td>
<input type="checkbox" class="day-checkbox strek-checkbox" id="day-${stekId}-${year}-${month}-${day}" data-date="${dateStr}" data-stek="${stekId}">
<label for="day-${stekId}-${year}-${month}-${day}">${day}</label>
</td>`;
dayOfWeek++;
}
while (dayOfWeek % 7 !== 0) { html += '<td></td>'; dayOfWeek++; }
html += '</tr></tbody></table></div>';
});
return html;
};
// TOGGLE LOGIC
document.getElementById('toggle-week').addEventListener('click', function() {
document.getElementById('toggle-week').classList.add('active');
document.getElementById('toggle-day').classList.remove('active');
document.getElementById('calendar-table-wrapper').style.display = '';
document.getElementById('monthly-calendar-wrapper').style.display = 'none';
document.getElementById('monthly-calendar-controls').style.display = 'none';
navDiv.style.display = '';
renderCalendar();
});
document.getElementById('toggle-day').addEventListener('click', function() {
document.getElementById('toggle-day').classList.add('active');
document.getElementById('toggle-week').classList.remove('active');
document.getElementById('calendar-table-wrapper').style.display = 'none';
document.getElementById('monthly-calendar-wrapper').style.display = '';
document.getElementById('monthly-calendar-controls').style.display = '';
navDiv.style.display = 'none';
renderMonthlyCalendar();
});
// Initial render
if(displayWeekly=='block'){
renderCalendar();
}else{
renderMonthlyCalendar();
}
document.getElementById('prev-month-btn').addEventListener('click', () => {
if (currentMonthIdx > 0) {
currentMonthIdx--;
renderMonthlyCalendar();
}
});
document.getElementById('next-month-btn').addEventListener('click', () => {
if (currentMonthIdx < loadedMonths.length - 1) {
currentMonthIdx++;
renderMonthlyCalendar();
}
});
document.addEventListener('change', function(e) {
if (
e.target.classList.contains('day-checkbox') &&
e.target.checked &&
!e.target.disabled
) {
// Uncheck all other day-checkboxes
document.querySelectorAll('.day-checkbox').forEach(cb => {
if (cb !== e.target) cb.checked = false;
});
const strekId = e.target.getAttribute('data-stek');
const productId = "<?php echo $product->get_id(); ?>";
const selectedDate = e.target.getAttribute('data-date');
// Set the check-in (start date) field and fire change event
const startDateInput = document.getElementById(`yith-wcbk-booking-start-date-${productId}`);
startDateInput.value = selectedDate;
// Fire both input and change events
startDateInput.dispatchEvent(new Event('input', { bubbles: true }));
startDateInput.dispatchEvent(new Event('change', { bubbles: true }));
// If a jQuery datepicker is attached, update it as well
if (typeof jQuery !== 'undefined' && jQuery(startDateInput).datepicker) {
jQuery(startDateInput).datepicker('setDate', selectedDate);
}
// Set the check-out (end date) field to one day after check-in
const endDateInput = document.getElementById(`yith-wcbk-booking-end-date-${productId}`);
if (endDateInput) {
// Calculate next day
const dateObj = new Date(selectedDate);
dateObj.setDate(dateObj.getDate() + 1);
// Format as yyyy-mm-dd
const yyyy = dateObj.getFullYear();
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
const dd = String(dateObj.getDate()).padStart(2, '0');
const nextDay = `${yyyy}-${mm}-${dd}`;
endDateInput.value = nextDay;
endDateInput.dispatchEvent(new Event('input', { bubbles: true }));
endDateInput.dispatchEvent(new Event('change', { bubbles: true }));
if (typeof jQuery !== 'undefined' && jQuery(endDateInput).datepicker) {
jQuery(endDateInput).datepicker('setDate', nextDay);
}
}
// Strek dropdown logic (as before)
const select = document.querySelector(`#yith-wcbk-booking-resources-${productId} select.yith-wcbk-selector__field`);
if (select) {
select.value = strekId;
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
const customItem = document.querySelector(`#yith-wcbk-booking-resources-${productId} .yith-wcbk-selector__item[data-key="${strekId}"]`);
if (customItem) {
customItem.click();
}
}
}
});
});
})(jQuery);
</script>
—
### 𧨠Why the Calendar tab isn't showing
Your current code checks:
$screen->base === 'post' && $screen->post_type === 'product'
But for product variations, the screen base is usually:
$screen->base = 'product'
$screen->post_type = 'product_variation'
However, WooCommerce **still loads `product_data_tabs` and `product_data_panels` filters** only on the **parent product edit screen** β not on variation edit pages or quick edit.
—
### β Confirm Fix: Check `get_post_type()` instead of `$screen->post_type`
Use `get_post_type( get_the_ID() )` to ensure you're on the correct product.
—
### β Final Working Code (Updated)
In your `__construct()` method:
public function __construct() {
if ( is_admin() && class_exists( 'YITH_WCBK' ) ) {
add_action( 'current_screen', array( $this, 'maybe_add_booking_tab' ) );
}
}
Then update your handler method:
public function maybe_add_booking_tab( $screen ) {
global $post;
if (
$screen &&
$screen->base === 'post' &&
$screen->post_type === 'product' &&
$post && get_post_type( $post ) === 'product'
) {
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_calendar_tab' ) );
add_action( 'woocommerce_product_data_panels', array( $this, 'add_calendar_tab_content' ) );
}
}
—
### β Additional Debugging Tip
Add this temporarily in `maybe_add_booking_tab()` to debug:
error_log('Screen base: ' . $screen->base);
error_log('Screen post_type: ' . $screen->post_type);
error_log('Current post_type: ' . get_post_type( get_the_ID() ));
Check your `wp-content/debug.log` to confirm conditions.
—
Let me know if you'd like to also show the tab for specific product types (like "booking" or "simple") or hide it for grouped/variation types.
${allowedDate
? `${new Date(allowedDate).toLocaleDateString('nl-NL', { weekday: 'short', day: '2-digit', month: '2-digit' })}`
: 'n.v.t.'}
${isAvailable ? 'β Available' : 'β Unavailable'}
`;
remvoe checkbox and ony show avial and non avail, in red and green
—
### β Updated Code (no checkbox, only availability text)
html += `<td style="padding: 20px 10px; text-align: center;">
${allowedDate
? `<div><small>${new Date(allowedDate).toLocaleDateString('nl-NL', { weekday: 'short', day: '2-digit', month: '2-digit' })}</small></div>`
: '<div><small>n.v.t.</small></div>'}
<div>
<small style="color: ${isAvailable ? 'green' : 'red'};">
${isAvailable ? 'β Available' : 'β Unavailable'}
</small>
</div>
</td>`;
—
### β Whatβs Changed
| Old Element | Replaced With |
|————————|—————————–|
| `