Add files via upload
Этот коммит содержится в:
@@ -0,0 +1,27 @@
|
||||
# Структура проекта
|
||||
|
||||
index.php - Главная страница со ссылками на адмнику и базы данных
|
||||
register.php - Форма регистрации для новых пользователей
|
||||
admin.php - Админка, для управления пользователями
|
||||
db.json,dmrid.dat - Базы данных
|
||||
savedata - Папка, в которую можно сохранять старое в случае изменений
|
||||
|
||||
## Админка
|
||||
admin.php
|
||||
Основной файл, html со вставками php, логика и стили импортируются из других файлов.
|
||||
Тут можно редактировать основную структуру страницы
|
||||
|
||||
adminLogic.php
|
||||
Файл, с кодом для основной логики, обрабатывает удаление, добавление, блокировку
|
||||
пользователей, вызванные из admin.php
|
||||
Импортируются в admin.php в первой строчке проекта
|
||||
Методы - sync, delete, block, confirm, post
|
||||
Переменные - данные о пользователях, данные о пользователях с пагинацией
|
||||
|
||||
/assets/...
|
||||
Папка со стилем и js-кодом (для валидации имени в форме создания).
|
||||
Импортируется в админку в теге <header> и перед последним тегом </body>
|
||||
|
||||
## Структура данных
|
||||
db.json - {"count": 4, "results":[{},{}]}
|
||||
dmrid.dat - ID;CALLSIGN;
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
require __DIR__ . '/adminLogic.php';
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Админка DMR</title>
|
||||
<link rel="stylesheet" href="/reg/assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>Панель администратора DMR ID
|
||||
<a href="https://xlx.dmrykt.ru/reg/" style="display:inline-block; float: right; color: white;">НАЗАД</a>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<!--Поле поиска и синхронизации над таблицей.-->
|
||||
<div class="left-section">
|
||||
<h2>Кореcпонденты:</h2>
|
||||
<div class="card-space">
|
||||
<form method="get" class="search-form">
|
||||
<input type="text" name="search" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Поиск...">
|
||||
<button type="submit">Найти</button>
|
||||
</form>
|
||||
<form method="get" class="search-form" style="display:inline-block; float: right;" onsubmit="return confirm('Синхронизировать список с JSON?')">
|
||||
<input type="hidden" name="sync" value="1">
|
||||
<button type="submit">Синхронизировать</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!--Таблица, в теле таблицы обработка каждой строки-->
|
||||
<table class="user-table">
|
||||
<thead>
|
||||
<tr data-id="<?= $row['id'] ?>">
|
||||
<th><?= sortLink('id', 'ID') ?></th>
|
||||
<th><?= sortLink('callsign', 'Позывной') ?></th>
|
||||
<th><?= sortLink('fname', 'Имя') ?></th>
|
||||
<th><?= sortLink('surname', 'Фамилия') ?></th>
|
||||
<th><?= sortLink('city', 'Город') ?></th>
|
||||
<th><?= sortLink('state', 'Регион') ?></th>
|
||||
<th><?= sortLink('country', 'Страна') ?></th>
|
||||
<th><?= sortLink('remarks', 'Телеграм') ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($currentPageData as $row): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($row['id']) ?></td>
|
||||
<td>
|
||||
<a href="https://dmrykt.ru/index.php?subaction=userinfo&user=<?= urlencode($row['callsign']) ?>"
|
||||
onmouseover="this.style.fontWeight='bold';"
|
||||
onmouseout="this.style.fontWeight='normal';">
|
||||
<?= htmlspecialchars($row['callsign']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($row['fname']) ?></td>
|
||||
<td><?= htmlspecialchars($row['surname']) ?></td>
|
||||
<td><?= htmlspecialchars($row['city']) ?></td>
|
||||
<td><?= htmlspecialchars($row['state']) ?></td>
|
||||
<td><?= htmlspecialchars($row['country']) ?></td>
|
||||
<td><a href="https://t.me/<?= htmlspecialchars($row['remarks']) ?>"
|
||||
onmouseover="this.style.fontWeight='bold';"
|
||||
onmouseout="this.style.fontWeight='normal';"><?= htmlspecialchars($row['remarks']) ?>
|
||||
|
||||
</a>
|
||||
</td>
|
||||
<!--Ячейка с кнопками, отправляет соотвествующие методы, частично обрабатывается через js-->
|
||||
<td class="button-group">
|
||||
<a href="#" class="edit-btn" title="Редактировать" >✏️</a>
|
||||
<a href="?delete=<?= $row['id'] ?>" title="Удалить" onclick="return confirm ('Вы действительно хотите удалить кореспондента?')">🗑️</a>
|
||||
<a href="?block=<?= $row['id'] ?>" title="Заблокировать" style="color=white" >✖️</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--Пагинация + отправка запроса на сервер. Проверяет количество страниц и в зависимости от выбранной редактирует переменную-->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?sort=<?= $sortKey ?>&order=<?= $sortOrder ?>&page=<?= $page - 1 ?>">« Назад</a>
|
||||
<?php endif; ?>
|
||||
<a href="?sort=<?= $sortKey ?>&order=<?= $sortOrder ?>&page=1" class="<?= $page == 1 ? 'current' : '' ?>">1</a>
|
||||
|
||||
<?php
|
||||
$start = max(2, $page - 1);
|
||||
$end = min($totalPages - 1, $page + 1);
|
||||
if ($start > 2) {
|
||||
echo '<span class="dots">...</span>';
|
||||
}
|
||||
for ($i = $start; $i <= $end; $i++): ?>
|
||||
<a href="?sort=<?= $sortKey ?>&order=<?= $sortOrder ?>&page=<?= $i ?>"
|
||||
class="<?= $i == $page ? 'current' : '' ?>"><?= $i ?></a>
|
||||
<?php endfor;
|
||||
if ($end < $totalPages - 1) {
|
||||
echo '<span class="dots">...</span>';
|
||||
}
|
||||
?>
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<a href="?sort=<?= $sortKey ?>&order=<?= $sortOrder ?>&page=<?= $totalPages ?>"
|
||||
class="<?= $page == $totalPages ? 'current' : '' ?>"><?= $totalPages ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if ($page < $totalPages): ?>
|
||||
<a href="?sort=<?= $sortKey ?>&order=<?= $sortOrder ?>&page=<?= $page + 1 ?>">Вперед »</a>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<!--Правая часть страницы с формой создания и подтверждения-->
|
||||
<div class="right-section">
|
||||
<h3>Добавить кореспондента</h3>
|
||||
<?php if (!empty($form_error)): ?>
|
||||
<div class="form-error"><?= htmlspecialchars($form_error) ?></div>
|
||||
<?php endif; ?>
|
||||
<!--Форма создания, по имени отправляет метод на сервер, выше отображает ошибку, если есть-->
|
||||
<form method="post" class="add-form">
|
||||
<input type="hidden" name="add" value="1">
|
||||
<input type="number" name="new_id" placeholder="DMR ID" required>
|
||||
<div class="form-error" id="id-error"></div>
|
||||
|
||||
<input oninput="this.value = this.value.toUpperCase()" type="text" name="new_callsign" placeholder="Позывной" required>
|
||||
<div class="form-error" id="callsign-error"></div>
|
||||
|
||||
<input type="text" name="fname" placeholder="Имя (латиница)">
|
||||
<input type="text" name="surname" placeholder="Фамилия (латиница)">
|
||||
<input type="text" name="city" placeholder="Город (латиница)">
|
||||
<input type="text" name="state" placeholder="Регион (латиница)">
|
||||
<input type="text" name="country" placeholder="Страна (латиница)">
|
||||
|
||||
<input type="text" name="remarks" placeholder="Телеграм">
|
||||
|
||||
<div class="form-error" id="latin-error"></div>
|
||||
<button type="submit" id="submit-btn">Добавить</button>
|
||||
</form>
|
||||
<!--Заявки, ожидающие подтверждения-->
|
||||
<div id="unconfirmed-cards" style="margin-top: 30px;">
|
||||
<h4>Ожидают подтверждения</h4>
|
||||
<?php foreach ($data as $row): ?>
|
||||
<?php if (empty($row['confirmed'])): ?>
|
||||
<div class="unconfirmed-card">
|
||||
<span><?= htmlspecialchars($row['callsign']) ?> (<?= htmlspecialchars($row['id']) ?>) -
|
||||
<a href="https://t.me/<?= htmlspecialchars($row['remarks']) ?>"
|
||||
onmouseover="this.style.fontWeight='bold';"
|
||||
onmouseout="this.style.fontWeight='normal';"> Написать в ТГ
|
||||
|
||||
</a>
|
||||
</span>
|
||||
<div class="button-group">
|
||||
<a href="?confirm=<?= $row['id'] ?>" title="Активировать" onclick="return confirm('Активировать кореспондента?')">✔️</a>
|
||||
<a href="?delete=<?= $row['id'] ?>" title="Удалить" onclick="return confirm('Вы действительно хотите удалить кореспондента?')">🗑️</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/reg/assets/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
//Файл может импортироваться в другие страницы и все переменные и методы будут видны
|
||||
$file = __DIR__ . '/db.json';
|
||||
$datFile = __DIR__ . '/dmrid.dat';
|
||||
$json = json_decode(file_get_contents($file), true) ?? ['count' => 0, 'results' => []];
|
||||
$data = $json['results'];
|
||||
|
||||
//Запрос, который отправляется сервером на форму создания
|
||||
if (isset($_GET['ajax_check'])) {
|
||||
$id = $_GET['id'] ?? '';
|
||||
$callsign = strtoupper(trim($_GET['callsign'] ?? ''));
|
||||
$response = [
|
||||
'idInvalid' => !preg_match('/^\d{6,7}$/', $id),
|
||||
'idExists' => false,
|
||||
'callsignInvalid' => !preg_match('/^[A-Z0-9]{4,7}$/', $callsign),
|
||||
'callsignExists' => false
|
||||
];
|
||||
//Проверяет id и callsign на существование
|
||||
foreach ($data as $row) {
|
||||
if ($row['id'] == $id) $response['idExists'] = true;
|
||||
if (strtoupper($row['callsign']) === $callsign) $response['callsignExists'] = true;
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
//Метод синхронизации
|
||||
if (isset($_GET['sync'])) {
|
||||
$lines = [];
|
||||
|
||||
//Читает json - берет из него id, callsign в нужном формате и добавляет в строки
|
||||
foreach ($data as $item) {
|
||||
if (!empty($item['confirmed'])) {
|
||||
$id = trim($item['id']);
|
||||
$callsign = strtoupper(trim($item['callsign']));
|
||||
$lines[] = "{$id};{$callsign};";
|
||||
}
|
||||
}
|
||||
|
||||
//Перезаписывем файл
|
||||
file_put_contents($datFile, implode(PHP_EOL, $lines) . PHP_EOL);
|
||||
|
||||
//Совершаем перенаправление на страницу админки
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
//Редактирование
|
||||
if (isset($_GET['edit'])) {
|
||||
//Отправляем не в форме а в json запросе, поэтому сначала должны получить формат
|
||||
$rawData = file_get_contents("php://input");
|
||||
$postData = json_decode($rawData, true);
|
||||
|
||||
$edit_id = (int)$postData['id'];
|
||||
$callsign = strtoupper(trim($postData['callsign']));
|
||||
$fname = trim($postData['fname'] ?? '');
|
||||
$surname = trim($postData['surname'] ?? '');
|
||||
$city = trim($postData['city'] ?? '');
|
||||
$state = trim($postData['state'] ?? '');
|
||||
$country = trim($postData['country'] ?? '');
|
||||
$remarks = trim($postData['remarks'] ?? '');
|
||||
|
||||
//Ищем среди данных по айди то поле, которое редактировали
|
||||
foreach ($data as &$item) {
|
||||
if ($item['id'] == $edit_id) {
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
$item['callsign'] = $callsign;
|
||||
$item['fname'] = $fname;
|
||||
$item['surname'] = $surname;
|
||||
$item['city'] = $city;
|
||||
$item['state'] = $state;
|
||||
$item['country'] = $country;
|
||||
$item['remarks'] = $remarks;
|
||||
//$entry присвоена до редактирования - фильтруем и удаляем, после - добавляем (FILE_APPEND)
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$filtered = array_filter($lines, function($line) use ($entry) {
|
||||
return trim($line) !== $entry;
|
||||
});
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
file_put_contents($datFile, $entry . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
//Так очищается переменная, чтоб использовать ее дальше. После записываем все в json
|
||||
unset($item);
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, array_map(fn($r) => "{$r['id']};{$r['callsign']};", $data)) . PHP_EOL);
|
||||
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
//Подтверждение. GET - без формы и json, просто ищем запись по id
|
||||
if (isset($_GET['confirm'])) {
|
||||
$confirmId = (int)$_GET['confirm'];
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if ($item['id'] === $confirmId) {
|
||||
$item['confirmed'] = true;
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
//нет необходимости удалять - до подтверждения записи не было
|
||||
if (!in_array($entry, $lines)) {
|
||||
file_put_contents($datFile, $entry . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
//Блокировка. Аналогично одобрению, меняем на false
|
||||
if (isset($_GET['block'])) {
|
||||
$confirmId = (int)$_GET['block'];
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if ($item['id'] === $confirmId) {
|
||||
$item['confirmed'] = false;
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
|
||||
$filtered = array_filter($lines, function($line) use ($entry) {
|
||||
return trim($line) !== $entry;
|
||||
});
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
//Удаление
|
||||
if (isset($_GET['delete'])) {
|
||||
$deleteId = (int)$_GET['delete'];
|
||||
|
||||
$callsign = null;
|
||||
|
||||
foreach ($data as $index => $item) {
|
||||
if ($item['id'] === $deleteId) {
|
||||
//Получаем позывной для строки в фильтре и очищаем json от найденной переменной
|
||||
$callsign = $item['callsign'];
|
||||
unset($data[$index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$data = array_values($data);
|
||||
|
||||
if ($callsign === null) {
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
//Перезаписываем
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$filtered = array_filter($lines, function($line) use ($deleteId, $callsign) {
|
||||
return trim($line) !== "{$deleteId};{$callsign};";
|
||||
});
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
//Переменная - используется в форме добавления. Аналогично редактированию, на есть форма - не надо доставать json
|
||||
$form_error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
|
||||
$new_id = (int)$_POST['new_id'];
|
||||
$callsign = strtoupper(trim($_POST['new_callsign']));
|
||||
$fname = trim($_POST['fname'] ?? '');
|
||||
$surname = trim($_POST['surname'] ?? '');
|
||||
$city = trim($_POST['city'] ?? '');
|
||||
$state = trim($_POST['state'] ?? '');
|
||||
$country = trim($_POST['country'] ?? '');
|
||||
$remarks = trim($_POST['remarks'] ?? '');
|
||||
|
||||
if ($new_id < 100000 || $new_id > 9999999) {
|
||||
$form_error = 'ID должен быть 6–7 цифр.';
|
||||
} elseif (!preg_match('/^[A-Z0-9]{4,7}$/', $callsign)) {
|
||||
$form_error = 'Позывной: 4–7 латинских символов/цифр.';
|
||||
} elseif (array_filter($data, fn($r) => $r['id'] == $new_id || strtoupper($r['callsign']) === $callsign)) {
|
||||
$form_error = 'Такой ID или позывной уже существует.';
|
||||
} elseif (preg_match('/[^a-zA-Z\s]/', $fname . $surname . $city . $state . $country)) {
|
||||
$form_error = 'Дополнительные поля должны содержать только латиницу.';
|
||||
} else {
|
||||
$data[] = [
|
||||
'id' => $new_id,
|
||||
'callsign' => $callsign,
|
||||
'fname' => $fname,
|
||||
'surname' => $surname,
|
||||
'city' => $city,
|
||||
'state' => $state,
|
||||
'country' => $country,
|
||||
'remarks' => $remarks,
|
||||
'confirmed' => true
|
||||
];
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$dat_line = "{$new_id};{$callsign};" . PHP_EOL;
|
||||
file_put_contents($datFile, $dat_line, FILE_APPEND);
|
||||
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
//Дальше идут поля, которые используются для пагинации в таблице.
|
||||
//Сначала идет поиск и фильтрация на подтвержденных
|
||||
$searchQuery = trim($_GET['search'] ?? '');
|
||||
|
||||
$confirmed = array_values(array_filter($data, fn($d) => !empty($d['confirmed'])));
|
||||
|
||||
if ($searchQuery !== '') {
|
||||
$confirmed = array_filter($confirmed, function($row) use ($searchQuery) {
|
||||
foreach (['id', 'callsign', 'fname', 'surname', 'city', 'state', 'country', 'remarks'] as $field) {
|
||||
if (stripos((string)($row[$field] ?? ''), $searchQuery) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
//Среди подтвержденных полей идет сортировка
|
||||
$validSortKeys = ['id', 'callsign', 'fname', 'surname', 'city', 'state', 'country', 'remarks'];
|
||||
$sortKey = $_GET['sort'] ?? 'id';
|
||||
$sortOrder = $_GET['order'] ?? 'asc';
|
||||
|
||||
if (!in_array($sortKey, $validSortKeys)) {
|
||||
$sortKey = 'id';
|
||||
}
|
||||
|
||||
usort($confirmed, function($a, $b) use ($sortKey, $sortOrder) {
|
||||
$valA = $a[$sortKey] ?? '';
|
||||
$valB = $b[$sortKey] ?? '';
|
||||
|
||||
$result = is_numeric($valA) && is_numeric($valB)
|
||||
? $valA <=> $valB
|
||||
: strcasecmp((string)$valA, (string)$valB);
|
||||
//asc - desc : направление сортировки
|
||||
return $sortOrder === 'desc' ? -$result : $result;
|
||||
});
|
||||
|
||||
//Пагинация
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$perPage = 15;
|
||||
$total = count($confirmed);
|
||||
$totalPages = ceil($total / $perPage);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
//$confirmed - сортированный массив; $offset - на сколько сдвинуть от начала; $perPage - сколько на странице
|
||||
$currentPageData = array_slice($confirmed, $offset, $perPage);
|
||||
|
||||
//Используем, чтоб сохранить пагинацию и сортировку - используется на главной странице
|
||||
function sortLink(string $key, string $label): string {
|
||||
$currentSort = $_GET['sort'] ?? 'id';
|
||||
$currentOrder = $_GET['order'] ?? 'asc';
|
||||
$nextOrder = ($currentSort === $key && $currentOrder === 'asc') ? 'desc' : 'asc';
|
||||
$page = $_GET['page'] ?? 1;
|
||||
|
||||
return "<a href=\"?sort=$key&order=$nextOrder&page=$page\">$label</a>";
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,180 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f3f4f6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
header {
|
||||
background: #1f2937;
|
||||
color: #fff;
|
||||
padding: 15px 30px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
gap: 30px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.left-section { flex: 3; }
|
||||
.right-section {
|
||||
flex: 1;
|
||||
position: sticky;
|
||||
top: 30px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
.user-table {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.user-table th, .user-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px 8px;
|
||||
text-align: left;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.user-table th {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.user-table th:last-child,
|
||||
.user-table td.button-group {
|
||||
width: 90px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.unconfirmed-card {
|
||||
background: #fff;
|
||||
padding: 5px;
|
||||
margin-bottom: 1px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 4px rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-space {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 4px rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card span { font-size: 14px; }
|
||||
|
||||
.button-group {white-space: nowrap;
|
||||
text-align: center;
|
||||
flex-direction: row;
|
||||
padding: 4px;}
|
||||
.card a, .button-group a { display: inline-block;
|
||||
padding: 4px 8px;
|
||||
margin: 0 2px;
|
||||
font-size: 0.9em;
|
||||
background-color: #f4f4f4; color: white;
|
||||
border: 1px solid #ccc;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
border-radius: 4px;}
|
||||
.card a:hover { background-color: #f4f4f4; color:#1f2937; font-weight:bold;}
|
||||
.button-group a:hover { background-color: #d7d7d7; color:#1f2937;}
|
||||
.confirm { color: #10b981; font-weight: bold; text-decoration: none; }
|
||||
.delete { color: #ef4444; text-decoration: none; }
|
||||
.add-form input {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.add-form button {
|
||||
transition: background-color 0.3s ease;
|
||||
padding: 10px;
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add-form button:hover {
|
||||
padding: 10px;
|
||||
background: #f4f4f4;
|
||||
color: #1f2937;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-error {
|
||||
color: red;
|
||||
font-size: 13px;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sync-button {
|
||||
transition: background-color 0.3s ease;
|
||||
background: #ccc;
|
||||
color: #1f2937;
|
||||
border: none;
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sync-button:hover {
|
||||
background: white;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-form {
|
||||
margin-bottom: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.search-form input {
|
||||
padding: 6px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.search-form button {
|
||||
padding: 6px 10px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.search-form button:hover {background-color: #1f2937; color: white; border-radius: 5px;}
|
||||
.pagination { margin-top: 15px; text-align: center; }
|
||||
.pagination a {padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background-color: #f2f2f2; color: #333; text-decoration: none; transition: background-color 0.2s ease;}
|
||||
a { color: black; text-decoration: none;}
|
||||
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:visited,
|
||||
a:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
.pagination .current { background: #1f2937; color: #fff; }
|
||||
h2, h3 { margin-top: 0; }
|
||||
@@ -0,0 +1,125 @@
|
||||
//Две функции в файле - добавление и редактирование. Загружаются при загрузке странице,
|
||||
//ищут переменные по id или имени, добавляются к переменным через EventListener
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const idInput = document.querySelector('input[name="new_id"]');
|
||||
const csInput = document.querySelector('input[name="new_callsign"]');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const idError = document.getElementById('id-error');
|
||||
const csError = document.getElementById('callsign-error');
|
||||
const latinError = document.getElementById('latin-error');
|
||||
const optionalInputs = document.querySelectorAll('.add-form input[type="text"]:not([name="new_callsign"]):not([name="remarks"])');
|
||||
|
||||
async function check() {
|
||||
const id = idInput.value.trim();
|
||||
const callsign = csInput.value.trim();
|
||||
idError.textContent = '';
|
||||
csError.textContent = '';
|
||||
latinError.textContent = '';
|
||||
submitBtn.disabled = false;
|
||||
//Вызываем метод из логики на валидацию
|
||||
const res = await fetch(`?ajax_check=1&id=${encodeURIComponent(id)}&callsign=${encodeURIComponent(callsign)}`);
|
||||
const json = await res.json();
|
||||
|
||||
if (json.idInvalid) {
|
||||
idError.textContent = 'ID должен быть 6–7 цифр.';
|
||||
} else if (json.idExists) {
|
||||
idError.textContent = 'Такой ID уже существует.';
|
||||
}
|
||||
|
||||
if (json.callsignInvalid) {
|
||||
csError.textContent = 'Позывной: 4–7 латинских символов/цифр.';
|
||||
} else if (json.callsignExists) {
|
||||
csError.textContent = 'Такой позывной уже существует.';
|
||||
}
|
||||
|
||||
if (json.idInvalid || json.idExists || json.callsignInvalid || json.callsignExists) {
|
||||
submitBtn.disabled = true;
|
||||
}
|
||||
|
||||
for (const input of optionalInputs) {
|
||||
if (input.value && !/^[a-zA-Z\s]*$/.test(input.value)) {
|
||||
latinError.textContent = 'Дополнительные поля должны содержать только латиницу.';
|
||||
submitBtn.disabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Добавляем функцию на ввод
|
||||
idInput.addEventListener('input', check);
|
||||
csInput.addEventListener('input', check);
|
||||
optionalInputs.forEach(input => input.addEventListener('input', check));
|
||||
});
|
||||
|
||||
//Редактирование
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
//По клику получаем ближайшую ячеку и строку
|
||||
document.querySelectorAll('.edit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const td = btn.closest('td');
|
||||
const tr = td.closest('tr');
|
||||
const id = tr.cells[0].innerText;
|
||||
|
||||
if (tr.classList.contains('editing')) return;
|
||||
|
||||
tr.classList.add('editing');
|
||||
const cells = tr.querySelectorAll('td');
|
||||
const headers = ['id', 'callsign', 'fname', 'surname', 'city', 'state', 'country', 'remarks'];
|
||||
//Проходимся по ячейкам и меняем то, что внутри, ограничиваем позывной и телеграм
|
||||
for (let i = 1; i < cells.length - 1; i++) {
|
||||
const field = headers[i];
|
||||
const value = cells[i].textContent.trim();
|
||||
console.log(field);
|
||||
let inputHTML = `<input type="text" name="${field}" value="${value}" style="width: 100%"`;
|
||||
|
||||
if (field === 'callsign') {
|
||||
inputHTML += ` maxlength="7" required>`;
|
||||
cells[i].innerHTML = inputHTML;
|
||||
|
||||
const input = cells[i].querySelector('input');
|
||||
input.addEventListener('input', () => {
|
||||
input.value = input.value.replace(/[^A-Za-z0-9]/g, '');
|
||||
});
|
||||
|
||||
} else if (field === 'remarks') {
|
||||
inputHTML += ` maxlength="32">`;
|
||||
cells[i].innerHTML = inputHTML;
|
||||
|
||||
const input = cells[i].querySelector('input');
|
||||
input.addEventListener('input', () => {
|
||||
input.value = input.value.replace(/[^A-Za-z0-9_]/g, '');
|
||||
});
|
||||
|
||||
} else {
|
||||
inputHTML += '>';
|
||||
cells[i].innerHTML = inputHTML;
|
||||
}
|
||||
}
|
||||
|
||||
//Меняем внутреннее содержимое ячейки
|
||||
td.innerHTML = `
|
||||
<button class="save-btn">💾</button>
|
||||
<button class="cancel-btn">↩️</button>
|
||||
`;
|
||||
//Отправляем в класс с логикой
|
||||
td.querySelector('.save-btn').addEventListener('click', () => {
|
||||
const data = { "id": id};
|
||||
console.log('click on test');
|
||||
for (let i = 1; i < cells.length - 1; i++) {
|
||||
const input = cells[i].querySelector('input');
|
||||
data[headers[i]] = input.value;
|
||||
}
|
||||
|
||||
fetch('?edit=1', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).then(res => {location.reload();});
|
||||
});
|
||||
//Просто перезагружаем страницу - ничего не отправляем
|
||||
td.querySelector('.cancel-btn').addEventListener('click', () => location.reload());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$new_id = $_GET['id'] ?? '';
|
||||
$new_callsign = $_GET['callsign'] ?? '';
|
||||
|
||||
$response = [
|
||||
'idExists' => false,
|
||||
'callsignExists' => false,
|
||||
'idInvalid' => false,
|
||||
'callsignInvalid' => false,
|
||||
];
|
||||
|
||||
// Валидация ID
|
||||
if (!preg_match('/^\d{6,7}$/', $new_id)) {
|
||||
$response['idInvalid'] = true;
|
||||
}
|
||||
|
||||
// Валидация позывного
|
||||
if (!preg_match('/^[A-Z0-9]{4,7}$/', $new_callsign)) {
|
||||
$response['callsignInvalid'] = true;
|
||||
}
|
||||
|
||||
// Проверка в db.json
|
||||
$dbPath = __DIR__ . '/db.json';
|
||||
if (file_exists($dbPath)) {
|
||||
$json = json_decode(file_get_contents($dbPath), true);
|
||||
foreach ($json as $entry) {
|
||||
if (isset($entry['id']) && $entry['id'] == $new_id) {
|
||||
$response['idExists'] = true;
|
||||
}
|
||||
if (isset($entry['callsign']) && strcasecmp($entry['callsign'], $new_callsign) === 0) {
|
||||
$response['callsignExists'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка в dmrid.dat
|
||||
$dmrPath = __DIR__ . '/dmrid.dat';
|
||||
if (file_exists($dmrPath)) {
|
||||
$lines = file($dmrPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$parts = explode(';', $line);
|
||||
if (count($parts) >= 2) {
|
||||
$id = trim($parts[0]);
|
||||
$cs = trim($parts[1]);
|
||||
|
||||
if ($id === $new_id) {
|
||||
$response['idExists'] = true;
|
||||
}
|
||||
if (strcasecmp($cs, $new_callsign) === 0) {
|
||||
$response['callsignExists'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
+2513
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,228 @@
|
||||
1400001;PHOENIX;
|
||||
1030095;EVGEN;
|
||||
6700001;SEVER;
|
||||
140001;XLXSMK;
|
||||
140004;XLXPDS;
|
||||
140005;ECHO;
|
||||
1030217;PHOENIX;
|
||||
6700002;DVSB;
|
||||
6700003;DVSF;
|
||||
1030176;KASKAD;
|
||||
1400077;DYNAMIT;
|
||||
1030174;LEKTRON;
|
||||
2501655;ICEBERG;
|
||||
1030171;ZELLO;
|
||||
1030170;FRNLINK;
|
||||
1030167;LESHIY;
|
||||
1030166;FAGOT;
|
||||
1030165;STALKER;
|
||||
2501501;IRON;
|
||||
1030152;USER1;
|
||||
1030153;USER2;
|
||||
1030154;USER3;
|
||||
1030155;USER4;
|
||||
1030156;USER5;
|
||||
1030157;BARMI;
|
||||
1030158;USER7;
|
||||
1030159;USER8;
|
||||
1030160;USER9;
|
||||
1030161;USER10;
|
||||
2500460;RYBAK;
|
||||
2500779;R7KAY;
|
||||
7801620;MUTABOR;
|
||||
1030148;JUPITER;
|
||||
5000941;KIRPICH;
|
||||
1030144;EGERY;
|
||||
2840338;HBLINK;
|
||||
1030136;RYBACH;
|
||||
1030137;LUDZA;
|
||||
1030138;BUY27;
|
||||
1030139;DEMBI;
|
||||
1030140;KOLDA;
|
||||
1030141;GRAS;
|
||||
1030142;GRAS;
|
||||
1030143;KOPPI;
|
||||
2501513;SWIFT;
|
||||
1030134;REGION;
|
||||
1030133;VOLNA;
|
||||
1030132;SUMRAK;
|
||||
2501517;R2BRI;
|
||||
7804951;DUNAY;
|
||||
1030129;RUBEZH;
|
||||
2503228;KARDAN;
|
||||
1030127;HAUX;
|
||||
1030126;NEBO2;
|
||||
1030125;LUCKY;
|
||||
6550059;GITARIST;
|
||||
1030123;DIZEL;
|
||||
1030122;AKRA;
|
||||
2503128;SIMBA;
|
||||
7807808;YKCYC;
|
||||
2501250;ADVOKAT;
|
||||
1030118;KARAT;
|
||||
2509132;BARISTA;
|
||||
1030106;CHAMBAR;
|
||||
2501316;NARCISS;
|
||||
6550001;ARENDAT;
|
||||
6550002;UMBRELA;
|
||||
6550003;MIHA111;
|
||||
6550004;KAREL;
|
||||
6550006;ZS6DLK;
|
||||
6550008;ZS6RES;
|
||||
6550009;EVGEN;
|
||||
6550010;MAGUS;
|
||||
1030104;ARENDAT;
|
||||
2500352;EGORYCH;
|
||||
2570200;STRELA;
|
||||
1030103;MALOY;
|
||||
2501349;FOXMAN;
|
||||
1030098;NARCIS;
|
||||
1030097;SHTURMN;
|
||||
2501301;GRACH;
|
||||
1030040;ARKTEST;
|
||||
1030096;BARMI;
|
||||
1030092;ALEXBUT;
|
||||
1030091;MASHUK;
|
||||
1030088;AKCENT;
|
||||
1030086;NET1;
|
||||
6555027;BOGDAN;
|
||||
1030084;BOGDAN;
|
||||
1030083;KARAS;
|
||||
2501190;IGORH;
|
||||
1030081;AZGARD;
|
||||
1030080;MORSKOY;
|
||||
2501231;PENZA;
|
||||
1030078;VORON;
|
||||
1030077;AZART;
|
||||
1030100;DOCTOR;
|
||||
2864293;MIHA111;
|
||||
2508125;MEHANIK;
|
||||
1030074;GRANIT;
|
||||
2501366;SASHKO;
|
||||
1030072;FRNLINK;
|
||||
2518001;KORUS;
|
||||
1030070;SLITER;
|
||||
4700830;FDR830;
|
||||
1030068;ALEX203;
|
||||
2502228;PRORAB;
|
||||
1030066;PRORAB;
|
||||
2501099;KIPARIS;
|
||||
7800056;AYVENGO;
|
||||
6700067;VIRAJ;
|
||||
1030061;SAN428;
|
||||
2501182;POOH;
|
||||
1030059;DVINA;
|
||||
2570193;KANAPA;
|
||||
1030057;AYDAR;
|
||||
1030003;MIHA111;
|
||||
1030056;GNOM;
|
||||
2500358;NAIL44;
|
||||
1230311;YUG311;
|
||||
2500780;MAX03;
|
||||
2500945;DIMGRAD;
|
||||
1030178;RYAZAN;
|
||||
1030049;LEXUS;
|
||||
2500944;KALASH;
|
||||
7700023;BARON;
|
||||
1030002;UMBREL;
|
||||
1030046;VETER;
|
||||
1030004;VOSTOK;
|
||||
1030005;ELISTA;
|
||||
1030006;ARTEK;
|
||||
1030007;VLAD770;
|
||||
1030008;GITARIS;
|
||||
1030009;TRI333;
|
||||
1030011;URAL;
|
||||
1030013;AYAKS;
|
||||
2500263;NOVAK;
|
||||
1030016;DMITROV;
|
||||
1030012;VOST555;
|
||||
1030018;SENATOR;
|
||||
1030030;STELS;
|
||||
1030021;DANTIST;
|
||||
2502181;DMIT920;
|
||||
2570148;MASTER;
|
||||
2500887;ALEX;
|
||||
1030027;KARE;
|
||||
2502168;BUMER;
|
||||
2506145;GURZA;
|
||||
1030032;ARTEM48;
|
||||
1030033;LESNIK;
|
||||
5973777;SOKOL;
|
||||
2500222;VOLKIS;
|
||||
6555016;URAL;
|
||||
4600403;ARTEK;
|
||||
6550007;VLAD770;
|
||||
6555008;NAVIRUS;
|
||||
2503316;GITARIS;
|
||||
2500247;SOKOL;
|
||||
1030038;MAZAY;
|
||||
2502072;GRANIT;
|
||||
1030001;NAVIRUS;
|
||||
2501668;NEMOI;
|
||||
1400841;YAKUT;
|
||||
2500893;BOREY;
|
||||
2501457;ALEX77;
|
||||
1030182;BRONIX;
|
||||
1030183;KALITKA;
|
||||
1030184;0350;
|
||||
5600001;VLAD46;
|
||||
2501726;PATRIOT;
|
||||
2509122;DOMINIK;
|
||||
1030188;AVER89;
|
||||
1030189;MAGNAT;
|
||||
1030191;TERIKON;
|
||||
1030192;OCTOBER;
|
||||
2501757;TRIDVA;
|
||||
1030194;LIMAN;
|
||||
1111777;TRAKTOR;
|
||||
1030196;UDM697;
|
||||
2500773;ITMAN;
|
||||
2500500;PORU4IK;
|
||||
2501815;KAREL;
|
||||
7900001;DIMA55;
|
||||
1030201;ANTIK;
|
||||
2501712;KLIM45;
|
||||
1030203;DENKENG;
|
||||
1030204;1908;
|
||||
1030205;NUMARK;
|
||||
1030207;IGOR377;
|
||||
2500866;PRAD0SC;
|
||||
2501222;ENOT;
|
||||
1030212;RUSSIA;
|
||||
1030214;GROZA;
|
||||
2501263;REDDI77;
|
||||
1030216;BOTCHI;
|
||||
2501789;BURAN88;
|
||||
2500810;SLON;
|
||||
5500055;ROSTIK;
|
||||
1030230;DANAG;
|
||||
2501891;MEXAHNK;
|
||||
1030232;ZEZERON;
|
||||
1030233;PEAKTOP;
|
||||
1030234;ZATON;
|
||||
1030235;ROKOT40;
|
||||
2501931;ENGINER;
|
||||
1030239;DENNM;
|
||||
1030240;FDMA;
|
||||
1030241;GAGIN;
|
||||
2501918;IRTISH;
|
||||
4000001;AVIATOR;
|
||||
2502062;TRUBACH;
|
||||
2506090;KAVKAZ;
|
||||
1030249;AMUR;
|
||||
1419867;JDJDDJ;
|
||||
1415260;123123;
|
||||
2508021;KORUS;
|
||||
2500591;VLAD;
|
||||
2506134;KAVKAZ;
|
||||
2501956;MARCUS;
|
||||
7900002;SHULTZ;
|
||||
6401088;MITRA;
|
||||
140003;XLXARK;
|
||||
140000;XLXYKT;
|
||||
2506105;PHOENIX;
|
||||
2506217;KASKAD;
|
||||
141000;HBLINK;
|
||||
145001;PISTAR1;
|
||||
1030169;RAMON;
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
$files = [
|
||||
'admin.php' => 'Админка',
|
||||
'https://xlx.dmrykt.ru/' => 'XLX Сервер',
|
||||
'db.json' => 'Файл базы данных для HBLink',
|
||||
'dmrid.dat' => 'Файл базы данных для XLX',
|
||||
'https://xlx.dmrykt.ru/pistar/DMRIdsYKT.dat' => 'Файл базы данных для Pi-star',
|
||||
];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DMR ID Панель</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 100px auto;
|
||||
background: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
padding: 14px 24px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
background: #efefef;
|
||||
border-color: #bbb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Панель управления DMR ID</h1>
|
||||
<?php foreach ($files as $file => $label): ?>
|
||||
<a class="link" href="<?= htmlspecialchars($file) ?>"><?= htmlspecialchars($label) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
$successMessage = '';
|
||||
$errorMessage = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$callsign = strtoupper(trim($_POST['callsign']));
|
||||
$telegram = strtoupper(trim($_POST['remarks']));
|
||||
|
||||
if (!preg_match('/^[A-Z0-9]{4,7}$/', $callsign)) {
|
||||
$errorMessage = "Позывной должен содержать только латиницу и цифры (4–7 символов).";
|
||||
} else if (!preg_match('/^[a-zA-Z0-9_]{5,32}$/', $telegram)) {
|
||||
$errorMessage = "Телеграм быть от 5 до 32 символов, содержать только латинские буквы, цифры и подчёркивания.";
|
||||
} else {
|
||||
$file = __DIR__ . '/db.json';
|
||||
$json = json_decode(file_get_contents($file), true) ?? ['count' => 0, 'results' => []];
|
||||
$data = $json['results'];
|
||||
|
||||
foreach ($data as $row) {
|
||||
if (strcasecmp($row['callsign'], $callsign) === 0) {
|
||||
$errorMessage = "Такой позывной уже существует!";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errorMessage) {
|
||||
do {
|
||||
$newId = rand(1410000, 1419999);
|
||||
$used = false;
|
||||
foreach ($data as $row) {
|
||||
if ($row['id'] == $newId) {
|
||||
$used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while ($used);
|
||||
|
||||
$data[] = [
|
||||
"id" => $newId,
|
||||
"callsign" => $callsign,
|
||||
"fname" => $_POST['fname'] ?? "",
|
||||
"surname" => $_POST['surname'] ?? "",
|
||||
"city" => $_POST['city'] ?? "",
|
||||
"state" => $_POST['state'] ?? "",
|
||||
"country" => $_POST['country'] ?? "",
|
||||
"remarks" => $_POST['remarks'] ?? "",
|
||||
"confirmed" => false
|
||||
];
|
||||
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
header("Location: register.php?success=1");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($_GET['success'])) {
|
||||
$successMessage = "Заявка отправлена! Ожидайте подтверждения.";
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Регистрация DMR</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #f8f8f8;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
form {
|
||||
background: white;
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px #ccc;
|
||||
max-width: 400px;
|
||||
}
|
||||
input {
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
width: 90%;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
display: none;
|
||||
}
|
||||
.success {
|
||||
color: green;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Заявка на DMR ID</h2>
|
||||
|
||||
<?php if ($successMessage): ?>
|
||||
<p class="success"><?= $successMessage ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($errorMessage): ?>
|
||||
<p class="error" style="display:block"><?= $errorMessage ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" id="regForm" novalidate>
|
||||
<input oninput="this.value = this.value.toUpperCase()" type="text" name="callsign" placeholder="Позывной (4–7 символов)" required minlength="4" maxlength="7">
|
||||
<div class="error" id="callsign-error">Позывной должен содержать только латинские буквы и цифры (4–7 символов)</div>
|
||||
<input type="text" name="remarks" placeholder="Telegram: (Пример: DMRYKT)" required>
|
||||
<div class="error" id="telegram-error">Telegram Обязательное поле, только латинские буквы и цифры.</div>
|
||||
<input type="text" name="fname" placeholder="Имя (необязательно)">
|
||||
<input type="text" name="surname" placeholder="Фамилия (необязательно)">
|
||||
<input type="text" name="city" placeholder="Город (необязательно)">
|
||||
<input type="text" name="state" placeholder="Регион (необязательно)">
|
||||
<input type="text" name="country" placeholder="Страна (необязательно)">
|
||||
<div class="error" id="latin-error">Дополнительные поля должны содержать только латиницу (A-Z).</div>
|
||||
|
||||
<button type="submit" id="submit-btn">Отправить</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const callsignInput = document.querySelector('input[name="callsign"]');
|
||||
const remarksInput = document.querySelector('input[name="remarks"]');
|
||||
const latinInputs = document.querySelectorAll('input[name="fname"], input[name="surname"], input[name="city"], input[name="state"], input[name="country"]');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const callsignError = document.getElementById('callsign-error');
|
||||
const latinError = document.getElementById('latin-error');
|
||||
const telegramError = document.getElementById('telegram-error');
|
||||
|
||||
function validateForm() {
|
||||
let valid = true;
|
||||
const remarks = remarksInput.value.trim().toUpperCase();
|
||||
const callsign = callsignInput.value.trim();
|
||||
const latinRegex = /^[A-Za-z\s]*$/;
|
||||
|
||||
// Проверка позывного
|
||||
if (!/^[A-Z0-9]{4,7}$/.test(callsign)) {
|
||||
callsignError.style.display = 'block';
|
||||
valid = false;
|
||||
} else {
|
||||
callsignError.style.display = 'none';
|
||||
}
|
||||
|
||||
// Проверка всех остальных полей
|
||||
let latinValid = true;
|
||||
latinInputs.forEach(input => {
|
||||
if (input.value && !latinRegex.test(input.value)) {
|
||||
latinValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!latinValid) {
|
||||
latinError.style.display = 'block';
|
||||
valid = false;
|
||||
} else {
|
||||
latinError.style.display = 'none';
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_]{5,32}$/.test(remarks)) {
|
||||
telegramError.style.display = 'block';
|
||||
valid = false;
|
||||
} else {
|
||||
telegramError.style.display = 'none';
|
||||
}
|
||||
|
||||
submitBtn.disabled = !valid;
|
||||
}
|
||||
|
||||
// Проверка при вводе
|
||||
callsignInput.addEventListener('input', validateForm);
|
||||
remarksInput.addEventListener('input', validateForm);
|
||||
latinInputs.forEach(input => input.addEventListener('input', validateForm));
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
$file = __DIR__ . '/db.json';
|
||||
$datFile = __DIR__ . '/dmrid.dat';
|
||||
$json = json_decode(file_get_contents($file), true) ?? ['count' => 0, 'results' => []];
|
||||
$data = $json['results'];
|
||||
|
||||
if (isset($_GET['ajax_check'])) {
|
||||
$id = $_GET['id'] ?? '';
|
||||
$callsign = strtoupper(trim($_GET['callsign'] ?? ''));
|
||||
$response = [
|
||||
'idInvalid' => !preg_match('/^\d{6,7}$/', $id),
|
||||
'idExists' => false,
|
||||
'callsignInvalid' => !preg_match('/^[A-Z0-9]{4,7}$/', $callsign),
|
||||
'callsignExists' => false
|
||||
];
|
||||
foreach ($data as $row) {
|
||||
if ($row['id'] == $id) $response['idExists'] = true;
|
||||
if (strtoupper($row['callsign']) === $callsign) $response['callsignExists'] = true;
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['sync'])) {
|
||||
$confirmed = [];
|
||||
foreach ($data as $item) {
|
||||
if (!empty($item['confirmed'])) {
|
||||
$confirmed["{$item['id']};{$item['callsign']};"] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$filtered = array_filter($lines, function($line) use ($confirmed) {
|
||||
return isset($confirmed[trim($line)]);
|
||||
});
|
||||
|
||||
$existingLines = array_flip(array_map('trim', $lines));
|
||||
foreach ($confirmed as $entry => $val) {
|
||||
if (!isset($existingLines[$entry])) {
|
||||
$filtered[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
|
||||
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['confirm'])) {
|
||||
$confirmId = (int)$_GET['confirm'];
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if ($item['id'] === $confirmId) {
|
||||
$item['confirmed'] = true;
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
|
||||
if (!in_array($entry, $lines)) {
|
||||
file_put_contents($datFile, $entry . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['block'])) {
|
||||
$confirmId = (int)$_GET['block'];
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if ($item['id'] === $confirmId) {
|
||||
$item['confirmed'] = false;
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
|
||||
$filtered = array_filter($lines, function($line) use ($entry) {
|
||||
return trim($line) !== $entry;
|
||||
});
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['delete'])) {
|
||||
$deleteId = (int)$_GET['delete'];
|
||||
|
||||
$callsign = null;
|
||||
|
||||
foreach ($data as $index => $item) {
|
||||
if ($item['id'] === $deleteId) {
|
||||
$callsign = $item['callsign'];
|
||||
unset($data[$index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$data = array_values($data);
|
||||
|
||||
if ($callsign === null) {
|
||||
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$filtered = array_filter($lines, function($line) use ($deleteId, $callsign) {
|
||||
return trim($line) !== "{$deleteId};{$callsign};";
|
||||
});
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$form_error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$new_id = (int)$_POST['new_id'];
|
||||
$callsign = strtoupper(trim($_POST['new_callsign']));
|
||||
$fname = trim($_POST['fname'] ?? '');
|
||||
$surname = trim($_POST['surname'] ?? '');
|
||||
$city = trim($_POST['city'] ?? '');
|
||||
$state = trim($_POST['state'] ?? '');
|
||||
$country = trim($_POST['country'] ?? '');
|
||||
$remarks = trim($_POST['remarks'] ?? '');
|
||||
|
||||
if ($new_id < 100000 || $new_id > 9999999) {
|
||||
$form_error = 'ID должен быть 6–7 цифр.';
|
||||
} elseif (!preg_match('/^[A-Z0-9]{4,7}$/', $callsign)) {
|
||||
$form_error = 'Позывной: 4–7 латинских символов/цифр.';
|
||||
} elseif (array_filter($data, fn($r) => $r['id'] == $new_id || strtoupper($r['callsign']) === $callsign)) {
|
||||
$form_error = 'Такой ID или позывной уже существует.';
|
||||
} elseif (preg_match('/[^a-zA-Z\s]/', $fname . $surname . $city . $state . $country . $remarks)) {
|
||||
$form_error = 'Дополнительные поля должны содержать только латиницу.';
|
||||
} else {
|
||||
$data[] = [
|
||||
'id' => $new_id,
|
||||
'callsign' => $callsign,
|
||||
'fname' => $fname,
|
||||
'surname' => $surname,
|
||||
'city' => $city,
|
||||
'state' => $state,
|
||||
'country' => $country,
|
||||
'remarks' => $remarks,
|
||||
'confirmed' => true
|
||||
];
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$dat_line = "{$new_id};{$callsign};" . PHP_EOL;
|
||||
file_put_contents($datFile, $dat_line, FILE_APPEND);
|
||||
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$confirmed = array_values(array_filter($data, fn($d) => !empty($d['confirmed'])));
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$perPage = 15;
|
||||
$total = count($confirmed);
|
||||
$totalPages = ceil($total / $perPage);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$currentPageData = array_slice($confirmed, $offset, $perPage);
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Админка DMR</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f3f4f6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
header {
|
||||
background: #1f2937;
|
||||
color: #fff;
|
||||
padding: 15px 30px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
gap: 30px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.left-section { flex: 2; }
|
||||
.right-section {
|
||||
flex: 1;
|
||||
position: sticky;
|
||||
top: 30px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 4px rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card span { font-size: 14px; }
|
||||
.confirm { color: #10b981; font-weight: bold; text-decoration: none; }
|
||||
.delete { color: #ef4444; text-decoration: none; }
|
||||
.add-form input {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.add-form button {
|
||||
padding: 10px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-error {
|
||||
color: red;
|
||||
font-size: 13px;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sync-button {
|
||||
background: white;
|
||||
color: #1f2937;
|
||||
border: none;
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pagination { margin-top: 15px; text-align: center; }
|
||||
.pagination a { margin: 0 5px; text-decoration: none; padding: 5px 10px; background: #eee; border-radius: 5px; }
|
||||
.pagination .current { background: #1f2937; color: #fff; }
|
||||
h2, h3 { margin-top: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>Панель администратора DMR
|
||||
<form method="get" style="display:inline-block; float: right;" onsubmit="return confirm('Синхронизировать список с JSON?')">
|
||||
<input type="hidden" name="sync" value="1">
|
||||
<button type="submit" class="sync-button">Синхронизировать</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="left-section">
|
||||
<h2>Заявки</h2>
|
||||
<div id="cards">
|
||||
<?php foreach ($currentPageData as $row): ?>
|
||||
<div class="card">
|
||||
<span> <b>ID:</b> <?= $row['id'] ?></br>
|
||||
<b>Позывной:</b> <a href="https://xlxsmk.ru/index.php?subaction=userinfo&user=<?= htmlspecialchars($row['callsign']) ?>" class="confirm"><?= htmlspecialchars($row['callsign']) ?></a></br>
|
||||
<b>Имя:</b> <?= htmlspecialchars($row['fname']) ?></br>
|
||||
<b>Фамилия:</b> <?= htmlspecialchars($row['surname']) ?></br>
|
||||
<b>Город:</b> <?= htmlspecialchars($row['city']) ?></br>
|
||||
<b>Регион:</b> <?= htmlspecialchars($row['state']) ?></br>
|
||||
<b>Страна:</b> <?= htmlspecialchars($row['country']) ?></br>
|
||||
<b>Примечание:</b> <?= htmlspecialchars($row['remarks']) ?></br>
|
||||
</span>
|
||||
<span>
|
||||
<a href="?delete=<?= $row['id'] ?>" class="delete" onclick="return confirm('Удалить эту запись?')">Удалить</a>
|
||||
<a href="?block=<?= $row['id'] ?>" class="delete">Заблокировать</a>
|
||||
</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php for ($p = 1; $p <= $totalPages; $p++): ?>
|
||||
<a href="?page=<?= $p ?>" class="<?= $p == $page ? 'current' : '' ?>"><?= $p ?></a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="right-section">
|
||||
<h3>Добавить вручную</h3>
|
||||
<?php if (!empty($form_error)): ?>
|
||||
<div class="form-error"><?= htmlspecialchars($form_error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="post" class="add-form">
|
||||
<input type="number" name="new_id" placeholder="DMR ID" required>
|
||||
<div class="form-error" id="id-error"></div>
|
||||
|
||||
<input oninput="this.value = this.value.toUpperCase()" type="text" name="new_callsign" placeholder="Позывной" required>
|
||||
<div class="form-error" id="callsign-error"></div>
|
||||
|
||||
<input type="text" name="fname" placeholder="Имя (латиница)">
|
||||
<input type="text" name="surname" placeholder="Фамилия (латиница)">
|
||||
<input type="text" name="city" placeholder="Город (латиница)">
|
||||
<input type="text" name="state" placeholder="Регион (латиница)">
|
||||
<input type="text" name="country" placeholder="Страна (латиница)">
|
||||
<input type="text" name="remarks" placeholder="Примечание (латиница)">
|
||||
<div class="form-error" id="latin-error"></div>
|
||||
<button type="submit" id="submit-btn">Добавить</button>
|
||||
</form>
|
||||
|
||||
<div id="unconfirmed-cards" style="margin-top: 30px;">
|
||||
<h4>Ожидают подтверждения</h4>
|
||||
<?php foreach ($data as $row): ?>
|
||||
<?php if (empty($row['confirmed'])): ?>
|
||||
<div class="card">
|
||||
<span>⏳ <?= htmlspecialchars($row['callsign']) ?> </span>
|
||||
<a href="?confirm=<?= $row['id'] ?>" class="confirm">Подтвердить</a>
|
||||
<a href="?delete=<?= $row['id'] ?>" class="delete" onclick="return confirm('Удалить эту запись?')">Удалить</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const idInput = document.querySelector('input[name="new_id"]');
|
||||
const csInput = document.querySelector('input[name="new_callsign"]');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const idError = document.getElementById('id-error');
|
||||
const csError = document.getElementById('callsign-error');
|
||||
const latinError = document.getElementById('latin-error');
|
||||
const optionalInputs = document.querySelectorAll('.add-form input[type="text"]:not([name="new_callsign"])');
|
||||
|
||||
async function check() {
|
||||
const id = idInput.value.trim();
|
||||
const callsign = csInput.value.trim();
|
||||
idError.textContent = '';
|
||||
csError.textContent = '';
|
||||
latinError.textContent = '';
|
||||
submitBtn.disabled = false;
|
||||
|
||||
const res = await fetch(`?ajax_check=1&id=${encodeURIComponent(id)}&callsign=${encodeURIComponent(callsign)}`);
|
||||
const json = await res.json();
|
||||
|
||||
if (json.idInvalid) {
|
||||
idError.textContent = 'ID должен быть 6–7 цифр.';
|
||||
} else if (json.idExists) {
|
||||
idError.textContent = 'Такой ID уже существует.';
|
||||
}
|
||||
|
||||
if (json.callsignInvalid) {
|
||||
csError.textContent = 'Позывной: 4–7 латинских символов/цифр.';
|
||||
} else if (json.callsignExists) {
|
||||
csError.textContent = 'Такой позывной уже существует.';
|
||||
}
|
||||
|
||||
if (json.idInvalid || json.idExists || json.callsignInvalid || json.callsignExists) {
|
||||
submitBtn.disabled = true;
|
||||
}
|
||||
|
||||
for (const input of optionalInputs) {
|
||||
if (input.value && !/^[a-zA-Z\s]*$/.test(input.value)) {
|
||||
latinError.textContent = 'Дополнительные поля должны содержать только латиницу.';
|
||||
submitBtn.disabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idInput.addEventListener('input', check);
|
||||
csInput.addEventListener('input', check);
|
||||
optionalInputs.forEach(input => input.addEventListener('input', check));
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,224 @@
|
||||
1400001;ADMIN;
|
||||
140002;XLXYKT;
|
||||
140003;XLXARK;
|
||||
140004;XLXPDS;
|
||||
1412262;SEVERY;
|
||||
1410461;TESTI;
|
||||
670001;XLXB;
|
||||
670003;PODOLSK;
|
||||
6700002;DVSB;
|
||||
6700003;DVSF;
|
||||
1030176;KASKAD;
|
||||
1400077;DYNAMIT;
|
||||
1030174;LEKTRON;
|
||||
2501655;ICEBERG;
|
||||
1030169;RAMON;
|
||||
1030171;ZELLO;
|
||||
1030170;FRNLINK;
|
||||
1030167;LESHIY;
|
||||
1030166;FAGOT;
|
||||
1030165;STALKER;
|
||||
1030164;NIRVANA;
|
||||
2501501;IRON;
|
||||
1030153;USER2;
|
||||
1030154;USER3;
|
||||
1030155;USER4;
|
||||
1030156;USER5;
|
||||
1030157;BARMI;
|
||||
1030158;USER7;
|
||||
1030159;USER8;
|
||||
1030160;USER9;
|
||||
1030161;USER10;
|
||||
2500460;RYBAK;
|
||||
2500779;R7KAY;
|
||||
7801620;MUTABOR;
|
||||
1030148;JUPITER;
|
||||
5000941;KIRPICH;
|
||||
1030144;EGERY;
|
||||
2840338;HBLINK;
|
||||
1030136;RYBACH;
|
||||
1030137;LUDZA;
|
||||
1030138;BUY27;
|
||||
1030139;DEMBI;
|
||||
1030140;KOLDA;
|
||||
1030141;GRAS;
|
||||
1030142;GRAS;
|
||||
1030143;KOPPI;
|
||||
2501513;SWIFT;
|
||||
1030134;REGION;
|
||||
1030133;VOLNA;
|
||||
1030132;SUMRAK;
|
||||
2501517;R2BRI;
|
||||
7804951;DUNAY;
|
||||
1030129;RUBEZH;
|
||||
2503228;KARDAN;
|
||||
1030127;HAUX;
|
||||
1030126;NEBO2;
|
||||
1030125;LUCKY;
|
||||
6550059;GITARIST;
|
||||
1030123;DIZEL;
|
||||
1030122;AKRA;
|
||||
2503128;SIMBA;
|
||||
7807808;YKCYC;
|
||||
2501250;ADVOKAT;
|
||||
1030118;KARAT;
|
||||
2509132;BARISTA;
|
||||
1030106;CHAMBAR;
|
||||
2501316;NARCISS;
|
||||
6550001;ARENDAT;
|
||||
6550002;UMBRELA;
|
||||
6550003;MIHA111;
|
||||
6550004;KAREL;
|
||||
6550006;ZS6DLK;
|
||||
6550008;ZS6RES;
|
||||
6550009;EVGEN;
|
||||
6550010;MAGUS;
|
||||
1030104;ARENDAT;
|
||||
2500352;EGORYCH;
|
||||
2570200;STRELA;
|
||||
1030103;MALOY;
|
||||
2501349;FOXMAN;
|
||||
1030098;NARCIS;
|
||||
1030097;SHTURMN;
|
||||
2501301;GRACH;
|
||||
1030040;ARKTEST;
|
||||
1030096;BARMI;
|
||||
1030092;ALEXBUT;
|
||||
1030091;MASHUK;
|
||||
1030088;AKCENT;
|
||||
1000780;P25;
|
||||
1030086;NET1;
|
||||
1030087;NET2;
|
||||
6555027;BOGDAN;
|
||||
1030084;BOGDAN;
|
||||
1030083;KARAS;
|
||||
2501190;IGORH;
|
||||
1030081;AZGARD;
|
||||
1030080;MORSKOY;
|
||||
2501231;PENZA;
|
||||
1030078;VORON;
|
||||
1030100;DOCTOR;
|
||||
2864293;MIHA111;
|
||||
2508125;MEHANIK;
|
||||
1030074;GRANIT;
|
||||
2501366;SASHKO;
|
||||
1030072;FRNLINK;
|
||||
2518001;KORUS;
|
||||
1030070;SLITER;
|
||||
4700830;FDR830;
|
||||
1030068;ALEX203;
|
||||
2502228;PRORAB;
|
||||
1030066;PRORAB;
|
||||
2501099;KIPARIS;
|
||||
7800056;AYVENGO;
|
||||
6700067;VIRAJ;
|
||||
1030061;SAN428;
|
||||
2501182;POOH;
|
||||
1030059;DVINA;
|
||||
2570193;KANAPA;
|
||||
1030057;AYDAR;
|
||||
1030003;MIHA111;
|
||||
1030056;GNOM;
|
||||
2500358;NAIL44;
|
||||
1230311;YUG311;
|
||||
2500780;MAX03;
|
||||
2500945;DIMGRAD;
|
||||
1030178;RYAZAN;
|
||||
1030049;LEXUS;
|
||||
2500944;KALASH;
|
||||
7700023;BARON;
|
||||
1030002;UMBREL;
|
||||
1030046;VETER;
|
||||
1030004;VOSTOK;
|
||||
1030005;ELISTA;
|
||||
1030006;ARTEK;
|
||||
1030007;VLAD770;
|
||||
1030008;GITARIS;
|
||||
1030009;TRI333;
|
||||
1030011;URAL;
|
||||
1030013;AYAKS;
|
||||
2500263;NOVAK;
|
||||
1030016;DMITROV;
|
||||
1030012;VOST555;
|
||||
1030018;SENATOR;
|
||||
1030030;STELS;
|
||||
1030021;DANTIST;
|
||||
2502181;DMIT920;
|
||||
2570148;MASTER;
|
||||
2500887;ALEX;
|
||||
1030027;KARE;
|
||||
2502168;BUMER;
|
||||
2506145;GURZA;
|
||||
1030032;ARTEM48;
|
||||
1030033;LESNIK;
|
||||
5973777;SOKOL;
|
||||
2500222;VOLKIS;
|
||||
6555016;URAL;
|
||||
4600403;ARTEK;
|
||||
6550007;VLAD770;
|
||||
6555008;NAVIRUS;
|
||||
2503316;GITARIS;
|
||||
2500247;SOKOL;
|
||||
1030038;MAZAY;
|
||||
2502072;GRANIT;
|
||||
1030001;NAVIRUS;
|
||||
2501668;NEMOI;
|
||||
1400841;YAKUT;
|
||||
2500893;BOREY;
|
||||
2501457;ALEX77;
|
||||
1030182;BRONIX;
|
||||
1030183;KALITKA;
|
||||
1030184;0350;
|
||||
5600001;VLAD46;
|
||||
2501726;PATRIOT;
|
||||
2509122;DOMINIK;
|
||||
1030188;AVER89;
|
||||
1030189;MAGNAT;
|
||||
1030192;OCTOBER;
|
||||
2501757;TRIDVA;
|
||||
1030194;LIMAN;
|
||||
1111777;TRAKTOR;
|
||||
1030196;UDM697;
|
||||
2500773;ITMAN;
|
||||
2500500;PORU4IK;
|
||||
2501815;KAREL;
|
||||
7900001;DIMA55;
|
||||
1030201;ANTIK;
|
||||
2501712;KLIM45;
|
||||
1030203;DENKENG;
|
||||
1030204;1908;
|
||||
1030205;NUMARK;
|
||||
1030207;IGOR377;
|
||||
2500866;PRAD0SC;
|
||||
2501222;ENOT;
|
||||
1030212;RUSSIA;
|
||||
1030214;GROZA;
|
||||
2501263;REDDI77;
|
||||
1030216;BOTCHI;
|
||||
2501789;BURAN88;
|
||||
2500810;SLON;
|
||||
5500055;ROSTIK;
|
||||
1030230;DANAG;
|
||||
2501891;MEXAHNK;
|
||||
1030232;ZEZERON;
|
||||
1030233;PEAKTOP;
|
||||
1030234;ZATON;
|
||||
1030235;ROKOT40;
|
||||
2501931;ENGINER;
|
||||
1030239;DENNM;
|
||||
1030240;FDMA;
|
||||
1030241;GAGIN;
|
||||
2501918;IRTISH;
|
||||
4000001;AVIATOR;
|
||||
2502062;TRUBACH;
|
||||
1030249;AMUR;
|
||||
123123;SDDD;
|
||||
2508009;KORUS2;
|
||||
2500591;VLAD;
|
||||
1419867;JDJDDJ;
|
||||
1419418;TESTTES;
|
||||
1417126;TESTTE1;
|
||||
1416712;TESTTE1;
|
||||
670002;ECHO1;
|
||||
2501956;MARCUS;
|
||||
7900002;SHULTZ;
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
$file = __DIR__ . '/db.json';
|
||||
$datFile = __DIR__ . '/dmrid.dat';
|
||||
$json = json_decode(file_get_contents($file), true) ?? ['count' => 0, 'results' => []];
|
||||
$data = $json['results'];
|
||||
|
||||
if (isset($_GET['ajax_check'])) {
|
||||
$callsign = strtoupper(trim($_GET['callsign'] ?? ''));
|
||||
$response = [
|
||||
'callsignInvalid' => !preg_match('/^[A-Z0-9]{4,7}$/', $callsign),
|
||||
'callsignExists' => false
|
||||
];
|
||||
foreach ($data as $row) {
|
||||
if (strtoupper($row['callsign']) === $callsign) $response['callsignExists'] = true;
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['sync'])) {
|
||||
$lines = [];
|
||||
|
||||
foreach ($data as $item) {
|
||||
if (!empty($item['confirmed'])) {
|
||||
$id = trim($item['id']);
|
||||
$callsign = strtoupper(trim($item['callsign']));
|
||||
$lines[] = "{$id};{$callsign};";
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $lines) . PHP_EOL);
|
||||
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['confirm'])) {
|
||||
$confirmId = (int)$_GET['confirm'];
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if ($item['id'] === $confirmId) {
|
||||
$item['confirmed'] = true;
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
|
||||
if (!in_array($entry, $lines)) {
|
||||
file_put_contents($datFile, $entry . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['block'])) {
|
||||
$confirmId = (int)$_GET['block'];
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if ($item['id'] === $confirmId) {
|
||||
$item['confirmed'] = false;
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$entry = "{$item['id']};{$item['callsign']};";
|
||||
|
||||
$filtered = array_filter($lines, function($line) use ($entry) {
|
||||
return trim($line) !== $entry;
|
||||
});
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['delete'])) {
|
||||
$deleteId = (int)$_GET['delete'];
|
||||
|
||||
$callsign = null;
|
||||
|
||||
foreach ($data as $index => $item) {
|
||||
if ($item['id'] === $deleteId) {
|
||||
$callsign = $item['callsign'];
|
||||
unset($data[$index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$data = array_values($data);
|
||||
|
||||
if ($callsign === null) {
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$lines = file($datFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$filtered = array_filter($lines, function($line) use ($deleteId, $callsign) {
|
||||
return trim($line) !== "{$deleteId};{$callsign};";
|
||||
});
|
||||
|
||||
file_put_contents($datFile, implode(PHP_EOL, $filtered) . PHP_EOL);
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$form_error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$new_id = (int)$_POST['new_id'];
|
||||
do {
|
||||
$new_id = rand(1410000, 1419999);
|
||||
$used = false;
|
||||
foreach ($data as $row) {
|
||||
if ($row['id'] == $newId) {
|
||||
$used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while ($used);
|
||||
$callsign = strtoupper(trim($_POST['new_callsign']));
|
||||
$fname = trim($_POST['fname'] ?? '');
|
||||
$surname = trim($_POST['surname'] ?? '');
|
||||
$city = trim($_POST['city'] ?? '');
|
||||
$state = trim($_POST['state'] ?? '');
|
||||
$country = trim($_POST['country'] ?? '');
|
||||
$remarks = trim($_POST['remarks'] ?? '');
|
||||
|
||||
if ($new_id < 100000 || $new_id > 9999999) {
|
||||
$form_error = 'ID должен быть 6–7 цифр.';
|
||||
} elseif (!preg_match('/^[A-Z0-9]{4,7}$/', $callsign)) {
|
||||
$form_error = 'Позывной: 4–7 латинских символов/цифр.';
|
||||
} elseif (array_filter($data, fn($r) => $r['id'] == $new_id || strtoupper($r['callsign']) === $callsign)) {
|
||||
$form_error = 'Такой ID или позывной уже существует.';
|
||||
} elseif (preg_match('/[^a-zA-Z\s]/', $fname . $surname . $city . $state . $country . $remarks)) {
|
||||
$form_error = 'Дополнительные поля должны содержать только латиницу.';
|
||||
} else {
|
||||
$data[] = [
|
||||
'id' => $new_id,
|
||||
'callsign' => $callsign,
|
||||
'fname' => $fname,
|
||||
'surname' => $surname,
|
||||
'city' => $city,
|
||||
'state' => $state,
|
||||
'country' => $country,
|
||||
'remarks' => $remarks,
|
||||
'confirmed' => true
|
||||
];
|
||||
$json['results'] = $data;
|
||||
$json['count'] = count($data);
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$dat_line = "{$new_id};{$callsign};" . PHP_EOL;
|
||||
file_put_contents($datFile, $dat_line, FILE_APPEND);
|
||||
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$searchQuery = trim($_GET['search'] ?? '');
|
||||
|
||||
$confirmed = array_values(array_filter($data, fn($d) => !empty($d['confirmed'])));
|
||||
|
||||
if ($searchQuery !== '') {
|
||||
$confirmed = array_filter($confirmed, function($row) use ($searchQuery) {
|
||||
foreach (['id', 'callsign', 'fname', 'surname', 'city', 'state', 'country', 'remarks'] as $field) {
|
||||
if (stripos((string)($row[$field] ?? ''), $searchQuery) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
$validSortKeys = ['id', 'callsign', 'fname', 'surname', 'city', 'state', 'country', 'remarks'];
|
||||
$sortKey = $_GET['sort'] ?? 'id';
|
||||
$sortOrder = $_GET['order'] ?? 'asc';
|
||||
|
||||
if (!in_array($sortKey, $validSortKeys)) {
|
||||
$sortKey = 'id';
|
||||
}
|
||||
|
||||
usort($confirmed, function($a, $b) use ($sortKey, $sortOrder) {
|
||||
$valA = $a[$sortKey] ?? '';
|
||||
$valB = $b[$sortKey] ?? '';
|
||||
|
||||
$result = is_numeric($valA) && is_numeric($valB)
|
||||
? $valA <=> $valB
|
||||
: strcasecmp((string)$valA, (string)$valB);
|
||||
|
||||
return $sortOrder === 'desc' ? -$result : $result;
|
||||
});
|
||||
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$perPage = 15;
|
||||
$total = count($confirmed);
|
||||
$totalPages = ceil($total / $perPage);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$currentPageData = array_slice($confirmed, $offset, $perPage);
|
||||
|
||||
function sortLink(string $key, string $label): string {
|
||||
|
||||
$currentSort = $_GET['sort'] ?? 'id';
|
||||
$currentOrder = $_GET['order'] ?? 'asc';
|
||||
$nextOrder = ($currentSort === $key && $currentOrder === 'asc') ? 'desc' : 'asc';
|
||||
$page = $_GET['page'] ?? 1;
|
||||
|
||||
return "<a href=\"?sort=$key&order=$nextOrder&page=$page\">$label</a>";
|
||||
}
|
||||
?>
|
||||
Ссылка в новой задаче
Block a user