Files
2026-03-05 10:25:39 +01:00

553 lines
23 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// admin/tools/new_clients.php
require_once __DIR__ . '/../../public/includes/init_admin.php';
// ID "super admin" (как в add_video.php)
const ADMIN_PRACTICIEN_ID = 88888888;
$me = (int)($_SESSION['reference_id'] ?? 0);
if ($me !== ADMIN_PRACTICIEN_ID) {
http_response_code(403);
echo '<div class="container py-5"><div class="alert alert-danger">'
. htmlspecialchars($translations['non_autorise'] ?? 'Доступ запрещён.', ENT_QUOTES, 'UTF-8')
. '</div></div>';
return;
}
$mustReauth=function(int $ttl=600){$t=(int)($_SESSION['admin_reauth_at']??0);return ($t===0||(time()-$t)>$ttl);};
if ($mustReauth()){ $qs=http_build_query(['page'=>'reauth','redir'=>($_SERVER['REQUEST_URI']??'index.php?page=maintenance')]); header('Location:?'.$qs); exit; }
// -- PDO si absent
// DB
// CSRF
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$CSRF = $_SESSION['csrf_token'];
// helpers
function e($s) {
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}
// ---- Параметры фильтрации ----
$q = trim($_GET['q'] ?? '');
$role = $_GET['role'] ?? 'all'; // all|client|praticien|secretaire
$period = $_GET['period'] ?? 'today'; // today|week|month|30days|custom
$dateFrom = trim($_GET['date_from'] ?? '');
$dateTo = trim($_GET['date_to'] ?? '');
// ❗ Pagination: utiliser "p" pour НЕ конфликтовать с ?page=new_inscrits (router)
$pageNum = max(1, (int)($_GET['p'] ?? 1));
$perPage = 25;
$validRoles = ['client', 'praticien', 'secretaire'];
// Период: если указаны даты "С/По", считаем custom
if ($dateFrom !== '' || $dateTo !== '') {
$period = 'custom';
}
// Диапазон дат по cree_le
$now = new DateTimeImmutable('now');
switch ($period) {
case 'today':
default:
$start = (new DateTimeImmutable('today'))->setTime(0,0,0);
$end = $start->modify('+1 day');
break;
case 'week':
$start = (new DateTimeImmutable('monday this week'))->setTime(0,0,0);
$end = $start->modify('+1 week');
break;
case 'month':
$start = (new DateTimeImmutable('first day of this month'))->setTime(0,0,0);
$end = $start->modify('first day of next month');
break;
case '30days':
$end = $now->setTime(23,59,59);
$start = $end->modify('-30 days')->setTime(0,0,0);
break;
case 'custom':
if ($dateFrom !== '') {
$tmp = DateTimeImmutable::createFromFormat('Y-m-d', $dateFrom) ?: new DateTimeImmutable('today');
$start = $tmp->setTime(0,0,0);
} else {
$start = (new DateTimeImmutable('2000-01-01'))->setTime(0,0,0);
}
if ($dateTo !== '') {
$tmp = DateTimeImmutable::createFromFormat('Y-m-d', $dateTo) ?: $now;
$end = $tmp->modify('+1 day')->setTime(0,0,0);
} else {
$end = $now->modify('+1 day')->setTime(0,0,0);
}
break;
}
$dtFrom = $start->format('Y-m-d H:i:s');
$dtTo = $end->format('Y-m-d H:i:s');
// ---- Сортировка ----
$allowedSort = [
'nom' => 'nom',
'prenom' => 'prenom',
'email' => 'email',
'telephone' => 'telephone',
'role' => 'role',
'cree_le' => 'cree_le',
];
$sort = $_GET['sort'] ?? 'cree_le';
$dir = strtolower($_GET['dir'] ?? 'desc');
if (!isset($allowedSort[$sort])) {
$sort = 'cree_le';
}
if (!in_array($dir, ['asc','desc'], true)) {
$dir = 'desc';
}
$orderBySql = $allowedSort[$sort] . ' ' . strtoupper($dir);
// ---- WHERE + параметры ----
$where = [];
$params = [];
// только client, praticien, secretaire
$where[] = "c.role IN ('client','praticien','secretaire')";
if (in_array($role, $validRoles, true)) {
$where[] = "c.role = :role";
$params[':role'] = $role;
}
// поиск по строке
if ($q !== '') {
$where[] = "(
COALESCE(p.nom, cl.nom, '') LIKE :q_nom
OR COALESCE(p.prenom, cl.prenom, '') LIKE :q_prenom
OR c.email LIKE :q_email
OR COALESCE(p.telephone, cl.telephone, '') LIKE :q_tel
)";
$like = '%' . $q . '%';
$params[':q_nom'] = $like;
$params[':q_prenom'] = $like;
$params[':q_email'] = $like;
$params[':q_tel'] = $like;
// 👉 Опция: когда ищем по строке, НЕ ограничиваем по дате
} else {
// фильтр по периоду только если нет q
$where[] = "c.cree_le >= :from AND c.cree_le < :to";
$params[':from'] = $dtFrom;
$params[':to'] = $dtTo;
}
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
// ---- Подсчет общего количества ----
$sqlCount = "
SELECT COUNT(*) AS cnt
FROM compte c
LEFT JOIN praticien p ON (c.role IN ('praticien','secretaire') AND c.reference_id = p.id)
LEFT JOIN client cl ON (c.role = 'client' AND c.reference_id = cl.id)
$whereSql
";
try {
$st = $pdo->prepare($sqlCount);
foreach ($params as $k=>$v) {
$st->bindValue($k, $v);
}
$st->execute();
$totalRows = (int)$st->fetchColumn();
} catch (Throwable $e) {
echo '<div class="alert alert-danger">'.e($translations['admin_new_clients_sql_err_count'] ?? 'Ошибка SQL (count): ').e($e->getMessage()).'</div>';
return;
}
$totalPages = max(1, (int)ceil($totalRows / $perPage));
if ($pageNum > $totalPages) {
$pageNum = $totalPages;
}
$offset = ($pageNum - 1) * $perPage;
// ---- Основной SELECT ----
$sql = "
SELECT
c.id AS compte_id,
c.email,
c.role,
c.cree_le,
c.email_valide,
c.token_validation,
COALESCE(p.nom, cl.nom) AS nom,
COALESCE(p.prenom, cl.prenom) AS prenom,
COALESCE(p.telephone, cl.telephone) AS telephone,
CASE
WHEN c.role='praticien' THEN p.accepted_cgu
WHEN c.role='client' THEN cl.accepted_cgu
ELSE NULL
END AS accepted_cgu
FROM compte c
LEFT JOIN praticien p ON (c.role IN ('praticien','secretaire') AND c.reference_id = p.id)
LEFT JOIN client cl ON (c.role = 'client' AND c.reference_id = cl.id)
$whereSql
ORDER BY $orderBySql
LIMIT :limit OFFSET :offset
";
try {
$st = $pdo->prepare($sql);
foreach ($params as $k=>$v) {
$st->bindValue($k, $v);
}
$st->bindValue(':limit', (int)$perPage, PDO::PARAM_INT);
$st->bindValue(':offset', (int)$offset, PDO::PARAM_INT);
$st->execute();
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
echo '<div class="alert alert-danger">'.e($translations['admin_new_clients_sql_err_select'] ?? 'Ошибка SQL (select): ').e($e->getMessage()).'</div>';
return;
}
// параметры для ссылок сортировки/пагинации (без sort/dir/p)
$baseParams = [
'q' => $q,
'role' => $role,
'period' => $period,
'date_from' => $dateFrom,
'date_to' => $dateTo,
];
// helper для построения URL сортировки
function sortLink($field, $label, $currentSort, $currentDir, $extraParams) {
$params = $extraParams;
$params['sort'] = $field;
if ($currentSort === $field) {
$params['dir'] = ($currentDir === 'asc') ? 'desc' : 'asc';
} else {
$params['dir'] = 'asc';
}
$url = admin_url('new_inscrits', $params);
$arrow = '';
if ($currentSort === $field) {
$arrow = $currentDir === 'asc' ? ' ▲' : ' ▼';
}
return '<a href="'.e($url).'">'.e($label.$arrow).'</a>';
}
?>
<div class="container-fluid py-4">
<div class="shadow p-4 rounded bg-light">
<h2 class="mb-3"><?= e($translations['admin_new_clients_title'] ?? 'Новые регистрации') ?></h2>
<form class="row g-3 mb-3" method="get" action="index.php">
<input type="hidden" name="page" value="new_inscrits">
<input type="hidden" name="period" value="<?= e($period) ?>">
<div class="col-md-3">
<label class="form-label"><?= e($translations['admin_new_clients_search_label'] ?? 'Поиск') ?></label>
<input type="text" name="q" class="form-control"
placeholder="<?= e($translations['admin_new_clients_search_ph'] ?? 'Имя, фамилия, e-mail, телефон…') ?>"
value="<?= e($q) ?>">
</div>
<div class="col-md-2">
<label class="form-label"><?= e($translations['admin_new_clients_role_label'] ?? 'Роль') ?></label>
<select name="role" class="form-select">
<option value="all" <?= $role==='all' ? 'selected' : '' ?>><?= e($translations['admin_new_clients_role_all'] ?? 'Все') ?></option>
<option value="client" <?= $role==='client' ? 'selected' : '' ?>><?= e($translations['admin_new_clients_role_clients'] ?? 'Клиенты') ?></option>
<option value="praticien" <?= $role==='praticien' ? 'selected' : '' ?>><?= e($translations['admin_new_clients_role_praticiens'] ?? 'Специалисты') ?></option>
<option value="secretaire" <?= $role==='secretaire' ? 'selected' : '' ?>><?= e($translations['admin_new_clients_role_secretaires'] ?? 'Секретари') ?></option>
</select>
</div>
<br><br>
<div class="col-md-3">
<label class="form-label d-block"><?= e($translations['admin_new_clients_period_label'] ?? 'Период') ?></label>
<div class="btn-group" role="group">
<?php
$periods = [
'today' => ($translations['admin_new_clients_period_today'] ?? 'Сегодня'),
'week' => ($translations['admin_new_clients_period_week'] ?? 'Неделя'),
'month' => ($translations['admin_new_clients_period_month'] ?? 'Месяц'),
'30days' => ($translations['admin_new_clients_period_30days'] ?? '30 дней'),
];
foreach ($periods as $key=>$lbl):
$btnParams = $baseParams;
$btnParams['period'] = $key;
$btnParams['p'] = 1;
$url = admin_url('new_inscrits', $btnParams);
?>
<a href="<?= e($url) ?>"
class="btn btn-sm <?= $period === $key ? 'btn-danger' : 'btn-outline-danger' ?>">
<?= e($lbl) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<div class="col-md-2">
<label class="form-label"><?= e($translations['admin_new_clients_date_from'] ?? 'С даты') ?></label>
<input type="date" name="date_from" class="form-control"
value="<?= e($dateFrom) ?>">
</div>
<div class="col-md-2">
<label class="form-label"><?= e($translations['admin_new_clients_date_to'] ?? 'По дату') ?></label>
<input type="date" name="date_to" class="form-control"
value="<?= e($dateTo) ?>">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-danger me-2" style="border-radius:999px;" type="submit">
<?= e($translations['admin_new_clients_apply'] ?? 'Применить') ?>
</button>
<?php
$resetUrl = admin_url('new_inscrits', [
'period' => 'today',
'role' => 'all',
'p' => 1,
]);
?>
<a href="<?= e($resetUrl) ?>" class="btn btn-outline-secondary" style="border-radius:999px;">
<?= e($translations['admin_new_clients_reset'] ?? 'Сброс') ?>
</a>
</div>
</form>
<div class="mb-2 text-muted">
<?= e($translations['admin_new_clients_found'] ?? 'Найдено записей:') ?>
<strong><?= (int)$totalRows ?></strong>.
<?= e($translations['admin_new_clients_page'] ?? 'Страница') ?>
<?= (int)$pageNum ?>
<?= e($translations['admin_new_clients_page_of'] ?? 'из') ?>
<?= (int)$totalPages ?>.
</div>
<style>
table.table thead th a {
color: #38378B;
font-weight: 600;
text-decoration: none;
}
table.table td a {
color: #38378B;
font-weight: 600;
text-decoration: none;
}
table.table thead th a:hover {
color: #E03C3F;
}
table.table td a:hover {
color: #E03C3F;
}
</style>
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle">
<thead class="table-light">
<tr>
<th><?= sortLink('nom', ($translations['admin_new_clients_th_nom'] ?? 'Фамилия'), $sort, $dir, $baseParams + ['p'=>$pageNum]) ?></th>
<th><?= sortLink('prenom', ($translations['admin_new_clients_th_prenom'] ?? 'Имя'), $sort, $dir, $baseParams + ['p'=>$pageNum]) ?></th>
<th><?= sortLink('email', ($translations['admin_new_clients_th_email'] ?? 'E-mail'), $sort, $dir, $baseParams + ['p'=>$pageNum]) ?></th>
<th><?= sortLink('telephone', ($translations['admin_new_clients_th_telephone'] ?? 'Телефон'), $sort, $dir, $baseParams + ['p'=>$pageNum]) ?></th>
<th><?= sortLink('role', ($translations['admin_new_clients_th_role'] ?? 'Роль'), $sort, $dir, $baseParams + ['p'=>$pageNum]) ?></th>
<th><?= sortLink('cree_le', ($translations['admin_new_clients_th_cree_le'] ?? 'Дата регистрации'), $sort, $dir, $baseParams + ['p'=>$pageNum]) ?></th>
<th><?= e($translations['admin_new_clients_th_email_status'] ?? 'E-mail') ?></th>
<th><?= e($translations['admin_new_clients_th_cgu'] ?? 'CGU') ?></th>
<th style="width:120px;"><?= e($translations['admin_new_clients_th_actions'] ?? 'Действия') ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr>
<td colspan="9" class="text-center text-muted py-4">
<?= e($translations['admin_new_clients_empty'] ?? 'Нет регистраций за выбранный период.') ?>
</td>
</tr>
<?php else: ?>
<?php foreach ($rows as $r): ?>
<?php
if ($r['role'] === 'client') $roleLabel = ($translations['admin_new_clients_role_client_one'] ?? 'Клиент');
elseif ($r['role'] === 'praticien')$roleLabel = ($translations['admin_new_clients_role_praticien_one'] ?? 'Специалист');
elseif ($r['role'] === 'secretaire') $roleLabel = ($translations['admin_new_clients_role_secretaire_one'] ?? 'Секретарь');
else $roleLabel = $r['role'];
$creeLe = $r['cree_le'] ? (new DateTime($r['cree_le']))->format('d.m.Y H:i') : '';
?>
<tr>
<td><?= e($r['nom']) ?></td>
<td><?= e($r['prenom']) ?></td>
<td><a href="mailto:<?= e($r['email']) ?>"><?= e($r['email']) ?></a></td>
<td><?= e($r['telephone']) ?></td>
<td><?= e($roleLabel) ?></td>
<td><?= e($creeLe) ?></td>
<td>
<?php if ((int)$r['email_valide'] === 1): ?>
<button class="btn btn-sm btn-outline-success" disabled><?= e($translations['admin_new_clients_email_ok'] ?? 'Подтверждён') ?></button>
<?php else: ?>
<button class="btn btn-sm btn-outline-danger" disabled><?= e($translations['admin_new_clients_email_ko'] ?? 'KO') ?></button>
<?php endif; ?>
</td>
<td>
<?php if ($r['accepted_cgu'] === null): ?>
<button class="btn btn-sm btn-outline-secondary" disabled><?= e($translations['admin_new_clients_dash'] ?? '—') ?></button>
<?php elseif ((int)$r['accepted_cgu'] === 1): ?>
<button class="btn btn-sm btn-outline-success" disabled><?= e($translations['admin_new_clients_cgu_yes'] ?? 'Приняты') ?></button>
<?php else: ?>
<button class="btn btn-sm btn-outline-danger" disabled><?= e($translations['admin_new_clients_cgu_no'] ?? 'Не приняты') ?></button>
<?php endif; ?>
</td>
<td>
<div class="d-flex flex-wrap gap-1">
<button type="button"
class="btn btn-sm btn-outline-danger js-show-details"
data-id="<?= (int)$r['compte_id'] ?>">
<?= e($translations['admin_new_clients_details'] ?? 'Подробно') ?>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<nav aria-label="<?= e($translations['admin_new_clients_pages_aria'] ?? 'Страницы') ?>">
<ul class="pagination justify-content-center mt-3">
<?php
$prevPage = max(1, $pageNum - 1);
$nextPage = min($totalPages, $pageNum + 1);
$prevParams = $baseParams + ['p'=>$prevPage, 'sort'=>$sort, 'dir'=>$dir];
$nextParams = $baseParams + ['p'=>$nextPage, 'sort'=>$sort, 'dir'=>$dir];
$prevUrl = admin_url('new_inscrits', $prevParams);
$nextUrl = admin_url('new_inscrits', $nextParams);
?>
<li class="page-item <?= $pageNum <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="<?= e($prevUrl) ?>"><?= e($translations['admin_new_clients_prev'] ?? '« Предыдущая') ?></a>
</li>
<?php
$startPage = max(1, $pageNum - 2);
$endPage = min($totalPages, $pageNum + 2);
for ($i = $startPage; $i <= $endPage; $i++):
$pParams = $baseParams + ['p'=>$i, 'sort'=>$sort, 'dir'=>$dir];
$pUrl = admin_url('new_inscrits', $pParams);
?>
<li class="page-item <?= $i === $pageNum ? 'active' : '' ?>">
<a class="page-link" href="<?= e($pUrl) ?>"><?= (int)$i ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $pageNum >= $totalPages ? 'disabled' : '' ?>">
<a class="page-link" href="<?= e($nextUrl) ?>"><?= e($translations['admin_new_clients_next'] ?? 'Следующая »') ?></a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
</div>
<script>
const NEWCLIENTS_CSRF = <?= json_encode($CSRF, JSON_UNESCAPED_UNICODE) ?>;
function postForm(url, data) {
const body = new URLSearchParams(data);
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString(),
credentials: 'same-origin'
}).then(r => r.json());
}
document.addEventListener('click', function(e) {
const btnSend = e.target.closest('.js-send-validation');
if (btnSend) {
const id = btnSend.dataset.id;
if (!id) return;
if (!confirm(<?= json_encode($translations['admin_new_clients_confirm_send_link'] ?? 'Отправить ссылку для подтверждения регистрации на этот e-mail?', JSON_UNESCAPED_UNICODE) ?>)) return;
postForm('tools/ajax_send_validation_link.php', {
csrf: NEWCLIENTS_CSRF,
id: id
}).then(data => {
if (data && data.ok) {
if (typeof Swal !== 'undefined') {
Swal.fire({ icon: 'success', title: <?= json_encode($translations['admin_new_clients_done'] ?? 'Готово', JSON_UNESCAPED_UNICODE) ?>, text: data.message || <?= json_encode($translations['admin_new_clients_link_sent'] ?? 'Ссылка отправлена.', JSON_UNESCAPED_UNICODE) ?> });
} else {
alert(data.message || <?= json_encode($translations['admin_new_clients_link_sent'] ?? 'Ссылка отправлена.', JSON_UNESCAPED_UNICODE) ?>);
}
} else {
const msg = (data && data.error) ? data.error : <?= json_encode($translations['admin_new_clients_err_send_link'] ?? 'Ошибка при отправке ссылки.', JSON_UNESCAPED_UNICODE) ?>;
if (typeof Swal !== 'undefined') {
Swal.fire({ icon: 'error', title: <?= json_encode($translations['admin_new_clients_error'] ?? 'Ошибка', JSON_UNESCAPED_UNICODE) ?>, text: msg });
} else {
alert(msg);
}
}
}).catch(err => {
console.error(err);
if (typeof Swal !== 'undefined') {
Swal.fire({ icon: 'error', title: <?= json_encode($translations['admin_new_clients_error'] ?? 'Ошибка', JSON_UNESCAPED_UNICODE) ?>, text: <?= json_encode($translations['admin_new_clients_err_request'] ?? 'Произошла ошибка запроса.', JSON_UNESCAPED_UNICODE) ?> });
} else {
alert(<?= json_encode($translations['admin_new_clients_err_request'] ?? 'Произошла ошибка запроса.', JSON_UNESCAPED_UNICODE) ?>);
}
});
return;
}
const btnDetails = e.target.closest('.js-show-details');
if (btnDetails) {
const id = btnDetails.dataset.id;
if (!id) return;
postForm('tools/ajax_fiche_user.php', {
csrf: NEWCLIENTS_CSRF,
id: id
}).then(data => {
if (data && data.ok && data.html) {
if (typeof Swal !== 'undefined') {
Swal.fire({
title: <?= json_encode($translations['admin_new_clients_user_card_title'] ?? 'Карточка пользователя', JSON_UNESCAPED_UNICODE) ?>,
html: data.html,
width: '600px',
confirmButtonText: <?= json_encode($translations['admin_new_clients_close'] ?? 'Закрыть', JSON_UNESCAPED_UNICODE) ?>
});
} else {
const w = window.open('', '_blank', 'width=600,height=600');
if (w) { w.document.write(data.html); w.document.close(); }
else { alert(<?= json_encode($translations['admin_new_clients_err_popup'] ?? 'Не удалось открыть окно с подробностями.', JSON_UNESCAPED_UNICODE) ?>); }
}
} else {
const msg = (data && data.error) ? data.error : <?= json_encode($translations['admin_new_clients_err_load'] ?? 'Не удалось загрузить данные.', JSON_UNESCAPED_UNICODE) ?>;
if (typeof Swal !== 'undefined') {
Swal.fire({ icon: 'error', title: <?= json_encode($translations['admin_new_clients_error'] ?? 'Ошибка', JSON_UNESCAPED_UNICODE) ?>, text: msg });
} else {
alert(msg);
}
}
}).catch(err => {
console.error(err);
if (typeof Swal !== 'undefined') {
Swal.fire({ icon: 'error', title: <?= json_encode($translations['admin_new_clients_error'] ?? 'Ошибка', JSON_UNESCAPED_UNICODE) ?>, text: <?= json_encode($translations['admin_new_clients_err_request'] ?? 'Произошла ошибка запроса.', JSON_UNESCAPED_UNICODE) ?> });
} else {
alert(<?= json_encode($translations['admin_new_clients_err_request'] ?? 'Произошла ошибка запроса.', JSON_UNESCAPED_UNICODE) ?>);
}
});
}
});
</script>