📅 Расписание + 1С: безопасная связка на PHP
Корпоративный календарь приёма пациентов — полный разбор функционала, механизмов безопасности и архитектуры.
📌 Что это за система?
Это веб-приложение для просмотра расписания на базе PHP + MySQL, которое интегрируется с внешним API (1С: Медицина / Стоматология). Пациенты (или сотрудники) авторизуются по телефону, выбирают дату и получают актуальные записи: ФИО пациента, время приёма, услугу, центр, возраст. Дополнительно система позволяет смотреть текст диагностики / заключение по специальному коду.
🧩 Ключевой функционал
🔐 Регистрация и вход
- Телефон (11 цифр) + пароль (min 4 символа)
- Защита от ботов: математическая CAPTCHA (пример: 12 + 7 = ?)
- Сессии PHP + хэширование паролей (password_hash)
- Админ-логин с отдельной учётной записью
📅 Просмотр расписания
- Выбор даты → запрос к API 1С (JSON)
- Отображение: время приёма, пациент, услуга, центр, возраст (годы+месяцы), код диагностики
- Фильтрация записей по центрам (Тропарёво, Ясенево, Коньково и др.)
- Если пациенту >50 лет — возраст скрыт (GDPR‑like забота)
🔬 Диагностический блок
- Каждая запись может содержать поле «Диагностика» (код). По клику система делает отдельный API-запрос с этим кодом.
- Показывает полный текст заключения, дату запроса — полезно для врачей и администраторов.
🛠️ Админ-панель
- CRUD пользователей (редактирование, сброс пароля, удаление)
- Настройка API URL, токена, rate-limit (число запросов, интервал, блокировка)
- Просмотр даты последнего входа, метка администратора
GET /api.php?token=***&DatRasp=20251201&telephone=79261234567 → JSON массив записей
🛡️ Защита: от XSS до ограничения частоты запросов
Приложение разработано с учётом стандартов веб-безопасности, чтобы исключить типичные уязвимости.
🧼 XSS (межсайтовый скриптинг)
Каждый вывод пользовательских данных или данных из API проходит через htmlspecialchars($string, ENT_QUOTES, 'UTF-8'). Функция h() экранирует кавычки, угловые скобки, что предотвращает внедрение JavaScript.
foreach ($_GET as $key => $value) {
$_GET[$key] = is_string($value) ? trim($value) : $value;
}
🗄️ Защита от SQL-инъекций
Используется PDO с подготовленными запросами + ATTR_EMULATE_PREPARES = false. Ни одно значение не подставляется напрямую в SQL — только плейсхолдеры ? или именованные параметры.
$stmt->execute([$phone]);
Таблицы создаются автоматически, но структура защищена от внедрения через разделение схемы и данных.
⏱️ Rate limiting (защита от брутфорса и DoS)
Реализовано на уровне БД: для каждого пользователя фиксируется время запросов к API 1С. Администратор может настроить:
- Количество запросов (rate_limit_requests) — например, 5 запросов
- Временной интервал (rate_limit_time) — например, 60 секунд
- Задержка после превышения (cooldown) — обязательная пауза
Старые записи удаляются каждый час, таблица не разрастается.
🔐 Хэширование паролей и сессии
Пароли хранятся с password_hash() (bcrypt по умолчанию). При авторизации используется password_verify(). Администратор может сбросить пароль до значения по умолчанию, но хэш в БД всегда необратимый. Сессии защищены стандартными механизмами PHP + session_start(), logout уничтожает сессию.
⚙️ Архитектура и ключевые механики
📁 Структура конфигурации
Отдельный файл config/db_config.php содержит параметры БД, api_url, api_token, настройки rate-limit. Все поля проверяются на наличие — если чего-то нет, система выдаёт понятную ошибку.
🧮 Расчёт возраста
Функция calculateAge() работает с датами рождения из 1С, корректно обрабатывает некорректные значения ("01.01.0001") и возвращает красивый формат: «5 лет 2 месяца» или «3 дня». Логика: если возраст > 50 лет — не показывать (конфиденциальность).
🌐 CURL-запросы к 1С
Запросы выполняются через cURL с отключённой проверкой SSL для внутренних сетей, таймаут 30 секунд. Ответ очищается от BOM-символов, JSON декодируется с проверкой ошибок.
Если API возвращает не расписание, а текст запроса/диагностики — система переключается в режим отображения диагностического блока, где можно прочитать полное заключение врача.
🗂️ База данных (MySQL)
Таблицы: users, settings, rate_limits. При старте проверяется существование и создаются при необходимости. Для управления настройками используется паттерн Key-Value (settings).
📱 Пользовательский интерфейс
Адаптивный, мобильный дизайн (font Inter, grid, медиа-запросы). Основные фишки:
Автоматическое обновление счётчика найденных записей, динамическая фильтрация через JavaScript без перезагрузки страницы (data-атрибуты).
function filterByCenter(center) {
cards.forEach(card => {
if (center === 'all' || card.dataset.center === center) { ... }
});
}
🧪 Что происходит под капотом при запросе расписания?
- Пользователь авторизован, выбирает дату → нажимает «Показать расписание» (GET-параметры).
- Срабатывает checkRateLimit(): проверяет, не превысил ли пользователь лимит запросов за период, а также не находится ли в кулдауне.
- Формируется URL: если есть код диагностики — используется параметр ND, иначе — телефон пользователя.
- cURL получает JSON → удаляется BOM → декодируется.
- Анализируется содержимое: если в ответе есть поля «ТекстЗапроса» или «ДатаЗапроса», то открывается режим диагностики. Иначе стандартное расписание.
- Для каждой записи вычисляется возраст, проверяются перерывы, формируется HTML карточка.
- Запись о запросе добавляется в таблицу rate_limits, устаревшие логи удаляются.
🚀 Ключевые особенности безопасности (чек-лист)
- Защита от XSS: двойное экранирование + фильтрация входящих данных.
- SQL injection: PDO prepare / execute, отсутствие конкатенации строк запроса.
- Rate Limiting + cooldown: предотвращает перебор запросов и нагрузку на API 1С.
- Пароли: только хэш в БД, никогда открытым текстом; сброс пароля через админку.
- Контроль доступа: разделение ролей user / admin, проверка сессии перед отображением панели.
- Капча при регистрации: динамическая сумма, хранится в скрытом поле (серверная валидация).
- Обработка ошибок: нет показа чувствительных данных пользователю (используются общие сообщения).
Также реализована автоматическая чистка rate_limits раз в час, чтобы БД не разрасталась, и продуман механизм «администратор не может удалить сам себя».
📌 Итог: для каких задач идеально подходит?
✔️ Медицинские центры, где 1С служит системой учёта, а сотрудникам нужен быстрый доступ к расписанию на день.
✔️ Замена громоздких RDP-подключений к серверу 1С — веб-интерфейс работает с телефонов и планшетов.
✔️ Безопасная выдача диагностических заключений по коду (без прямого доступа к БД).
✔️ Администратор может управлять лимитами API и пользователями без программиста.
Готово к расширению: можно добавить двухфакторную аутентификацию, логирование действий, экспорт расписания в PDF.
✨ Архитектура демонстрирует грамотный подход: разделение логики, защита по стандартам OWASP, адаптивный интерфейс и надёжная интеграция с корпоративной экосистемой 1С.
PHP календарь
<?php
session_start();
// Защита от XSS
function h($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
// Защита от SQL-инъекций через глобальные массивы (доп. уровень)
foreach ($_GET as $key => $value) {
$_GET[$key] = is_string($value) ? trim($value) : $value;
}
foreach ($_POST as $key => $value) {
$_POST[$key] = is_string($value) ? trim($value) : $value;
}
// ========== ПОДКЛЮЧЕНИЕ КОНФИГА ==========
$configPath = __DIR__ . '/../../../papkakonfiga/db_config.php';
if (!file_exists($configPath)) {
die("Ошибка конфигурации: файл настроек не найден по пути: " . $configPath);
}
$config = require $configPath;
// Проверяем наличие всех необходимых полей
$requiredFields = ['db_host', 'db_name', 'db_user', 'db_pass', 'api_url', 'api_token', 'rate_limit_requests', 'rate_limit_time', 'rate_limit_cooldown'];
$missingFields = [];
foreach ($requiredFields as $field) {
if (!isset($config[$field])) {
$missingFields[] = $field;
}
}
if (!empty($missingFields)) {
die("Ошибка конфигурации: отсутствуют поля: " . implode(', ', $missingFields));
}
// ========== ПОДКЛЮЧЕНИЕ К БД ==========
try {
$db = new PDO(
"mysql:host={$config['db_host']};dbname={$config['db_name']};charset=utf8mb4",
$config['db_user'],
$config['db_pass']
);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Создание таблиц
$db->exec("CREATE TABLE IF NOT EXISTS settings (
setting_key VARCHAR(100) PRIMARY KEY,
setting_value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)");
$db->exec("CREATE TABLE IF NOT EXISTS rate_limits (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
request_time DATETIME NOT NULL,
INDEX idx_user_time (user_id, request_time)
)");
$db->exec("CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
phone VARCHAR(20) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
is_admin TINYINT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP NULL
)");
} catch (PDOException $e) {
die("Ошибка базы данных: " . $e->getMessage());
}
// Отключаем эмуляцию подготовленных запросов
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// Включаем режим ошибок (у вас уже есть)
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// ========== ФУНКЦИИ ==========
// Функция для расчета возраста
function calculateAge($birthDate, $currentDate = null) {
if (empty($birthDate)) return '';
// Некорректные даты
if ($birthDate == '01.01.0001 0:00:00' || $birthDate == '0000-00-00') {
return '';
}
try {
$birth = new DateTime($birthDate);
if ($currentDate && $currentDate != '0000-00-00') {
$current = new DateTime($currentDate);
} else {
$current = new DateTime();
}
$diff = $birth->diff($current);
$years = $diff->y;
$months = $diff->m;
$days = $diff->d;
// Если больше 50 лет - не показываем
if ($years > 50) {
return '';
}
if ($years > 0) {
return $years . ' ' . getYearsWord($years) . ' ' . $months . ' ' . getMonthsWord($months);
} elseif ($months > 0) {
return $months . ' ' . getMonthsWord($months);
} else {
return $days . ' ' . getDaysWord($days);
}
} catch (Exception $e) {
return '';
}
}
function getYearsWord($years) {
if ($years % 10 == 1 && $years % 100 != 11) return 'год';
if (in_array($years % 10, [2,3,4]) && !in_array($years % 100, [12,13,14])) return 'года';
return 'лет';
}
function getMonthsWord($months) {
if ($months % 10 == 1 && $months % 100 != 11) return 'месяц';
if (in_array($months % 10, [2,3,4]) && !in_array($months % 100, [12,13,14])) return 'месяца';
return 'месяцев';
}
function getDaysWord($days) {
if ($days % 10 == 1 && $days % 100 != 11) return 'день';
if (in_array($days % 10, [2,3,4]) && !in_array($days % 100, [12,13,14])) return 'дня';
return 'дней';
}
function getSetting($db, $key, $default = '') {
$stmt = $db->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['setting_value'] : $default;
}
function updateSetting($db, $key, $value) {
$stmt = $db->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
return $stmt->execute([$key, $value, $value]);
}
function checkRateLimit($db, $userId, $config) {
if (!$userId) return ['allowed' => true, 'message' => null];
try {
$now = new DateTime();
$stmt = $db->prepare("SELECT request_time FROM rate_limits WHERE user_id = ? ORDER BY request_time DESC LIMIT 1");
$stmt->execute([$userId]);
$lastRequest = $stmt->fetch(PDO::FETCH_ASSOC);
if ($lastRequest) {
$lastTime = new DateTime($lastRequest['request_time']);
$interval = $now->getTimestamp() - $lastTime->getTimestamp();
if ($interval < $config['rate_limit_cooldown']) {
return ['allowed' => false, 'message' => "Подождите " . ($config['rate_limit_cooldown'] - $interval) . " секунд"];
}
}
$periodStart = new DateTime();
$periodStart->modify('-' . $config['rate_limit_time'] . ' seconds');
$stmt = $db->prepare("SELECT COUNT(*) as count FROM rate_limits WHERE user_id = ? AND request_time > ?");
$stmt->execute([$userId, $periodStart->format('Y-m-d H:i:s')]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result['count'] >= $config['rate_limit_requests']) {
return ['allowed' => false, 'message' => "Лимит: " . $config['rate_limit_requests'] . " запросов в " . $config['rate_limit_time'] . " сек"];
}
$stmt = $db->prepare("INSERT INTO rate_limits (user_id, request_time) VALUES (?, ?)");
$stmt->execute([$userId, $now->format('Y-m-d H:i:s')]);
$cleanupTime = new DateTime();
$cleanupTime->modify('-1 hour');
$stmt = $db->prepare("DELETE FROM rate_limits WHERE request_time < ?");
$stmt->execute([$cleanupTime->format('Y-m-d H:i:s')]);
return ['allowed' => true, 'message' => null];
} catch (PDOException $e) {
error_log("Rate limit error: " . $e->getMessage());
return ['allowed' => true, 'message' => null];
}
}
// ========== ИНИЦИАЛИЗАЦИЯ НАСТРОЕК ==========
//if (!getSetting($db, 'api_url')) {
// updateSetting($db, 'api_url', $config['api_url']);
// updateSetting($db, 'api_token', $config['api_token']);
// updateSetting($db, 'rate_limit_requests', $config['rate_limit_requests']);
// updateSetting($db, 'rate_limit_time', $config['rate_limit_time']);
// updateSetting($db, 'rate_limit_cooldown', $config['rate_limit_cooldown']);
//}
// Создаем администратора
$adminPasswordHash = password_hash($config['admin_default_password'] ?? '123456', PASSWORD_DEFAULT);
$stmt = $db->prepare("SELECT id FROM users WHERE phone = 'admin'");
$stmt->execute();
if (!$stmt->fetch()) {
$stmt = $db->prepare("INSERT INTO users (phone, password, full_name, is_admin) VALUES (?, ?, ?, 1)");
$stmt->execute(['admin', $adminPasswordHash, 'Администратор']);
}
$apiUrlBase = $config['api_url'];
$defaultToken = $config['api_token'];
// ========== ОБРАБОТКА ЗАПРОСОВ ==========
$error = '';
$success = '';
$admin_error = '';
$action = isset($_GET['action']) ? $_GET['action'] : 'login';
// РЕГИСТРАЦИЯ - ИСПРАВЛЕННАЯ ВЕРСИЯ
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['register'])) {
$phone = preg_replace('/[^0-9]/', '', $_POST['phone']);
$password = $_POST['password'];
$confirm = $_POST['confirm_password'];
$full_name = trim($_POST['full_name'] ?? '');
// Получаем данные капчи
$user_captcha = trim($_POST['captcha'] ?? '');
$expected_captcha = trim($_POST['captcha_expected'] ?? '');
// ВАЖНО: Проверяем, что капча не пустая и совпадает
if (empty($user_captcha)) {
$error = "Введите ответ на пример";
} elseif (empty($expected_captcha)) {
$error = "Ошибка验证 капчи, попробуйте снова";
} elseif ($user_captcha != $expected_captcha) {
$error = "Неверный ответ на пример";
} elseif (empty($phone) || empty($password)) {
$error = "Заполните обязательные поля";
} elseif (strlen($phone) != 11) {
$error = "Телефон: 11 цифр";
} elseif (strlen($password) < 4) {
$error = "Пароль минимум 4 символа";
} elseif ($password != $confirm) {
$error = "Пароли не совпадают";
} else {
$stmt = $db->prepare("SELECT id FROM users WHERE phone = ?");
$stmt->execute([$phone]);
if ($stmt->fetch()) {
$error = "Пользователь уже существует";
} else {
$hashed = password_hash($password, PASSWORD_DEFAULT);
// Вариант 1: указываем все 4 поля
$stmt = $db->prepare("INSERT INTO users (phone, password, full_name, is_admin) VALUES (?, ?, ?, ?)");
$stmt->execute([$phone, $hashed, $full_name, 0]);
// ИЛИ Вариант 2: is_admin не указываем (если в БД DEFAULT 0)
// $stmt = $db->prepare("INSERT INTO users (phone, password, full_name) VALUES (?, ?, ?)");
// $stmt->execute([$phone, $hashed, $full_name]);
if ($stmt) {
$success = "Регистрация успешна!";
$action = 'login';
} else {
$error = "Ошибка регистрации";
}
}
}
}
// Авторизация
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['login'])) {
$phone = preg_replace('/[^0-9]/', '', $_POST['phone']);
$password = $_POST['password'];
if (empty($phone) || empty($password)) {
$error = "Введите телефон и пароль";
} else {
$stmt = $db->prepare("SELECT id, phone, password, full_name, is_admin FROM users WHERE phone = ?");
$stmt->execute([$phone]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_phone'] = $user['phone'];
$_SESSION['user_name'] = $user['full_name'];
$_SESSION['is_admin'] = $user['is_admin'];
$stmt = $db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
$stmt->execute([$user['id']]);
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
} else {
$error = "Неверный телефон или пароль";
}
}
}
// Админ-логин
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['admin_login'])) {
$adminLogin = $_POST['admin_login_name'] ?? '';
$adminPass = $_POST['admin_password'] ?? '';
if ($adminLogin == 'admin' && password_verify($adminPass, $adminPasswordHash)) {
$_SESSION['user_id'] = 1;
$_SESSION['user_phone'] = 'admin';
$_SESSION['user_name'] = 'Администратор';
$_SESSION['is_admin'] = 1;
header('Location: ' . $_SERVER['PHP_SELF'] . '?admin_panel=1');
exit;
} else {
$admin_error = "Неверный логин или пароль";
}
}
// Выход
if (isset($_GET['logout'])) {
session_destroy();
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
// Админ-панель
$showAdminPanel = isset($_GET['admin_panel']) && ($_SESSION['is_admin'] ?? 0) == 1;
$users = [];
if ($showAdminPanel) {
$stmt = $db->query("SELECT id, phone, full_name, is_admin, created_at, last_login FROM users ORDER BY id DESC");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Обработка админ-действий
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_POST['update_api_settings'])) {
updateSetting($db, 'api_url', trim($_POST['api_url']));
updateSetting($db, 'api_token', trim($_POST['api_token']));
updateSetting($db, 'rate_limit_requests', (int)$_POST['rate_limit_requests']);
updateSetting($db, 'rate_limit_time', (int)$_POST['rate_limit_time']);
updateSetting($db, 'rate_limit_cooldown', (int)$_POST['rate_limit_cooldown']);
$_SESSION['admin_message'] = "Настройки обновлены";
header('Location: ' . $_SERVER['PHP_SELF'] . '?admin_panel=1');
exit;
}
if (isset($_POST['reset_password'])) {
$new_hash = password_hash($config['admin_default_password'] ?? '123456', PASSWORD_DEFAULT);
$stmt = $db->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$new_hash, $_POST['user_id']]);
$_SESSION['admin_message'] = "Пароль сброшен";
header('Location: ' . $_SERVER['PHP_SELF'] . '?admin_panel=1');
exit;
}
if (isset($_POST['delete_user']) && $_POST['user_id'] != $_SESSION['user_id']) {
$stmt = $db->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$_POST['user_id']]);
$_SESSION['admin_message'] = "Пользователь удален";
header('Location: ' . $_SERVER['PHP_SELF'] . '?admin_panel=1');
exit;
}
if (isset($_POST['edit_user'])) {
$phone = preg_replace('/[^0-9]/', '', $_POST['phone']);
$stmt = $db->prepare("UPDATE users SET full_name = ?, phone = ? WHERE id = ?");
$stmt->execute([$_POST['full_name'], $phone, $_POST['user_id']]);
$_SESSION['admin_message'] = "Данные обновлены";
header('Location: ' . $_SERVER['PHP_SELF'] . '?admin_panel=1');
exit;
}
}
}
$isAuthenticated = isset($_SESSION['user_id']);
$userPhone = $isAuthenticated ? $_SESSION['user_phone'] : '';
// ========== ПОЛУЧЕНИЕ РАСПИСАНИЯ ==========
$schedule = [];
$error_api = null;
$submitted = false;
$date = isset($_GET['date']) ? preg_replace('/[^0-9]/', '', $_GET['date']) : date('Ymd');
$diagnosticCode = isset($_GET['diagnostic']) ? preg_replace('/[^0-9]/', '', $_GET['diagnostic']) : null;
$showDiagnostic = false;
$diagnosticText = '';
$diagnosticDate = '';
if ($isAuthenticated && isset($_GET['show_schedule']) && !$showAdminPanel) {
$rateLimitCheck = checkRateLimit($db, $_SESSION['user_id'], $config);
if (!$rateLimitCheck['allowed']) {
$error_api = $rateLimitCheck['message'];
} else {
$submitted = true;
if ($diagnosticCode) {
$url = $apiUrlBase . "?token=" . urlencode($defaultToken) . "&DatRasp=" . $date . "&ZZ=T&ND=" . urlencode($diagnosticCode);
} else {
$url = $apiUrlBase . "?token=" . urlencode($defaultToken) . "&DatRasp=" . $date . "&telephone=" . urlencode($userPhone);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
$error_api = "Ошибка соединения: " . $curlError;
} elseif ($httpCode != 200) {
$error_api = "Ошибка сервера: " . $httpCode;
} else {
// Удаляем BOM если есть
$bom = pack('H*','EFBBBF');
$response = preg_replace('/^' . $bom . '/', '', $response);
$data = json_decode($response, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
// Проверяем, есть ли поле ТекстЗапроса в любом элементе
if (isset($data[0]['ТекстЗапроса']) || isset($data[0]['ДатаЗапроса'])) {
// Это диагностическая информация - сохраняем ВСЕ записи
$diagnosticEntries = []; // Массив для всех записей
foreach ($data as $entry) {
if (!empty($entry['ТекстЗапроса']) || !empty($entry['ДатаЗапроса'])) {
$diagnosticEntries[] = [
'date' => $entry['ДатаЗапроса'] ?? 'Дата не указана',
'text' => $entry['ТекстЗапроса'] ?? ''
];
}
}
if (!empty($diagnosticEntries)) {
$diagnosticData = $diagnosticEntries; // Сохраняем ВСЕ записи
$showDiagnostic = true;
}
$schedule = []; // Очищаем расписание
} else {
$schedule = $data;
}
} else {
$error_api = "Ошибка обработки данных: " . json_last_error_msg();
}
}
}
}
$admin_message = $_SESSION['admin_message'] ?? null;
unset($_SESSION['admin_message']);
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Моё расписание</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-title {display: none !important;}
/* Фильтр по центрам */
.filter-section {
background: #f7fafc;
border-radius: 16px;
padding: 16px;
margin-bottom: 20px;
}
.filter-title {
font-size: 0.85rem;
font-weight: 600;
color: #4a5568;
margin-bottom: 12px;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.filter-btn {
padding: 6px 14px;
border-radius: 30px;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #e2e8f0;
background: white;
color: #4a5568;
}
.filter-btn.active {
background: #612f8c;
color: white;
border-color: #612f8c;
}
.filter-btn:hover:not(.active) {
background: #edf2f7;
}
.filter-reset {
background: #e2e8f0;
color: #4a5568;
}
.filter-reset.active {
background: #718096;
border-color: #718096;
}
.filter-count {
font-size: 0.7rem;
margin-left: 6px;
opacity: 0.7;
}
.no-results {
text-align: center;
padding: 40px 20px;
color: #718096;
}
.card { background: white; border-radius: 24px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); overflow: hidden; margin-bottom: 20px; }
.card-header { background: #612f8c; padding: 24px; text-align: center; color: white; }
.card-header h1 { font-size: 1.8rem; margin-bottom: 8px; color: white}
.card-header p { opacity: 0.9; font-size: 0.9rem; }
.card-body { padding: 24px; background: #fff; }
.admin-header { background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%); }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-size: 0.85rem; font-weight: 600; color: #4a5568; margin-bottom: 8px; }
.form-group input, .form-group textarea { width: 100%; padding: 12px 16px; border: 2px solid #e2e8f0; border-radius: 12px; font-size: 1rem; transition: all 0.3s; }
.form-group input:focus { outline: none; border-color: #612f8c; }
.btnn { padding: 12px 24px; border: none; border-radius: 40px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s; text-decoration: none; display: inline-block; text-align: center; background: #612f8c; color: white; width: 100%; }
.btnn:hover { padding: 12px 24px; border: none; border-radius: 40px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s; text-decoration: none; display: inline-block; text-align: center;background: #8b4dc0; color: white; width: 100%; }
.btnn-secondary { border: #ccc 1px solid}
.btnn-danger { background: #e53e3e; }
.btnn-sm { padding: 8px 16px; font-size: 0.85rem; width: auto; }
.schedule-list { display: flex; flex-direction: column; gap: 12px; margin-top: 20px; }
.schedule-card-item { background: #f7fafc; border-radius: 16px; padding: 16px; border-left: 4px solid #612f8c; transition: all 0.2s; }
.schedule-card-item.break { border-left-color: #ed8936; background: #fff5f0; }
.schedule-time { font-size: 1.2rem; font-weight: 700; color: #612f8c; margin-bottom: 8px; }
.schedule-time.break-time { color: #ed8936; }
.schedule-patient { font-size: 1rem; font-weight: 600; color: #2d3748; margin-bottom: 4px; }
.schedule-service { font-size: 0.85rem; color: #718096; margin-bottom: 8px; }
.schedule-diagnostic { margin-top: 8px; }
.diagnostic-link { display: inline-flex; align-items: center; gap: 6px; background: #e9d8fd; color: #612f8c; padding: 6px 12px; border-radius: 20px; font-size: 0.75rem; text-decoration: none; font-weight: 500; }
.diagnostic-link:hover { background: #d6bcfa; }
.diagnostic-box { background: #edf2f7; border-radius: 16px; padding: 20px; margin-top: 20px; }
.diagnostic-text {
background: white;
padding: 12px 16px;
border-radius: 12px;
border-left: 4px solid #612f8c;
white-space: pre-wrap;
font-family: monospace;
font-size: 0.8rem;
line-height: 1.4; /* Уменьшили межстрочный интервал */
margin-top: 0;
}
.alert { padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; }
.alert-error { background: #fed7d7; color: #c53030; }
.alert-success { background: #c6f6d5; color: #22543d; }
.alert-info { background: #edf2f7; color: #4a5568; text-align: center; }
.user-info { background: #edf2f7; padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
.link { text-align: center; margin-top: 20px; }
.link a { color: #612f8c; text-decoration: none; font-weight: 600; }
.users-table { width: 100%; border-collapse: collapse; }
.users-table th, .users-table td { padding: 12px; text-align: left; border-bottom: 1px solid #e2e8f0; }
.users-table th { background: #f7fafc; font-weight: 600; }
.badge-admin { background: #e53e3e; color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.7rem; }
.badge-user { background: #48bb78; color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.7rem; }
.action-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
.schedule-center {
margin-bottom: 8px;
}
.schedule-center span {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
}
hr { margin: 20px 0; border-color: #e2e8f0; }
@media (max-width: 480px) {
.card-header { padding: 20px; }
.card-body { padding: 20px; }
.schedule-card-item { padding: 12px; }
}
</style>
</head>
<body>
<div class="container">
<?php if ($showAdminPanel): ?>
<!-- АДМИН-ПАНЕЛЬ -->
<div class="card">
<div class="card-header admin-header">
<h1>🔧 Админ-панель</h1>
<p>Управление пользователями и настройками</p>
</div>
<div class="card-body">
<?php if ($admin_message): ?>
<div class="alert alert-success">✅ <?php echo htmlspecialchars($admin_message); ?></div>
<?php endif; ?>
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<a href="<?php echo $_SERVER['PHP_SELF']; ?>" class="btnn btn-sm" style="background:#edf2f7; color:#4a5568;">📅 Основная</a>
<a href="?logout" class="btnn btn-sm btn-danger">🚪 Выйти</a>
</div>
<h3>👥 Пользователи</h3>
<div style="overflow-x: auto; margin-top: 16px;">
<table class="users-table">
<thead>
<tr><th>ID</th><th>Телефон</th><th>Имя</th><th>Роль</th><th>Действия</th>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo $user['id']; ?></td>
<td><?php echo htmlspecialchars($user['phone']); ?></td>
<td><?php echo htmlspecialchars($user['full_name'] ?: '—'); ?></td>
<td><?php echo $user['is_admin'] ? '<span class="badge-admin">Админ</span>' : '<span class="badge-user">Пользователь</span>'; ?></td>
<td class="action-buttons">
<button onclick='editUser(<?php echo $user['id']; ?>, "<?php echo htmlspecialchars($user['phone']); ?>", "<?php echo htmlspecialchars($user['full_name']); ?>")' class="btnn btn-sm" style="background:#4299e1;">✏️</button>
<button onclick='resetPassword(<?php echo $user['id']; ?>)' class="btnn btn-sm" style="background:#ed8936;">🔑</button>
<?php if ($user['id'] != $_SESSION['user_id']): ?>
<button onclick='deleteUser(<?php echo $user['id']; ?>)' class="btnn btn-sm btn-danger">🗑️</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div id="editModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); align-items:center; justify-content:center; z-index:1000;">
<div style="background:white; padding:30px; border-radius:24px; max-width:400px; width:90%;">
<h3>Редактировать</h3>
<form method="POST">
<input type="hidden" name="user_id" id="edit_user_id">
<div class="form-group"><label>Телефон</label><input type="tel" name="phone" id="edit_phone" required></div>
<div class="form-group"><label>Имя</label><input type="text" name="full_name" id="edit_full_name"></div>
<div style="display:flex; gap:10px;">
<button type="submit" name="edit_user" class="btnn">Сохранить</button>
<button type="button" onclick="closeModal()" class="btnn btn-secondary">Отмена</button>
</div>
</form>
</div>
</div>
<form method="POST" id="resetForm" style="display:none;"><input type="hidden" name="user_id" id="reset_user_id"><input type="hidden" name="reset_password" value="1"></form>
<form method="POST" id="deleteForm" style="display:none;"><input type="hidden" name="user_id" id="delete_user_id"><input type="hidden" name="delete_user" value="1"></form>
<?php elseif (!$isAuthenticated): ?>
<!-- АВТОРИЗАЦИЯ -->
<div class="card">
<div class="card-header">
<h1>📅 Расписание</h1>
<p>Вход в систему</p>
</div>
<div class="card-body">
<?php if ($error): ?>
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
<?php endif; ?>
<?php if ($action == 'login'): ?>
<form method="POST">
<div class="form-group"><label>📞 Телефон</label><input type="tel" name="phone" placeholder="79260000000" required></div>
<div class="form-group"><label>🔒 Пароль</label><input type="password" name="password" required></div>
<button type="submit" name="login" class="btnn">Войти</button>
<div class="link">Нет аккаунта? <a href="?action=register">Регистрация</a></div>
</form>
<?php else: ?>
<form method="POST">
<div class="form-group"><label>📞 Телефон</label><input type="tel" name="phone" placeholder="79260000000" required></div>
<div class="form-group"><label>👤 Имя</label><input type="text" name="full_name" placeholder="Иван Иванов"></div>
<div class="form-group"><label>🔒 Пароль</label><input type="password" name="password" required></div>
<div class="form-group"><label>🔒 Подтверждение</label><input type="password" name="confirm_password" required></div>
<div class="form-group">
<?php
$num1 = rand(1, 20);
$num2 = rand(1, 20);
$captcha_result = $num1 + $num2;
?>
<label>🧩 Решите пример: <?php echo $num1; ?> + <?php echo $num2; ?> = ?</label>
<input type="text" name="captcha" placeholder="Введите число" required autocomplete="off">
<input type="hidden" name="captcha_expected" value="<?php echo $captcha_result; ?>">
</div>
<button type="submit" name="register" class="btnn">Зарегистрироваться</button>
<div class="link">Уже есть аккаунт? <a href="?action=login">Войти</a></div>
</form>
<?php endif; ?>
<hr>
</div>
</div>
<?php else: ?>
<!-- ОСНОВНАЯ СТРАНИЦА -->
<div class="card">
<div class="card-header">
<h1>📅 Моё расписание</h1>
<p>Добро пожаловать, <?php echo htmlspecialchars($_SESSION['user_name'] ?: substr($_SESSION['user_phone'], -4)); ?>!</p>
</div>
<div class="card-body">
<div class="user-info">
<span>📞 Телефон: <?php echo substr(htmlspecialchars($userPhone), 0, 4) . '***' . substr(htmlspecialchars($userPhone), -4); ?></span>
<div style="display:flex; gap:8px;">
<?php if ($_SESSION['is_admin'] ?? 0): ?>
<a href="?admin_panel=1" class="btnn btn-sm" style="background:#e53e3e;">🔧 Админка</a>
<?php endif; ?>
<a href="?logout" class="btnn btn-sm btn-secondary">🚪 Выйти</a>
</div>
</div>
<form method="GET">
<div class="form-group">
<label>📅 Выберите дату:</label>
<input type="date" name="date" value="<?php echo date('Y-m-d', strtotime($date)); ?>" required>
</div>
<button type="submit" name="show_schedule" class="btnn">Показать расписание</button>
</form>
<?php if ($submitted && !$error_api && empty($schedule) && !$showDiagnostic): ?>
<div class="alert alert-info">📭 Нет записей на выбранную дату</div>
<?php elseif ($error_api): ?>
<div class="alert alert-error">⚠️ <?php echo htmlspecialchars($error_api); ?></div>
<?php elseif ($showDiagnostic && !empty($diagnosticData)): ?>
<div class="diagnostic-box">
<h3 style="margin-bottom: 16px;">📋 История по диагностике</h3>
<div style="display: flex; flex-direction: column; gap: 20px;">
<?php foreach ($diagnosticData as $entry): ?>
<div style="border-left: 3px solid #612f8c; padding-left: 16px;">
<div style="font-weight: 600; color: #612f8c;">
📅 <?php echo htmlspecialchars($entry['date']); ?>
</div>
<div class="diagnostic-text" style="margin-top: 0; white-space: pre-wrap; line-height: 1.3;">
<?php
// Чистим текст от лишних переносов
$cleanText = preg_replace("/\n{3,}/", "\n\n", $entry['text']); // 3 и более переносов -> 2
$cleanText = preg_replace("/<br\s*\/?>\s*<br\s*\/?>/i", "<br>", nl2br(htmlspecialchars($cleanText))); // убираем двойные <br>
echo $cleanText;
?>
</div>
</div>
<?php if (next($diagnosticData)): ?>
<hr style="margin: 5px 0;">
<?php endif; ?>
<?php endforeach; ?>
</div>
<div style="margin-top: 20px;">
<a href="?show_schedule=1&date=<?php echo $date; ?>" class="btnn btn-sm" style="width:auto;">← Вернуться к расписанию</a>
</div>
</div>
<?php elseif (!empty($schedule)): ?>
<?php
// Собираем уникальные центры для фильтра
$centers = [];
foreach ($schedule as $item) {
$center = trim($item['Центр'] ?? '');
if (!empty($center) && !in_array($center, $centers)) {
$centers[] = $center;
}
}
sort($centers); // Сортируем по алфавиту
?>
<div class="alert alert-info" style="margin-top:20px;">
📋 Найдено: <span id="totalCount"><?php echo count($schedule); ?></span> записей на <?php echo date('d.m.Y', strtotime($date)); ?>
</div>
<!-- Фильтр по центрам -->
<?php if (!empty($centers)): ?>
<div class="filter-section">
<div class="filter-title">📍 Фильтр по центру:</div>
<div class="filter-buttons">
<button class="filter-btn active" data-center="all" onclick="filterByCenter('all')">
📌 Все <span class="filter-count">(<?php echo count($schedule); ?>)</span>
</button>
<?php foreach ($centers as $center):
// Считаем количество записей в этом центре
$centerCount = 0;
foreach ($schedule as $item) {
if (trim($item['Центр'] ?? '') === $center) $centerCount++;
}
?>
<button class="filter-btn" data-center="<?php echo htmlspecialchars($center); ?>" onclick="filterByCenter('<?php echo htmlspecialchars($center); ?>')">
📍 <?php echo htmlspecialchars($center); ?> <span class="filter-count">(<?php echo $centerCount; ?>)</span>
</button>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="schedule-list">
<?php foreach ($schedule as $item):
$birthDate = $item['ДатаРождения'] ?? '';
$age = ($item['Услуга'] == 'Перерыв') ? '' : calculateAge($birthDate, $date);
$isBreak = ($item['Услуга'] ?? '') == 'Перерыв';
$dCode = $item['Диагностика'] ?? null;
$startTime = !empty($item['НачалоПриема']) ? date('H:i', strtotime($item['НачалоПриема'])) : '';
$endTime = !empty($item['ОкончаниеПриема']) ? date('H:i', strtotime($item['ОкончаниеПриема'])) : '';
$center = trim($item['Центр'] ?? '');
// Определяем цвет для центра
$centerColor = '';
if (stripos($center, 'тропарево') !== false) {
$centerColor = '#9b59b6';
} elseif (stripos($center, 'ясенево') !== false) {
$centerColor = '#2ecc71';
} elseif (stripos($center, 'коньково') !== false) {
$centerColor = '#e74c3c';
} else {
$centerColor = '#3498db';
}
?>
<div class="schedule-card-item <?php echo $isBreak ? 'break' : ''; ?>" data-center="<?php echo htmlspecialchars($center); ?>">
<div class="schedule-time <?php echo $isBreak ? 'break-time' : ''; ?>">
🕐 <?php echo $startTime; ?> - <?php echo $endTime; ?>
</div>
<?php if ($center): ?>
<div class="schedule-center" style="margin-bottom: 8px;">
<span style="display: inline-block; background: <?php echo $centerColor; ?>20; color: <?php echo $centerColor; ?>; padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 600;">
📍 <?php echo htmlspecialchars($center); ?>
</span>
</div>
<?php endif; ?>
<div class="schedule-patient">
👤 <?php echo htmlspecialchars($item['Пациент'] ?? '—'); ?>
<?php if ($age): ?>
<span style="font-size: 0.65rem; background: #e74c3c20; color: #e74c3c; padding: 2px 8px; border-radius: 20px; margin-left: 8px;">
🎂 <?php echo $age; ?>
</span>
<?php endif; ?>
</div>
<div class="schedule-service">
📋 <?php echo htmlspecialchars($item['Услуга'] ?? '—'); ?>
</div>
<?php if ($birthDate && !$age && !$isBreak && $birthDate != '0000-00-00' && $birthDate != '01.01.0001 0:00:00' && strpos($birthDate, '0001') === false): ?>
<div class="schedule-birth" style="font-size: 0.65rem; color: #718096;">
🎂 ДР: <?php echo htmlspecialchars($birthDate); ?>
</div>
<?php endif; ?>
<div class="schedule-diagnostic">
<?php if ($dCode): ?>
<a href="?show_schedule=1&date=<?php echo $date; ?>&diagnostic=<?php echo $dCode; ?>" class="diagnostic-link">
🔬 Диагностика
</a>
<?php else: ?>
<span style="font-size:0.75rem; color:#718096;">
📝 <?php echo htmlspecialchars($item['Нарушение'] ?? '—'); ?>
</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php elseif (!$submitted): ?>
<div class="alert alert-info">🗓️ Выберите дату и нажмите "Показать расписание"</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<script>
document.querySelectorAll('input[type="tel"]').forEach(input => {
input.addEventListener('input', function(e) {
this.value = this.value.replace(/[^0-9]/g, '').slice(0, 11);
});
});
function editUser(id, phone, name) {
document.getElementById('edit_user_id').value = id;
document.getElementById('edit_phone').value = phone;
document.getElementById('edit_full_name').value = name;
document.getElementById('editModal').style.display = 'flex';
}
function closeModal() {
document.getElementById('editModal').style.display = 'none';
}
function resetPassword(id) {
if(confirm('Сбросить пароль на 123456?')) {
document.getElementById('reset_user_id').value = id;
document.getElementById('resetForm').submit();
}
}
function deleteUser(id) {
if(confirm('Удалить пользователя?')) {
document.getElementById('delete_user_id').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<script>
// Фильтр по центрам
let currentCenterFilter = 'all';
function filterByCenter(center) {
currentCenterFilter = center;
// Обновляем активные кнопки
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.center === center) {
btn.classList.add('active');
}
});
// Фильтруем карточки
const cards = document.querySelectorAll('.schedule-card-item');
let visibleCount = 0;
cards.forEach(card => {
const cardCenter = card.dataset.center || '';
if (center === 'all' || cardCenter === center) {
card.style.display = 'block';
visibleCount++;
} else {
card.style.display = 'none';
}
});
// Показываем сообщение если ничего не найдено
let noResultsMsg = document.querySelector('.no-results-message');
if (!noResultsMsg && visibleCount === 0) {
const scheduleList = document.querySelector('.schedule-list');
const msg = document.createElement('div');
msg.className = 'no-results no-results-message';
msg.innerHTML = '😕 Нет записей в выбранном центре';
scheduleList.appendChild(msg);
} else if (noResultsMsg && visibleCount > 0) {
noResultsMsg.remove();
} else if (noResultsMsg && visibleCount === 0) {
// уже есть
}
}
</script>
</body>
</html>
админка
<?php
session_start();
// Подавляем вывод ошибок в буфер
ob_start();
// Подключаем конфиг
$configPath = __DIR__ . '/../../../papkakonfiga/db_config.php';
if (!file_exists($configPath)) {
die("Ошибка конфигурации");
}
$config = require $configPath;
// Проверяем пароль
$error = '';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$login = $_POST['login'] ?? '';
$password = $_POST['password'] ?? '';
// Хеш админ-пароля из конфига
$adminPasswordHash = password_hash($config['admin_default_password'] ?? '123456', PASSWORD_DEFAULT);
if ($login == 'admin' && password_verify($password, $adminPasswordHash)) {
$_SESSION['user_id'] = 1;
$_SESSION['user_phone'] = 'admin';
$_SESSION['user_name'] = 'Администратор';
$_SESSION['is_admin'] = 1;
// Очищаем буфер и делаем редирект
ob_end_clean();
header('Location: /raspisaniee/?admin_panel=1');
exit;
} else {
$error = "Неверные данные";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-вход</title>
<style>
body {
font-family: 'Inter', sans-serif;
background: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.admin-login-card {
background: white;
border-radius: 24px;
padding: 32px;
max-width: 400px;
width: 100%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.admin-login-card h1 {
color: #e53e3e;
margin-bottom: 24px;
text-align: center;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: #4a5568;
margin-bottom: 8px;
}
.form-group input {
width: 90%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 1rem;
}
.btn {
width: 100%;
padding: 12px;
background: #e53e3e;
color: white;
border: none;
border-radius: 40px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.error {
background: #fed7d7;
color: #c53030;
padding: 12px;
border-radius: 12px;
margin-bottom: 20px;
text-align: center;
}
</style>
</head>
<body>
<div class="admin-login-card">
<h1>🔐 Админ-панель</h1>
<?php if ($error): ?>
<div class="error"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label>Логин</label>
<input type="text" name="login" required autocomplete="off">
</div>
<div class="form-group">
<label>Пароль</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn">Войти</button>
</form>
</div>
</body>
</html>
Комментарии
Пока нет комментариев. Будьте первым!