Compare commits
10 Commits
45bba687cd
...
c72df3fd00
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72df3fd00 | ||
|
|
889a408c57 | ||
|
|
a1b961ae20 | ||
|
|
4ef050e3c8 | ||
|
|
54784a41da | ||
|
|
21834d7d71 | ||
|
|
dd66cb5712 | ||
|
|
0d2003b335 | ||
|
|
f81b268d34 | ||
|
|
391f35e4be |
@ -5,3 +5,7 @@
|
|||||||
/.gitignore
|
/.gitignore
|
||||||
/.git
|
/.git
|
||||||
/logs/
|
/logs/
|
||||||
|
/telezab.db
|
||||||
|
/db/
|
||||||
|
/db/telezab.db
|
||||||
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -5,3 +5,9 @@
|
|||||||
/logs
|
/logs
|
||||||
/__pycache__
|
/__pycache__
|
||||||
/venv
|
/venv
|
||||||
|
/app.log
|
||||||
|
/logs/
|
||||||
|
/logs/app.log
|
||||||
|
/logs/error.log
|
||||||
|
/db/
|
||||||
|
/db/telezab.db
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@ -1,12 +1,17 @@
|
|||||||
FROM python:3.11.9-slim
|
FROM python:3.12.3-slim
|
||||||
LABEL authors="UdoChudo"
|
LABEL authors="UdoChudo"
|
||||||
# Установим необходимые пакеты
|
# Установим необходимые пакеты
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
gcc \
|
gcc \
|
||||||
|
tzdata \
|
||||||
|
sqlite3 \
|
||||||
|
curl \
|
||||||
|
telnet \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
# Установим рабочую директорию
|
# Установим рабочую директорию
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -18,6 +23,9 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
# Откроем порт для нашего приложения
|
# Откроем порт для нашего приложения
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
ENV FLASK_APP=telezab.py
|
ENV TZ=Europe/Moscow
|
||||||
|
ENV FLASK_APP telezab.py
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
# Запуск Gunicorn
|
# Запуск Gunicorn
|
||||||
CMD ["python3", "telezab.py"]
|
CMD ["python3", "telezab.py"]
|
||||||
201
log_manager.py
Normal file
201
log_manager.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import dictConfig
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
class UTF8StreamHandler(logging.StreamHandler):
|
||||||
|
def __init__(self, stream=None):
|
||||||
|
super().__init__(stream)
|
||||||
|
self.setStream(stream)
|
||||||
|
|
||||||
|
def setStream(self, stream):
|
||||||
|
super().setStream(stream)
|
||||||
|
if hasattr(stream, 'reconfigure'):
|
||||||
|
stream.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
class FilterByMessage(logging.Filter):
|
||||||
|
def filter(self, record):
|
||||||
|
# Фильтруем сообщения, содержащие 'Received 1 new updates'
|
||||||
|
return 'Received ' not in record.getMessage()
|
||||||
|
|
||||||
|
class LogManager:
|
||||||
|
def __init__(self, log_dir='logs', retention_days=30):
|
||||||
|
self.log_dir = log_dir
|
||||||
|
self.retention_days = retention_days
|
||||||
|
self.log_files = {
|
||||||
|
'flask': os.path.join(self.log_dir, 'flask.log'),
|
||||||
|
'flask_error': os.path.join(self.log_dir, 'flask_error.log'),
|
||||||
|
'app': os.path.join(self.log_dir, 'app.log'),
|
||||||
|
'app_error': os.path.join(self.log_dir, 'app_error.log'),
|
||||||
|
'debug': os.path.join(self.log_dir, 'debug.log'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure the log directory exists
|
||||||
|
if not os.path.exists(self.log_dir):
|
||||||
|
os.makedirs(self.log_dir)
|
||||||
|
|
||||||
|
# Setup logging configuration
|
||||||
|
self.setup_logging()
|
||||||
|
|
||||||
|
def setup_logging(self):
|
||||||
|
dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'default': {
|
||||||
|
'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s',
|
||||||
|
},
|
||||||
|
'error': {
|
||||||
|
'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s',
|
||||||
|
},
|
||||||
|
'werkzeug': {
|
||||||
|
'format': '[%(asctime)s] %(levelname)s %(message)s'
|
||||||
|
},
|
||||||
|
'debug': {
|
||||||
|
'format': '[%(asctime)s] %(levelname)s %(module)s [%(funcName)s:%(lineno)d]: %(message)s'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'telebot_console': {
|
||||||
|
'class': 'log_manager.UTF8StreamHandler',
|
||||||
|
'stream': 'ext://sys.stdout',
|
||||||
|
'formatter': 'default',
|
||||||
|
},
|
||||||
|
'flask_console': {
|
||||||
|
'class': 'log_manager.UTF8StreamHandler',
|
||||||
|
'stream': 'ext://sys.stdout',
|
||||||
|
'formatter': 'werkzeug',
|
||||||
|
},
|
||||||
|
'flask_file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': self.log_files['flask'],
|
||||||
|
'when': 'midnight',
|
||||||
|
'backupCount': self.retention_days,
|
||||||
|
'formatter': 'werkzeug',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
},
|
||||||
|
'flask_error_file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': self.log_files['flask_error'],
|
||||||
|
'when': 'midnight',
|
||||||
|
'backupCount': self.retention_days,
|
||||||
|
'formatter': 'werkzeug',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
'level': 'ERROR',
|
||||||
|
},
|
||||||
|
'app_file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': self.log_files['app'],
|
||||||
|
'when': 'midnight',
|
||||||
|
'backupCount': self.retention_days,
|
||||||
|
'formatter': 'default',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
},
|
||||||
|
'app_error_file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': self.log_files['app_error'],
|
||||||
|
'when': 'midnight',
|
||||||
|
'backupCount': self.retention_days,
|
||||||
|
'formatter': 'error',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
'level': 'ERROR',
|
||||||
|
},
|
||||||
|
'debug_file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': self.log_files['debug'],
|
||||||
|
'when': 'midnight',
|
||||||
|
'backupCount': self.retention_days,
|
||||||
|
'formatter': 'debug',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
'level': 'DEBUG',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'flask': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'handlers': ['flask_file', 'flask_error_file', 'flask_console'],
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'telebot': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'handlers': ['app_file', 'app_error_file', 'telebot_console'],
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'werkzeug': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'handlers': ['flask_file', 'flask_error_file', 'flask_console'],
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'debug': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'handlers': ['debug_file'],
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# 'root': {
|
||||||
|
# 'level': 'DEBUG',
|
||||||
|
# 'handlers': ['flask_console', 'telebot_console'],
|
||||||
|
# }
|
||||||
|
})
|
||||||
|
|
||||||
|
def archive_old_logs(self):
|
||||||
|
"""Archives old log files and removes logs older than retention_days."""
|
||||||
|
# Get yesterday's date
|
||||||
|
yesterday_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
for log_name, log_file in self.log_files.items():
|
||||||
|
if os.path.exists(log_file):
|
||||||
|
archive_name = f"{log_name}_{yesterday_date}.zip"
|
||||||
|
archive_path = os.path.join(self.log_dir, archive_name)
|
||||||
|
|
||||||
|
# Archive the log file
|
||||||
|
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||||
|
zipf.write(log_file, arcname=os.path.basename(log_file))
|
||||||
|
|
||||||
|
# Remove the old log file after archiving
|
||||||
|
os.remove(log_file)
|
||||||
|
|
||||||
|
# Clean up old archives
|
||||||
|
self.cleanup_old_archives()
|
||||||
|
|
||||||
|
def configure_werkzeug_logging(self):
|
||||||
|
"""Отключаем встроенный логгер Werkzeug и задаём собственные настройки логирования."""
|
||||||
|
werkzeug_logger = logging.getLogger('werkzeug')
|
||||||
|
werkzeug_logger.handlers = [] # Удаляем существующие обработчики
|
||||||
|
|
||||||
|
# Добавляем кастомный обработчик для форматирования логов
|
||||||
|
handler = TimedRotatingFileHandler(self.log_files['flask'], when='midnight', backupCount=self.retention_days, encoding='utf-8')
|
||||||
|
handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s %(message)s'))
|
||||||
|
werkzeug_logger.addHandler(handler)
|
||||||
|
|
||||||
|
# Отключаем дублирование логов
|
||||||
|
werkzeug_logger.propagate = False
|
||||||
|
|
||||||
|
def cleanup_old_archives(self):
|
||||||
|
"""Deletes archived logs older than retention_days."""
|
||||||
|
now = datetime.now()
|
||||||
|
cutoff = now - timedelta(days=self.retention_days)
|
||||||
|
|
||||||
|
for file in os.listdir(self.log_dir):
|
||||||
|
if file.endswith('.zip'):
|
||||||
|
file_path = os.path.join(self.log_dir, file)
|
||||||
|
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
|
||||||
|
if file_time < cutoff:
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
def schedule_log_rotation(self):
|
||||||
|
"""Schedules daily log rotation and archiving."""
|
||||||
|
from threading import Timer
|
||||||
|
now = datetime.now()
|
||||||
|
next_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
||||||
|
delay = (next_midnight - now).total_seconds()
|
||||||
|
|
||||||
|
Timer(delay, self.rotate_and_archive_logs).start()
|
||||||
|
|
||||||
|
def rotate_and_archive_logs(self):
|
||||||
|
"""Rotates and archives logs."""
|
||||||
|
self.archive_old_logs()
|
||||||
|
self.schedule_log_rotation() # Schedule the next rotation
|
||||||
|
|
||||||
107
region_api.py
Normal file
107
region_api.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
db_lock = Lock()
|
||||||
|
|
||||||
|
# Инициализируем логгер
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG) # Устанавливаем уровень логирования
|
||||||
|
|
||||||
|
|
||||||
|
class RegionAPI:
|
||||||
|
def __init__(self, db_path):
|
||||||
|
self.db_path = db_path
|
||||||
|
|
||||||
|
def add_region(self, region_id: int, region_name: str):
|
||||||
|
logger.info(f"Запрос на добавление региона: id={region_id}, name={region_name}")
|
||||||
|
|
||||||
|
# Проверка валидности region_id
|
||||||
|
if not str(region_id).isdigit():
|
||||||
|
logger.error(f"region_id {region_id} не является числом.")
|
||||||
|
return {"status": "failure", "message": "Region_id must be digit only"}
|
||||||
|
|
||||||
|
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
logger.debug(f"Проверка существования региона с id={region_id}")
|
||||||
|
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
# Добавляем новый регион
|
||||||
|
cursor.execute('INSERT INTO regions (region_id, region_name, active) VALUES (?, ?, 1)',
|
||||||
|
(region_id, region_name))
|
||||||
|
conn.commit()
|
||||||
|
logger.info(f"Регион с id={region_id} успешно добавлен.")
|
||||||
|
return {"status": "success", "message": "Region added successfully"}
|
||||||
|
else:
|
||||||
|
logger.warning(f"Регион с id={region_id} уже существует.")
|
||||||
|
return {"status": "error", "message": "Region already exists"}
|
||||||
|
|
||||||
|
def remove_region(self, region_id):
|
||||||
|
logger.info(f"Запрос на удаление региона: id={region_id}")
|
||||||
|
|
||||||
|
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
logger.debug(f"Проверка существования региона с id={region_id}")
|
||||||
|
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
logger.warning(f"Регион с id={region_id} не найден.")
|
||||||
|
return {"status": "error", "message": "Region not found"}
|
||||||
|
else:
|
||||||
|
cursor.execute('DELETE FROM regions WHERE region_id = ?', (region_id,))
|
||||||
|
conn.commit()
|
||||||
|
logger.info(f"Регион с id={region_id} успешно удалён.")
|
||||||
|
return {"status": "success", "message": "Region removed successfully"}
|
||||||
|
|
||||||
|
def get_regions(self):
|
||||||
|
logger.info("Запрос на получение списка регионов.")
|
||||||
|
|
||||||
|
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
logger.debug("Извлечение данных из таблицы regions.")
|
||||||
|
cursor.execute('SELECT region_id, region_name, active FROM regions')
|
||||||
|
regions = cursor.fetchall()
|
||||||
|
logger.info(f"Получено {len(regions)} регионов.")
|
||||||
|
return [{"region_id": r[0], "region_name": r[1], "regions_active": r[2]} for r in regions]
|
||||||
|
|
||||||
|
def change_region_status(self, region_id, active):
|
||||||
|
logger.info(f"Запрос на изменение статуса региона: id={region_id}, статус={active}")
|
||||||
|
|
||||||
|
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
logger.debug(f"Проверка существования региона с id={region_id}")
|
||||||
|
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
logger.warning(f"Регион с id={region_id} не найден.")
|
||||||
|
return {"status": "error", "message": "Region not found"}
|
||||||
|
else:
|
||||||
|
cursor.execute('UPDATE regions SET active = ? WHERE region_id = ?', (active, region_id))
|
||||||
|
conn.commit()
|
||||||
|
logger.info(f"Статус региона с id={region_id} успешно изменён.")
|
||||||
|
return {"status": "success", "message": "Region status updated successfully"}
|
||||||
|
|
||||||
|
def update_region_status(self, region_id, active):
|
||||||
|
logger.info(f"Запрос на обновление статуса региона: id={region_id}, активность={active}")
|
||||||
|
|
||||||
|
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Проверяем существование региона
|
||||||
|
logger.debug(f"Проверка существования региона с id={region_id}")
|
||||||
|
cursor.execute("SELECT region_name FROM regions WHERE region_id = ?", (region_id,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if not result:
|
||||||
|
logger.warning(f"Регион с id={region_id} не найден.")
|
||||||
|
return {"status": "error", "message": "Регион не найден"}
|
||||||
|
|
||||||
|
# Обновляем статус активности региона
|
||||||
|
cursor.execute("UPDATE regions SET active = ? WHERE region_id = ?", (int(active), region_id))
|
||||||
|
conn.commit()
|
||||||
|
action = "Активирован" if active else "Отключён"
|
||||||
|
logger.info(f"Регион с id={region_id} {action}.")
|
||||||
|
return {"status": "success", "message": f"Регион {region_id} {action}"}
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
5051
static/css/bootstrap-grid.css
vendored
Normal file
5051
static/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/css/bootstrap-grid.css.map
Normal file
1
static/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap-grid.min.css
vendored
Normal file
7
static/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap-grid.min.css.map
Normal file
1
static/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
5050
static/css/bootstrap-grid.rtl.css
vendored
Normal file
5050
static/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/css/bootstrap-grid.rtl.css.map
Normal file
1
static/css/bootstrap-grid.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap-grid.rtl.min.css
vendored
Normal file
7
static/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap-grid.rtl.min.css.map
Normal file
1
static/css/bootstrap-grid.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
485
static/css/bootstrap-reboot.css
vendored
Normal file
485
static/css/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2021 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2021 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--bs-blue: #0d6efd;
|
||||||
|
--bs-indigo: #6610f2;
|
||||||
|
--bs-purple: #6f42c1;
|
||||||
|
--bs-pink: #d63384;
|
||||||
|
--bs-red: #dc3545;
|
||||||
|
--bs-orange: #fd7e14;
|
||||||
|
--bs-yellow: #ffc107;
|
||||||
|
--bs-green: #198754;
|
||||||
|
--bs-teal: #20c997;
|
||||||
|
--bs-cyan: #0dcaf0;
|
||||||
|
--bs-white: #fff;
|
||||||
|
--bs-gray: #6c757d;
|
||||||
|
--bs-gray-dark: #343a40;
|
||||||
|
--bs-gray-100: #f8f9fa;
|
||||||
|
--bs-gray-200: #e9ecef;
|
||||||
|
--bs-gray-300: #dee2e6;
|
||||||
|
--bs-gray-400: #ced4da;
|
||||||
|
--bs-gray-500: #adb5bd;
|
||||||
|
--bs-gray-600: #6c757d;
|
||||||
|
--bs-gray-700: #495057;
|
||||||
|
--bs-gray-800: #343a40;
|
||||||
|
--bs-gray-900: #212529;
|
||||||
|
--bs-primary: #0d6efd;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-info: #0dcaf0;
|
||||||
|
--bs-warning: #ffc107;
|
||||||
|
--bs-danger: #dc3545;
|
||||||
|
--bs-light: #f8f9fa;
|
||||||
|
--bs-dark: #212529;
|
||||||
|
--bs-primary-rgb: 13, 110, 253;
|
||||||
|
--bs-secondary-rgb: 108, 117, 125;
|
||||||
|
--bs-success-rgb: 25, 135, 84;
|
||||||
|
--bs-info-rgb: 13, 202, 240;
|
||||||
|
--bs-warning-rgb: 255, 193, 7;
|
||||||
|
--bs-danger-rgb: 220, 53, 69;
|
||||||
|
--bs-light-rgb: 248, 249, 250;
|
||||||
|
--bs-dark-rgb: 33, 37, 41;
|
||||||
|
--bs-white-rgb: 255, 255, 255;
|
||||||
|
--bs-black-rgb: 0, 0, 0;
|
||||||
|
--bs-body-color-rgb: 33, 37, 41;
|
||||||
|
--bs-body-bg-rgb: 255, 255, 255;
|
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
|
--bs-body-font-size: 1rem;
|
||||||
|
--bs-body-font-weight: 400;
|
||||||
|
--bs-body-line-height: 1.5;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-body-bg: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
text-align: var(--bs-body-text-align);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: inherit;
|
||||||
|
background-color: currentColor;
|
||||||
|
border: 0;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr:not([size]) {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: calc(1.3rem + 0.6vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title],
|
||||||
|
abbr[data-bs-original-title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0.2em;
|
||||||
|
background-color: #fcf8e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0d6efd;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #0a58ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 1em;
|
||||||
|
direction: ltr /* rtl:ignore */;
|
||||||
|
unicode-bidi: bidi-override;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: #d63384;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
a > code {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #212529;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
kbd kbd {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
caption-side: bottom;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
select:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[list]::-webkit-calendar-picker-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type=button],
|
||||||
|
[type=reset],
|
||||||
|
[type=submit] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[type=button]:not(:disabled),
|
||||||
|
[type=reset]:not(:disabled),
|
||||||
|
[type=submit]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend + * {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
::-webkit-datetime-edit-text,
|
||||||
|
::-webkit-datetime-edit-minute,
|
||||||
|
::-webkit-datetime-edit-hour-field,
|
||||||
|
::-webkit-datetime-edit-day-field,
|
||||||
|
::-webkit-datetime-edit-month-field,
|
||||||
|
::-webkit-datetime-edit-year-field {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=search] {
|
||||||
|
outline-offset: -2px;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rtl:raw:
|
||||||
|
[type="tel"],
|
||||||
|
[type="url"],
|
||||||
|
[type="email"],
|
||||||
|
[type="number"] {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||||
1
static/css/bootstrap-reboot.css.map
Normal file
1
static/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
static/css/bootstrap-reboot.min.css
vendored
Normal file
8
static/css/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap-reboot.min.css.map
Normal file
1
static/css/bootstrap-reboot.min.css.map
Normal file
File diff suppressed because one or more lines are too long
482
static/css/bootstrap-reboot.rtl.css
vendored
Normal file
482
static/css/bootstrap-reboot.rtl.css
vendored
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2021 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2021 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--bs-blue: #0d6efd;
|
||||||
|
--bs-indigo: #6610f2;
|
||||||
|
--bs-purple: #6f42c1;
|
||||||
|
--bs-pink: #d63384;
|
||||||
|
--bs-red: #dc3545;
|
||||||
|
--bs-orange: #fd7e14;
|
||||||
|
--bs-yellow: #ffc107;
|
||||||
|
--bs-green: #198754;
|
||||||
|
--bs-teal: #20c997;
|
||||||
|
--bs-cyan: #0dcaf0;
|
||||||
|
--bs-white: #fff;
|
||||||
|
--bs-gray: #6c757d;
|
||||||
|
--bs-gray-dark: #343a40;
|
||||||
|
--bs-gray-100: #f8f9fa;
|
||||||
|
--bs-gray-200: #e9ecef;
|
||||||
|
--bs-gray-300: #dee2e6;
|
||||||
|
--bs-gray-400: #ced4da;
|
||||||
|
--bs-gray-500: #adb5bd;
|
||||||
|
--bs-gray-600: #6c757d;
|
||||||
|
--bs-gray-700: #495057;
|
||||||
|
--bs-gray-800: #343a40;
|
||||||
|
--bs-gray-900: #212529;
|
||||||
|
--bs-primary: #0d6efd;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-info: #0dcaf0;
|
||||||
|
--bs-warning: #ffc107;
|
||||||
|
--bs-danger: #dc3545;
|
||||||
|
--bs-light: #f8f9fa;
|
||||||
|
--bs-dark: #212529;
|
||||||
|
--bs-primary-rgb: 13, 110, 253;
|
||||||
|
--bs-secondary-rgb: 108, 117, 125;
|
||||||
|
--bs-success-rgb: 25, 135, 84;
|
||||||
|
--bs-info-rgb: 13, 202, 240;
|
||||||
|
--bs-warning-rgb: 255, 193, 7;
|
||||||
|
--bs-danger-rgb: 220, 53, 69;
|
||||||
|
--bs-light-rgb: 248, 249, 250;
|
||||||
|
--bs-dark-rgb: 33, 37, 41;
|
||||||
|
--bs-white-rgb: 255, 255, 255;
|
||||||
|
--bs-black-rgb: 0, 0, 0;
|
||||||
|
--bs-body-color-rgb: 33, 37, 41;
|
||||||
|
--bs-body-bg-rgb: 255, 255, 255;
|
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
|
--bs-body-font-size: 1rem;
|
||||||
|
--bs-body-font-weight: 400;
|
||||||
|
--bs-body-line-height: 1.5;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-body-bg: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
text-align: var(--bs-body-text-align);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: inherit;
|
||||||
|
background-color: currentColor;
|
||||||
|
border: 0;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr:not([size]) {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: calc(1.3rem + 0.6vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title],
|
||||||
|
abbr[data-bs-original-title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0.2em;
|
||||||
|
background-color: #fcf8e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0d6efd;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #0a58ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 1em;
|
||||||
|
direction: ltr ;
|
||||||
|
unicode-bidi: bidi-override;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: #d63384;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
a > code {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #212529;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
kbd kbd {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
caption-side: bottom;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
select:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[list]::-webkit-calendar-picker-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type=button],
|
||||||
|
[type=reset],
|
||||||
|
[type=submit] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[type=button]:not(:disabled),
|
||||||
|
[type=reset]:not(:disabled),
|
||||||
|
[type=submit]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
float: right;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend + * {
|
||||||
|
clear: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
::-webkit-datetime-edit-text,
|
||||||
|
::-webkit-datetime-edit-minute,
|
||||||
|
::-webkit-datetime-edit-hour-field,
|
||||||
|
::-webkit-datetime-edit-day-field,
|
||||||
|
::-webkit-datetime-edit-month-field,
|
||||||
|
::-webkit-datetime-edit-year-field {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=search] {
|
||||||
|
outline-offset: -2px;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="tel"],
|
||||||
|
[type="url"],
|
||||||
|
[type="email"],
|
||||||
|
[type="number"] {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||||
1
static/css/bootstrap-reboot.rtl.css.map
Normal file
1
static/css/bootstrap-reboot.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
8
static/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
8
static/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap-reboot.rtl.min.css.map
Normal file
1
static/css/bootstrap-reboot.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
4866
static/css/bootstrap-utilities.css
vendored
Normal file
4866
static/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/css/bootstrap-utilities.css.map
Normal file
1
static/css/bootstrap-utilities.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap-utilities.min.css
vendored
Normal file
7
static/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap-utilities.min.css.map
Normal file
1
static/css/bootstrap-utilities.min.css.map
Normal file
File diff suppressed because one or more lines are too long
4857
static/css/bootstrap-utilities.rtl.css
vendored
Normal file
4857
static/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/css/bootstrap-utilities.rtl.css.map
Normal file
1
static/css/bootstrap-utilities.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
7
static/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap-utilities.rtl.min.css.map
Normal file
1
static/css/bootstrap-utilities.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
11266
static/css/bootstrap.css
vendored
Normal file
11266
static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/css/bootstrap.css.map
Normal file
1
static/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap.min.css
vendored
Normal file
7
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap.min.css.map
Normal file
1
static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
11242
static/css/bootstrap.rtl.css
vendored
Normal file
11242
static/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/css/bootstrap.rtl.css.map
Normal file
1
static/css/bootstrap.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/css/bootstrap.rtl.min.css
vendored
Normal file
7
static/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/css/bootstrap.rtl.min.css.map
Normal file
1
static/css/bootstrap.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
23
static/css/regions.css
Normal file
23
static/css/regions.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/* Увеличение ширины колонки с регионами */
|
||||||
|
#region-list {
|
||||||
|
max-height: 600px; /* Устанавливаем максимальную высоту для вертикальной прокрутки */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-list {
|
||||||
|
max-height: 400px; /* Устанавливаем максимальную высоту для вертикальной прокрутки */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стилизация заголовков и кнопок */
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0 5px; /* Добавляем немного пространства между кнопками */
|
||||||
|
}
|
||||||
102
static/css/styles.css
Normal file
102
static/css/styles.css
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/* Обнуляем отступы для более гибкого макета */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Основные контейнеры */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Основной контейнер, занимает весь экран */
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Контейнер для списка пользователей */
|
||||||
|
.user-list-container {
|
||||||
|
width: 30%; /* Список пользователей занимает 30% ширины */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Прокручивающийся список пользователей */
|
||||||
|
.user-list {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.selected {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Блок управления */
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 70%; /* Блок с информацией и кнопкой занимает 70% ширины */
|
||||||
|
}
|
||||||
|
|
||||||
|
#show-events {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-info {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#events {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#events ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#events ul li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Контейнер для строки поиска */
|
||||||
|
.search-container {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
25
static/css/users.css
Normal file
25
static/css/users.css
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/* Добавим выравнивание и отступы для кнопок управления */
|
||||||
|
.table-hover tbody tr td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr td .btn-manage {
|
||||||
|
margin-left: 20px; /* Отступ кнопки от названия региона */
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
text-align: left; /* Выровнять содержимое слева */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr {
|
||||||
|
height: 50px; /* Сделать строки таблицы выше для лучшего визуального эффекта */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для пагинации */
|
||||||
|
.d-flex .btn {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
6812
static/js/bootstrap.bundle.js
vendored
Normal file
6812
static/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/js/bootstrap.bundle.js.map
Normal file
1
static/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/bootstrap.bundle.min.js.map
Normal file
1
static/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4999
static/js/bootstrap.esm.js
vendored
Normal file
4999
static/js/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/js/bootstrap.esm.js.map
Normal file
1
static/js/bootstrap.esm.js.map
Normal file
File diff suppressed because one or more lines are too long
7
static/js/bootstrap.esm.min.js
vendored
Normal file
7
static/js/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/bootstrap.esm.min.js.map
Normal file
1
static/js/bootstrap.esm.min.js.map
Normal file
File diff suppressed because one or more lines are too long
5046
static/js/bootstrap.js
vendored
Normal file
5046
static/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/js/bootstrap.js.map
Normal file
1
static/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
static/js/bootstrap.min.js
vendored
Normal file
7
static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/js/bootstrap.min.js.map
Normal file
1
static/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
10881
static/js/jquery-3.6.0.js
vendored
Normal file
10881
static/js/jquery-3.6.0.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
static/js/jquery-3.6.0.min.js
vendored
Normal file
2
static/js/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
133
static/js/regions.js
Normal file
133
static/js/regions.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
const regionList = $('#region-list');
|
||||||
|
const userInfo = $('#user-info');
|
||||||
|
const userList = $('#user-list');
|
||||||
|
const curentRegion = $('#curent-region')
|
||||||
|
const pagination = $('#pagination');
|
||||||
|
let regions = [];
|
||||||
|
let users = [];
|
||||||
|
let selectedRegionName = null;
|
||||||
|
let selectedRegionId = null;
|
||||||
|
const regionsPerPage = 10;
|
||||||
|
let currentPage = 1;
|
||||||
|
|
||||||
|
// Загрузка регионов
|
||||||
|
function loadRegions() {
|
||||||
|
$.getJSON('/telezab/regions/get', function(data) {
|
||||||
|
regions = data;
|
||||||
|
regions.sort((a, b) => a.region_id - b.region_id); // Сортировка по региону
|
||||||
|
renderRegions();
|
||||||
|
renderPagination();
|
||||||
|
}).fail(function() {
|
||||||
|
console.error("Не удалось получить регионы.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка пользователей
|
||||||
|
function loadUsers() {
|
||||||
|
$.getJSON('/telezab/users/get', function(data) {
|
||||||
|
users = data;
|
||||||
|
}).fail(function() {
|
||||||
|
console.error("Не удалось получить пользователей.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отрисовка списка регионов
|
||||||
|
function renderRegions() {
|
||||||
|
regionList.empty();
|
||||||
|
const start = (currentPage - 1) * regionsPerPage;
|
||||||
|
const end = start + regionsPerPage;
|
||||||
|
const paginatedRegions = regions.slice(start, end);
|
||||||
|
|
||||||
|
paginatedRegions.forEach(region => {
|
||||||
|
const listItem = $('<a>', {
|
||||||
|
href: '#',
|
||||||
|
class: 'list-group-item list-group-item-action',
|
||||||
|
text: `${region.region_name} (${region.region_id})`,
|
||||||
|
'data-region-id': region.region_id,
|
||||||
|
'data-region-name': region.region_name
|
||||||
|
}).on('click', function() {
|
||||||
|
selectedRegionId = $(this).data('region-id');
|
||||||
|
selectedRegionName = $(this).data('region-name');
|
||||||
|
showRegionDetails();
|
||||||
|
});
|
||||||
|
regionList.append(listItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отрисовка кнопок пагинации
|
||||||
|
function renderPagination() {
|
||||||
|
pagination.empty();
|
||||||
|
const totalPages = Math.ceil(regions.length / regionsPerPage);
|
||||||
|
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
const pageItem = $('<li>', {
|
||||||
|
class: `page-item ${i === currentPage ? 'active' : ''}`
|
||||||
|
}).append($('<a>', {
|
||||||
|
class: 'page-link',
|
||||||
|
href: '#',
|
||||||
|
text: i
|
||||||
|
}).on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
currentPage = i;
|
||||||
|
renderRegions();
|
||||||
|
renderPagination();
|
||||||
|
}));
|
||||||
|
pagination.append(pageItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показать детали региона
|
||||||
|
function showRegionDetails() {
|
||||||
|
userInfo.empty();
|
||||||
|
userList.empty();
|
||||||
|
curentRegion.empty(); // Очистить текущий элемент
|
||||||
|
|
||||||
|
// Найти текущий регион по selectedRegionId
|
||||||
|
const selectedRegion = regions.find(region => region.region_name === selectedRegionName);
|
||||||
|
|
||||||
|
// Проверить, что регион найден
|
||||||
|
if (selectedRegion) {
|
||||||
|
// Создать и добавить заголовок h3 с именем региона
|
||||||
|
var newHeading = $('<h3>').text(selectedRegion.region_name);
|
||||||
|
$('#curent-region').append(newHeading);
|
||||||
|
} else {
|
||||||
|
$('#curent-region').text('Регион не найден.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтровать пользователей, подписанных на этот регион
|
||||||
|
const subscribedUsers = users.filter(user =>
|
||||||
|
user.subscriptions.split(', ').includes(selectedRegionId.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Показать список пользователей или сообщение, если подписчиков нет
|
||||||
|
if (subscribedUsers.length > 0) {
|
||||||
|
subscribedUsers.forEach(user => {
|
||||||
|
userList.append(`<li>${user.email}</li>`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userInfo.text('Нет подписчиков на этот регион.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Инициализация
|
||||||
|
loadRegions();
|
||||||
|
loadUsers();
|
||||||
|
|
||||||
|
// Обработчики кнопок в модальном окне
|
||||||
|
$('#delete-region').on('click', function() {
|
||||||
|
// Функция удаления региона
|
||||||
|
console.log('Удалить регион', selectedRegionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#disable-region').on('click', function() {
|
||||||
|
// Функция отключения региона
|
||||||
|
console.log('Отключить регион', selectedRegionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#activate-region').on('click', function() {
|
||||||
|
// Функция активации региона
|
||||||
|
console.log('Активировать регион', selectedRegionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
55
static/js/users.js
Normal file
55
static/js/users.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
// Получаем список сотрудников
|
||||||
|
$.getJSON('/telezab/users/get', function(data) {
|
||||||
|
var userList = $('#user-list');
|
||||||
|
userList.empty();
|
||||||
|
data.forEach(function(user) {
|
||||||
|
var email = user.email;
|
||||||
|
var name = email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase());
|
||||||
|
var listItem = $('<li class="list-group-item"></li>').text(name).data('user', user);
|
||||||
|
userList.append(listItem);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик кликов по пользователям
|
||||||
|
$('#user-list').on('click', '.list-group-item', function() {
|
||||||
|
// Удаляем активный класс у всех элементов списка
|
||||||
|
$('.list-group-item').removeClass('active');
|
||||||
|
|
||||||
|
// Добавляем активный класс к выбранному элементу
|
||||||
|
$(this).addClass('active');
|
||||||
|
|
||||||
|
var user = $(this).data('user');
|
||||||
|
$('#user-info').removeClass('d-none');
|
||||||
|
$('#user-name').text(user.email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase()));
|
||||||
|
|
||||||
|
// Отображаем регионы в одну строку
|
||||||
|
var regions = $('#user-regions');
|
||||||
|
regions.empty();
|
||||||
|
if (user.subscriptions) {
|
||||||
|
var subscriptions = user.subscriptions.split(',').map(function(sub) { return sub.trim(); });
|
||||||
|
if (subscriptions.length > 0) {
|
||||||
|
regions.text(subscriptions.join(', '));
|
||||||
|
} else {
|
||||||
|
regions.text('Нет подписок');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
regions.text('Нет подписок');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображаем действия
|
||||||
|
var events = $('#user-events');
|
||||||
|
events.empty();
|
||||||
|
if (user.events && user.events.length > 0) {
|
||||||
|
user.events.forEach(function(event) {
|
||||||
|
var eventText = event.type;
|
||||||
|
if (event.region) {
|
||||||
|
eventText += ' (Регион: ' + event.region + ')';
|
||||||
|
}
|
||||||
|
events.append('<div><strong>' + event.date + '</strong> - ' + eventText + '</div>');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
events.append('<div>Нет действий</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
1983
telezab.py
1983
telezab.py
File diff suppressed because it is too large
Load Diff
53
templates/regions.html
Normal file
53
templates/regions.html
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Region Management</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/regions.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Левый контейнер со списком регионов -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>Список Регионов</h3>
|
||||||
|
<div class="list-group" id="region-list">
|
||||||
|
<!-- Список регионов будет добавляться сюда -->
|
||||||
|
</div>
|
||||||
|
<!-- Пагинация -->
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination" id="pagination">
|
||||||
|
<!-- Кнопки пагинации будут добавляться сюда -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<!-- Правый контейнер с информацией о пользователях -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>Пользователи</h3>
|
||||||
|
<div id="curent-region">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="user-info">
|
||||||
|
<!-- Информация о пользователях будет добавляться сюда -->
|
||||||
|
</div>
|
||||||
|
<ul id="user-list">
|
||||||
|
<!-- Список пользователей будет добавляться сюда -->
|
||||||
|
</ul>
|
||||||
|
<!-- Кнопки управления регионами -->
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-3">
|
||||||
|
<button type="button" class="btn btn-danger me-2" id="delete-region">Удалить регион</button>
|
||||||
|
<button type="button" class="btn btn-warning me-2" id="disable-region">Отключить регион</button>
|
||||||
|
<button type="button" class="btn btn-success" id="activate-region">Активировать регион</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/regions.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
templates/users.html
Normal file
37
templates/users.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Сотрудники</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
|
||||||
|
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<ul id="user-list" class="list-group">
|
||||||
|
<!-- Список сотрудников будет вставлен сюда -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div id="user-info" class="d-none">
|
||||||
|
<h3 id="user-name"></h3>
|
||||||
|
<h5>Подписки на регионы</h5>
|
||||||
|
<ul id="user-regions" class="list-group">
|
||||||
|
<!-- Подписки на регионы будут вставлены сюда -->
|
||||||
|
</ul>
|
||||||
|
<h5>Действия</h5>
|
||||||
|
<div id="user-events" class="overflow-auto" style="max-height: 300px;">
|
||||||
|
<!-- Действия будут вставлены сюда -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
user_state_manager.py
Normal file
16
user_state_manager.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
class UserStateManager:
|
||||||
|
def __init__(self):
|
||||||
|
# Словарь для хранения состояния каждого пользователя
|
||||||
|
self.user_states = {}
|
||||||
|
|
||||||
|
def set_state(self, chat_id, state):
|
||||||
|
"""Устанавливает состояние для пользователя."""
|
||||||
|
self.user_states[chat_id] = state
|
||||||
|
|
||||||
|
def get_state(self, chat_id):
|
||||||
|
"""Получает текущее состояние пользователя."""
|
||||||
|
return self.user_states.get(chat_id, "MAIN_MENU")
|
||||||
|
|
||||||
|
def reset_state(self, chat_id):
|
||||||
|
"""Сбрасывает состояние пользователя в главное меню."""
|
||||||
|
self.user_states[chat_id] = "MAIN_MENU"
|
||||||
Loading…
x
Reference in New Issue
Block a user