Krok po kroku: wdrażamy własny system rezerwacji w WooCommerce – bez płatnych wtyczek
Znudziły Cię drogie wtyczki do rezerwacji, które nie dają pełnej kontroli?
Chcesz wdrożyć własny system rezerwacji oparty na kodzie i WooCommerce?
Dobrze trafiłeś! W tym poradniku pokażę Ci, jak to zrobić – od A do Z.
Zamienisz zwykłe zamówienie WooCommerce w interaktywną rezerwację terminu, która:
- wysyła e-mail z potwierdzeniem,
- blokuje wybrane godziny,
- nie pozwala na rezerwację tych samych slotów,
- ukrywa kalendarz po zakończeniu rezerwacji.
Wszystko bez żadnej płatnej wtyczki.
Tylko kod. Twój własny, elastyczny, darmowy.
Stwórz stronę z podziękowaniem
Przejdź do Strony → Dodaj nową.
Nadaj stronie tytuł, np. „Dziękujemy za zamówienie”.
W treści strony wklej shortcode:
[kalendarz_rezerwacji]
Zapisz i opublikuj.
Adres tej strony ( np. /podziekowanie-za-zamowienie/ ) przyda się w kolejnym kroku.
Dodaj kod do WordPressa
Zainstaluj darmową wtyczkę Code Snippets.
Dodaj nowy snippet i wklej cały kod, który znajduje się na samym dole wpisu.
Zapisz i aktywuj.
Kod ten:
- rejestruje shortcode [kalendarz_rezerwacji],
- tworzy własny system rezerwacji,
- wysyła potwierdzenia e-mailowe,
- przechowuje dane w bazie WordPressa,
- wykorzystuje AJAX do dynamicznego blokowania godzin.
Nie używa żadnych zewnętrznych wtyczek do rezerwacji – to w 100% własny system rezerwacji.
Dostosuj kod do własnych potrzeb
Aby wszystko działało u Ciebie, musisz zmienić kilka kluczowych rzeczy.
a) Podmień adres przekierowania po zakupie
Znajdź w kodzie fragment:
$redirect_url = 'https://twojadomena.com/podziekowanie-za-zamowienie/?order_id=' . $order_id;
Zamień URL na adres swojej strony z podziękowaniem.
Pamiętaj, by pozostawić końcówkę ?order_id= – ten parametr jest wymagany.
b) Ustaw swoje ID produktu
Kod przekierowuje użytkownika po zakupie produktu o ID 22929
.
Zmień:
if ($product_id == 22929) {
Pamiętaj że w funkcji function przekieruj_po_zakupie_produktu_22929($order_id) tam gdzie występuje 22929 wszędzie musisz wpisać ID swojego produktu. To jest bardzo ważne, jeśli tego nie zrobisz to TWÓJ KOD NIE ZADZIAŁA! Robimy to po to żeby dla tego konkretnego produktu, pojawiała się ta konkretna strona z podziękowaniami, na której będzie kalendarz.
Gdzie sprawdzić ID produktu? Produkty > Wszystkie produkty i w momencie najechania kursorem na interesujący Cię produkt pokaże się jego ID.
c) Ustaw dostępne dni i godziny
W kodzie znajduje się fragment:
const availableDays = [1, 3]; const availableHours = ['10:00', '12:00', '14:00'];
Dni tygodnia to numery od 0 (niedziela) do 6 (sobota).
Kod używa przelicznika (day + 6) % 7
, więc:
1
oznacza poniedziałek,2
– wtorek,3
– środa,- itd.
Możesz dowolnie ustawić, w które dni chcesz przyjmować rezerwacje.
Godziny są przypisane identycznie dla każdego dnia – czyli np. w każdy poniedziałek, środę i piątek dostępne są te same sloty godzinowe. Planuje w niedalekiej przyszłości udoskonalić ten kod i wprowadzić do niego zmiany więc na pewno pojawi się on na mojej stronie.
d) Zablokuj dni ręcznie
Jeśli chcesz zablokować konkretne dni, dodaj je do listy blockedDays w formacie YYYY-MM-DD:
const blockedDays = ['2025-07-22'];
Po dodaniu daty, ten dzień stanie się czerwony w kalendarzu i nieklikalny.
Na razie nie ma możliwości blokowania konkretnych godzin, ale jeśli taką funkcję wprowadzę również znajdzie się ona na mojej stronie w bazie wiedzy.
Jak działa system rezerwacji?
- Po zakupie produktu użytkownik trafia na stronę z kalendarzem.
- Jeśli w bazie nie ma rezerwacji przypisanej do zamówienia, użytkownik może wybrać datę i godzinę.
- Kiedy wszystkie godziny w danym dniu są zajęte – dzień jest czerwony i nie można go kliknąć.
- Jeśli konkretna godzina jest już zarezerwowana – zostaje przekreślona i zablokowana.
- Po dokonaniu rezerwacji, system wysyła e-mail potwierdzający konsultację.
- Kalendarz więcej się nie pojawi – ponieważ sprawdza numer zamówienia. Klient nie może dokonać podwójnej rezerwacji nawet po odświeżeniu strony.
Finalne efekty
Po wdrożeniu, Twoi klienci:
- kupują produkt,
- są przekierowani do strony z wyborem terminu,
- widzą dostępny kalendarz,
- rezerwują godzinę,
- otrzymują e-mail,
- nie mogą zarezerwować ponownie tego samego zamówienia.
A Ty masz pełną kontrolę – bez płatnych wtyczek, bez limitów i w 100% pod swoim nadzorem.
Ten własny system rezerwacji sprawdzi się idealnie w konsultacjach, usługach indywidualnych i pracy online.
Dodatkowo w kodzie występuje takie elementy jak:
$subject = 'Potwierdzenie konsultacji'; $message = "<center><font face='montserrat'>Dziękujemy za dokonanie rezerwacji.<br><br> Potwierdzamy termin konsultacji:<br><br> 📅 Data: <b>$data</b><br> ⏰ Godzina: <b>$godzina</b><br><br> Konsultacja odbędzie się w <b>formie online</b>.<br><br>Link do konsultacji zostanie wysłany<br> <b>do 10 minut</b> przed jej rozpoczęciem.<br><br> Pamiętaj o przesłaniu badań <br>oraz wypełnieniu kwestionariusza.</font></center>";
Ten fragment pozwala na dostosowanie treści, która ma być wysłana do klienta po dokonaniu rezerwacji. Treść oczywiście możesz zmienić wedle własnego uznania. W treści możesz używać HTML’a dla lepszej przejrzystości.
Kolejny dodatkowy element to:
fetch('https://hooks.zapier.com/hooks_od_zapiera/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData
Ten fragment służy do integracji z zewnętrzną platformą ZAPIER. Ta integracja pozwala na stworzenie pełnych automatyzacji z innymi zewnętrznymi platformami takimi jak np. instagram, facebook, activecamaign itp. W naszym przypadku hook od Zapiera można wykorzystać do automatyzacji przekazywania rezerwacji do kalendarza Google. Taki hook każdy musi stworzyć indywidualnie na platformie. Niestety żeby móc korzystać z ZAPIER, trzeba opłacać abonament w formie subskrypcji. Bez tej integracji kod będzie działał bez problemu. To jest tylko dodatek.
Jeśli jednak będziesz chciał/a dokonać takiej integracji – chętnie pomogę. Koszt takiej integracji ustalam z każdym indywidualnie.
KOMPLETNY KOD DO WDROŻENIA
<?php add_shortcode('kalendarz_rezerwacji', 'kalendarz_rezerwacji_shortcode'); function kalendarz_rezerwacji_shortcode() { $order_id = isset($_GET['order_id']) ? intval($_GET['order_id']) : null; if (!$order_id) return '<p>Brak ID zamówienia w URL (np. ?order_id=1234).</p>'; $order = wc_get_order($order_id); if (!$order) return '<p>Nieprawidłowe zamówienie.</p>'; $name = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(); $email = $order->get_billing_email(); $phone = $order->get_billing_phone(); global $wpdb; $table = $wpdb->prefix . 'zarezerwowane_godziny'; $reservation = $wpdb->get_row($wpdb->prepare("SELECT data_konsultacji, godzina_konsultacji FROM $table WHERE numer_zamowienia = %s LIMIT 1", $order_id)); if ($reservation) { $data = esc_html($reservation->data_konsultacji); $godzina = esc_html(substr($reservation->godzina_konsultacji, 0, 5)); return '<div class="kalendarz-potwierdzenie"><h3>Twoja konsultacja została potwierdzona!</h3><p>📅 Data: ' . $data . '<br>⏰ Godzina: ' . $godzina . '</p></div>'; } ob_start(); ?> <div id="kalendarz-rezerwacji-container" class="kalendarz-wrapper"> <div id="calendar" class="kalendarz-graficzny"></div> <div id="available-times" class="kalendarz-godziny"></div> <button id="book-button" class="kalendarz-przycisk" disabled>Rezerwuj termin</button> </div> <script> document.addEventListener('DOMContentLoaded', async function() { const monthNames = ['Stycze\u0144', 'Luty', 'Marzec', 'Kwiecie\u0144', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpie\u0144', 'Wrzesie\u0144', 'Pa\u017adziernik', 'Listopad', 'Grudzie\u0144']; const dayNames = ['Pon', 'Wt', '\u015ar', 'Czw', 'Pt', 'Sob', 'Ndz']; const availableDays = [1, 3]; const availableHours = ['10:00', '12:00', '14:00']; const today = new Date(); const blockedDays = ['2025-07-22']; let currentMonth = today.getMonth(); let currentYear = today.getFullYear(); let selectedDate = null; let selectedHour = null; let reservedSlots = {}; const calendarEl = document.getElementById('calendar'); const timesEl = document.getElementById('available-times'); const bookBtn = document.getElementById('book-button'); const container = document.getElementById('kalendarz-rezerwacji-container'); const response = await fetch("<?php echo admin_url('admin-ajax.php'); ?>?action=pobierz_zarezerwowane_godziny"); reservedSlots = await response.json(); function isFullyBooked(dateStr) { return reservedSlots[dateStr] && reservedSlots[dateStr].length >= availableHours.length; } function renderCalendar(month, year) { calendarEl.innerHTML = ''; const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDay = (firstDay.getDay() + 6) % 7; const header = document.createElement('div'); header.className = 'kalendarz-header'; if (!(month === today.getMonth() && year === today.getFullYear())) { const prevBtn = document.createElement('button'); prevBtn.innerText = '<'; prevBtn.className = 'kalendarz-strzalka-lewo'; prevBtn.onclick = () => { if (month > today.getMonth()) renderCalendar(month - 1, year); }; header.appendChild(prevBtn); } const title = document.createElement('span'); title.innerText = monthNames[month] + ' ' + year; header.appendChild(title); if (!(month === today.getMonth() + 1)) { const nextBtn = document.createElement('button'); nextBtn.innerText = '>'; nextBtn.className = 'kalendarz-strzalka-prawo'; nextBtn.onclick = () => { if (month < today.getMonth() + 1) renderCalendar(month + 1, year); }; header.appendChild(nextBtn); } calendarEl.appendChild(header); const grid = document.createElement('div'); grid.className = 'kalendarz-grid'; for (let d of dayNames) { const dayEl = document.createElement('div'); dayEl.className = 'kalendarz-dzien-naglowek'; dayEl.innerText = d; grid.appendChild(dayEl); } for (let i = 0; i < startDay; i++) { const empty = document.createElement('div'); empty.className = 'kalendarz-puste-pole'; grid.appendChild(empty); } for (let d = 1; d <= lastDay.getDate(); d++) { const date = new Date(year, month, d); const day = date.getDay(); const isoDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; const cell = document.createElement('div'); cell.className = 'kalendarz-komorka'; cell.innerText = d; const todayStr = today.toISOString().split('T')[0]; const cellDateStr = date.toISOString().split('T')[0]; if (blockedDays.includes(isoDate) || isFullyBooked(isoDate)) { cell.classList.add('kalendarz-zablokowany'); cell.title = 'Termin zablokowany'; } else if (availableDays.includes((day + 6) % 7) && cellDateStr >= todayStr) { cell.classList.add('kalendarz-dostepny'); cell.onclick = () => { selectedDate = isoDate; selectedHour = null; showHours(); updateButtonState(); highlightSelectedDate(d); }; } else { cell.classList.add('kalendarz-przeszly'); cell.title = 'Termin niedost\u0119pny'; } grid.appendChild(cell); } calendarEl.appendChild(grid); } function highlightSelectedDate(dayNumber) { document.querySelectorAll('.kalendarz-komorka').forEach(el => { el.classList.remove('kalendarz-wybrany'); if (parseInt(el.innerText) === dayNumber && el.classList.contains('kalendarz-dostepny')) { el.classList.add('kalendarz-wybrany'); } }); } function showHours() { timesEl.innerHTML = ''; const label = document.createElement('p'); label.innerText = 'Wybierz godzin\u0119:'; timesEl.appendChild(label); availableHours.forEach(hour => { const btn = document.createElement('button'); btn.className = 'kalendarz-godzina-btn'; btn.innerText = hour; const reserved = (reservedSlots[selectedDate] || []).includes(hour); if (reserved) { btn.classList.add('kalendarz-godzina-zajeta'); btn.disabled = true; btn.style.textDecoration = 'line-through'; } else { btn.onclick = () => { selectedHour = hour; document.querySelectorAll('.kalendarz-godzina-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); updateButtonState(); }; } timesEl.appendChild(btn); }); } function updateButtonState() { bookBtn.disabled = !(selectedDate && selectedHour); } bookBtn.addEventListener('click', function() { bookBtn.disabled = true; const formattedStart = `${selectedDate} ${selectedHour}`; const [h, m] = selectedHour.split(':'); const endHour = new Date(Date.UTC(1970, 0, 1, h, m)); endHour.setMinutes(endHour.getMinutes() + 90); const endH = String(endHour.getUTCHours()).padStart(2, '0'); const endM = String(endHour.getUTCMinutes()).padStart(2, '0'); const formattedEnd = `${selectedDate} ${endH}:${endM}`; const formData = new URLSearchParams(); formData.append('name', '<?php echo esc_js($name); ?>'); formData.append('email', '<?php echo esc_js($email); ?>'); formData.append('phone', '<?php echo esc_js($phone); ?>'); formData.append('order_id', '<?php echo esc_js($order_id); ?>'); formData.append('start', formattedStart); formData.append('end', formattedEnd); formData.append('data_konsultacji', selectedDate); formData.append('godzina_konsultacji', selectedHour); fetch(https://hooks.zapier.com/hooks_od_zapiera/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData }) .then(response => response.text()) .then(data => { container.innerHTML = '<div class="kalendarz-potwierdzenie"><h3>Twoja konsultacja została potwierdzona!</h3><p>📅 Data: ' + selectedDate + '<br>,⏰ Godzina: ' + selectedHour + '</p></div>'; fetch("<?php echo admin_url('admin-ajax.php'); ?>?action=wyslij_maila_konsultacja", { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ email: '<?php echo esc_js($email); ?>', data: selectedDate, godzina: selectedHour }) }); }); fetch("<?php echo admin_url('admin-ajax.php'); ?>?action=zapisz_rezerwacje", { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ name: '<?php echo esc_js($name); ?>', order_id: '<?php echo esc_js($order_id); ?>', date: selectedDate, time: selectedHour }) }) .then(() => fetch("<?php echo admin_url('admin-ajax.php'); ?>?action=pobierz_zarezerwowane_godziny")) .then(r => r.json()) .then(newSlots => { reservedSlots = newSlots; showHours(); }); }); renderCalendar(currentMonth, currentYear); }); </script> <style> .kalendarz-zablokowany { background-color: #f44336; color: white; cursor: not-allowed; } </style> <?php return ob_get_clean(); } add_action('wp_ajax_wyslij_maila_konsultacja', 'wyslij_maila_konsultacja'); add_action('wp_ajax_nopriv_wyslij_maila_konsultacja', 'wyslij_maila_konsultacja'); function wyslij_maila_konsultacja() { $email = sanitize_email($_POST['email']); $data = sanitize_text_field($_POST['data']); $godzina = sanitize_text_field($_POST['godzina']); $subject = 'Potwierdzenie konsultacji'; $message = "<center><font face='montserrat'>Dziękujemy za dokonanie rezerwacji.<br><br> Potwierdzamy termin konsultacji:<br><br> 📅 Data: <b>$data</b><br> ⏰ Godzina: <b>$godzina</b><br><br> Konsultacja odbędzie się w <b>formie online</b>.<br><br>Link do konsultacji zostanie wysłany<br> <b>do 10 minut</b> przed jej rozpoczęciem.<br><br> Pamiętaj o przesłaniu badań <br>oraz wypełnieniu kwestionariusza.</font></center>"; wp_mail($email, $subject, $message); wp_die(); } // Pozostała część kodu (ajax, baza danych, przekierowania) pozostaje bez zmian. add_action('wp_ajax_pobierz_zarezerwowane_godziny', 'ajax_pobierz_zarezerwowane_godziny'); add_action('wp_ajax_nopriv_pobierz_zarezerwowane_godziny', 'ajax_pobierz_zarezerwowane_godziny'); function ajax_pobierz_zarezerwowane_godziny() { global $wpdb; $table = $wpdb->prefix . 'zarezerwowane_godziny'; $results = $wpdb->get_results("SELECT data_konsultacji, godzina_konsultacji FROM $table", ARRAY_A); $output = []; foreach ($results as $row) { $godzina = substr($row['godzina_konsultacji'], 0, 5); // skróć do HH:MM $output[$row['data_konsultacji']][] = $godzina; } echo json_encode($output); wp_die(); } add_action('wp_ajax_zapisz_rezerwacje', 'ajax_zapisz_rezerwacje'); add_action('wp_ajax_nopriv_zapisz_rezerwacje', 'ajax_zapisz_rezerwacje'); function ajax_zapisz_rezerwacje() { global $wpdb; $table = $wpdb->prefix . 'zarezerwowane_godziny'; $wpdb->insert($table, [ 'imie_nazwisko' => sanitize_text_field($_POST['name']), 'numer_zamowienia' => sanitize_text_field($_POST['order_id']), 'data_konsultacji' => sanitize_text_field($_POST['date']), 'godzina_konsultacji' => sanitize_text_field($_POST['time']) ]); wp_die(); } add_action('init', 'utworz_tabele_rezerwacji_jezeli_nie_istnieje'); function utworz_tabele_rezerwacji_jezeli_nie_istnieje() { global $wpdb; $table_name = $wpdb->prefix . 'zarezerwowane_godziny'; if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, imie_nazwisko text NOT NULL, numer_zamowienia varchar(50) NOT NULL, data_konsultacji date NOT NULL, godzina_konsultacji time NOT NULL, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } add_action('woocommerce_thankyou', 'przekieruj_po_zakupie_produktu_22929'); function przekieruj_po_zakupie_produktu_22929($order_id) { if (!$order_id) return; $order = wc_get_order($order_id); if (!$order) return; foreach ($order->get_items() as $item) { $product_id = $item->get_product_id(); if ($product_id == 22929) { $redirect_url = 'https://twojadomena.com/podziekowanie-za-zamowienie/?order_id=' . $order_id; wp_redirect($redirect_url); exit; } } }
Dodatkowa stylizacja kalendarza CSS
.kalendarz-wrapper { max-width: 600px; margin: 40px auto; padding: 20px; font-family: montserrat, sans-serif; background-color: #fff; } .kalendarz-godzina-zajeta { cursor: not-allowed; opacity: 0.5; color:white; pointer-events: none; text-decoration: line-through; } .kalendarz-klient-dane { margin-bottom: 20px; font-size: 16px; } .kalendarz-graficzny { text-align: center; } .kalendarz-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 20px; font-weight: bold; } .kalendarz-header button { background: none; border: none; font-size: 22px; cursor: pointer; padding: 5px 10px; } .kalendarz-przeszly { background-color: #eee; color: #999; cursor: not-allowed; pointer-events: none; opacity: 0.6; } .kalendarz-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; } .kalendarz-dzien-naglowek { font-weight: bold; padding: 8px 0; background-color: #f0f0f0; border-radius: 6px; } .kalendarz-komorka { padding: 10px 0; border-radius: 6px; cursor: default; font-weight: 500; } .kalendarz-dostepny { background-color: #219EC5; color: white; cursor: pointer; transition: 0.2s; } .kalendarz-dostepny:hover { background-color: #219EC570; } .kalendarz-niedostepny { background-color: #f9f9f9; color: #ccc; } .kalendarz-wybrany { background-color: #166983 !important; color: #fff !important; font-weight: bold; } .kalendarz-godziny { margin-top: 20px; text-align: center; } .kalendarz-godzina-btn { margin: 5px; padding: 10px 20px; border: 1px solid #219EC5; border-radius: 8px; background-color: #219EC5; cursor: pointer; transition: 0.3s; } .kalendarz-godzina-btn:hover { background-color: #219EC570; border-color:#219EC570 !important; color:white; } .kalendarz-godzina-btn.selected { background-color: #166983; border-color: #166983 !important; font-size:18px; color: white; font-weight: 500; } .kalendarz-przycisk { display: block; margin: 30px auto 0; padding: 12px 24px; font-size: 16px; background-color: #ccc; color: white; border: none; border-radius: 8px; cursor: not-allowed; transition: background-color 0.3s ease; } .kalendarz-przycisk:enabled { background-color: #219EC5; cursor: pointer; } .kalendarz-przycisk:enabled:hover { background-color: #166983; color:white; }
Bez stylizacji, Twój kalendarz będzie kompletnie rozsypany. Stylizację możesz dodać wchodząc w Dostosuj > Dodatkowy CSS.