пока не пробовал, просто сохранил как есть
CREATE DATABASE booking_calendar;
USE booking_calendar;
-- Таблица для записей
CREATE TABLE bookings (
id INT AUTO_INCREMENT PRIMARY KEY,
booking_date DATE NOT NULL,
booking_time TIME NOT NULL,
client_name VARCHAR(100) NOT NULL,
client_phone VARCHAR(20) NOT NULL,
client_email VARCHAR(100),
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status ENUM('active', 'cancelled', 'completed') DEFAULT 'active',
UNIQUE KEY unique_booking (booking_date, booking_time)
);
-- Индексы для быстрого поиска
CREATE INDEX idx_date ON bookings(booking_date);
CREATE INDEX idx_status ON bookings(status);
<?php
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'booking_calendar');
// Подключение к БД
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Установка кодировки
$conn->set_charset("utf8mb4");
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Календарь записи</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.calendar-header {
background: #4a5568;
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.calendar-header h2 {
font-size: 24px;
font-weight: 600;
}
.month-nav {
display: flex;
gap: 20px;
}
.month-nav button {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
.month-nav button:hover {
background: rgba(255,255,255,0.3);
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
background: #e2e8f0;
padding: 10px;
text-align: center;
font-weight: 600;
color: #4a5568;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
background: #cbd5e0;
padding: 2px;
}
.calendar-day {
background: white;
min-height: 120px;
padding: 10px;
cursor: pointer;
transition: transform 0.2s;
}
.calendar-day:hover {
transform: scale(1.02);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1;
}
.calendar-day.empty {
background: #f7fafc;
cursor: default;
}
.calendar-day.empty:hover {
transform: none;
box-shadow: none;
}
.day-number {
font-weight: 600;
color: #4a5568;
margin-bottom: 5px;
}
.time-slots {
display: flex;
flex-wrap: wrap;
gap: 3px;
}
.time-slot {
background: #48bb78;
color: white;
padding: 2px 4px;
border-radius: 3px;
font-size: 10px;
cursor: pointer;
transition: background 0.3s;
}
.time-slot:hover {
background: #38a169;
}
.time-slot.booked {
background: #f56565;
cursor: not-allowed;
opacity: 0.6;
}
.time-slot.booked:hover {
background: #f56565;
}
.booking-form {
padding: 30px;
background: #f7fafc;
border-top: 2px solid #e2e8f0;
}
.form-title {
font-size: 20px;
color: #4a5568;
margin-bottom: 20px;
}
.selected-info {
background: #e2e8f0;
padding: 10px;
border-radius: 10px;
margin-bottom: 20px;
font-weight: 600;
color: #4a5568;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #4a5568;
font-weight: 500;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.btn-submit {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.3s;
}
.btn-submit:hover {
transform: translateY(-2px);
}
.btn-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.message {
padding: 10px;
border-radius: 10px;
margin-top: 20px;
}
.message.success {
background: #c6f6d5;
color: #22543d;
}
.message.error {
background: #fed7d7;
color: #742a2a;
}
</style>
</head>
<body>
<div class="container">
<div class="calendar-header">
<h2 id="currentMonth"></h2>
<div class="month-nav">
<button onclick="changeMonth(-1)">← Предыдущий</button>
<button onclick="changeMonth(1)">Следующий →</button>
</div>
</div>
<div class="weekdays">
<div>Пн</div>
<div>Вт</div>
<div>Ср</div>
<div>Чт</div>
<div>Пт</div>
<div>Сб</div>
<div>Вс</div>
</div>
<div class="calendar-grid" id="calendar"></div>
<div class="booking-form">
<h3 class="form-title">Запись на услугу</h3>
<div class="selected-info" id="selectedInfo">
Выберите дату и время
</div>
<form id="bookingForm">
<div class="form-group">
<label for="name">Имя *</label>
<input type="text" id="name" required>
</div>
<div class="form-group">
<label for="phone">Телефон *</label>
<input type="tel" id="phone" required pattern="^\+?[0-9\s\-\(\)]+$">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email">
</div>
<div class="form-group">
<label for="message">Сообщение</label>
<textarea id="message"></textarea>
</div>
<button type="submit" class="btn-submit" id="submitBtn">Записаться</button>
</form>
<div id="message" class="message" style="display: none;"></div>
</div>
</div>
<script>
let currentDate = new Date();
let selectedDate = null;
let selectedTime = null;
let bookedSlots = {};
// Доступное время (как на вашем скриншоте)
const timeSlots = [
'09:00', '10:00', '11:00',
'09:30', '10:30', '11:30',
'12:00', '13:00', '14:00', '15:00', '16:00',
'12:30', '13:30', '14:30', '15:30', '16:30',
'17:00', '17:30'
];
function loadBookedSlots(year, month) {
fetch(`get_bookings.php?year=${year}&month=${month}`)
.then(response => response.json())
.then(data => {
bookedSlots = data;
renderCalendar();
});
}
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
document.getElementById('currentMonth').textContent =
new Date(year, month).toLocaleString('ru', { month: 'long', year: 'numeric' });
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
let startDay = firstDay.getDay() - 1;
if (startDay < 0) startDay = 6;
const totalDays = lastDay.getDate();
const calendar = document.getElementById('calendar');
calendar.innerHTML = '';
// Пустые ячейки до первого дня месяца
for (let i = 0; i < startDay; i++) {
calendar.appendChild(createEmptyDay());
}
// Ячейки для каждого дня месяца
for (let day = 1; day <= totalDays; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const dayBookings = bookedSlots[dateStr] || [];
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
dayElement.innerHTML = `
<div class="day-number">${day}</div>
<div class="time-slots" id="slots-${dateStr}">
${renderTimeSlots(dateStr, dayBookings)}
</div>
`;
calendar.appendChild(dayElement);
}
}
function renderTimeSlots(dateStr, bookedTimes) {
return timeSlots.map(time => {
const isBooked = bookedTimes.includes(time);
return `<span class="time-slot ${isBooked ? 'booked' : ''}"
onclick="selectTime('${dateStr}', '${time}', ${isBooked})">
${time}
</span>`;
}).join('');
}
function selectTime(date, time, isBooked) {
if (isBooked) {
alert('Это время уже занято');
return;
}
selectedDate = date;
selectedTime = time;
const formattedDate = new Date(date).toLocaleString('ru', {
day: 'numeric',
month: 'long'
});
document.getElementById('selectedInfo').textContent =
`Выбрано: ${formattedDate} в ${time}`;
// Подсветка выбранного времени
document.querySelectorAll('.time-slot').forEach(slot => {
slot.style.background = '';
});
event.target.style.background = '#667eea';
}
function createEmptyDay() {
const empty = document.createElement('div');
empty.className = 'calendar-day empty';
return empty;
}
function changeMonth(delta) {
currentDate.setMonth(currentDate.getMonth() + delta);
loadBookedSlots(currentDate.getFullYear(), currentDate.getMonth() + 1);
}
// Обработка формы
document.getElementById('bookingForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!selectedDate || !selectedTime) {
alert('Пожалуйста, выберите дату и время');
return;
}
const formData = {
date: selectedDate,
time: selectedTime,
name: document.getElementById('name').value,
phone: document.getElementById('phone').value,
email: document.getElementById('email').value,
message: document.getElementById('message').value
};
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
fetch('save_booking.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
const messageDiv = document.getElementById('message');
messageDiv.style.display = 'block';
if (data.success) {
messageDiv.className = 'message success';
messageDiv.textContent = 'Запись успешно создана!';
// Очистка формы
document.getElementById('bookingForm').reset();
document.getElementById('selectedInfo').textContent = 'Выберите дату и время';
// Перезагрузка календаря
loadBookedSlots(currentDate.getFullYear(), currentDate.getMonth() + 1);
selectedDate = null;
selectedTime = null;
} else {
messageDiv.className = 'message error';
messageDiv.textContent = data.message || 'Ошибка при сохранении';
}
submitBtn.disabled = false;
setTimeout(() => {
messageDiv.style.display = 'none';
}, 5000);
})
.catch(error => {
console.error('Error:', error);
submitBtn.disabled = false;
});
});
// Инициализация
loadBookedSlots(currentDate.getFullYear(), currentDate.getMonth() + 1);
</script>
</body>
</html>
<?php
require_once 'config.php';
header('Content-Type: application/json');
$year = isset($_GET['year']) ? intval($_GET['year']) : date('Y');
$month = isset($_GET['month']) ? intval($_GET['month']) : date('m');
$start_date = "$year-$month-01";
$end_date = date('Y-m-t', strtotime($start_date));
$sql = "SELECT booking_date, GROUP_CONCAT(DATE_FORMAT(booking_time, '%H:%i') ORDER BY booking_time) as times
FROM bookings
WHERE booking_date BETWEEN ? AND ?
AND status = 'active'
GROUP BY booking_date";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $start_date, $end_date);
$stmt->execute();
$result = $stmt->get_result();
$bookings = [];
while ($row = $result->fetch_assoc()) {
$bookings[$row['booking_date']] = explode(',', $row['times']);
}
echo json_encode($bookings);
?>
<?php
require_once 'config.php';
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(['success' => false, 'message' => 'Нет данных']);
exit;
}
// Валидация
if (empty($data['date']) || empty($data['time']) || empty($data['name']) || empty($data['phone'])) {
echo json_encode(['success' => false, 'message' => 'Заполните все обязательные поля']);
exit;
}
// Проверка, свободно ли время
$check_sql = "SELECT id FROM bookings WHERE booking_date = ? AND booking_time = ? AND status = 'active'";
$check_stmt = $conn->prepare($check_sql);
$check_stmt->bind_param("ss", $data['date'], $data['time']);
$check_stmt->execute();
$check_result = $check_stmt->get_result();
if ($check_result->num_rows > 0) {
echo json_encode(['success' => false, 'message' => 'Это время уже занято']);
exit;
}
// Сохранение
$sql = "INSERT INTO bookings (booking_date, booking_time, client_name, client_phone, client_email, message)
VALUES (?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param(
"ssssss",
$data['date'],
$data['time'],
$data['name'],
$data['phone'],
$data['email'],
$data['message']
);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Запись создана']);
} else {
echo json_encode(['success' => false, 'message' => 'Ошибка базы данных']);
}
?>
Комментарии
Пока нет комментариев. Будьте первым!