Massive rework of menu,
Add endpoint telezab/users Add endpoint telezab/users/add Add endpoint telezab/users/del Add endpoint telezab/users/get Add endpoint telezab/regions Add endpoint telezab/regions/add Add endpoint telezab/regions/del Add endpoint telezab/regions/get Rework Active Triggers button now don't need subscription Rework Help button Add option to change what Notification type you want reciving All or Disaster Only Rework Settings button removed some misc buttons Rework Registration mechanism now using POST JSON users/add Rework formating of Zabbix Triggers for Active triggers and Notification from Zabbix
This commit is contained in:
parent
dd66cb5712
commit
21834d7d71
@ -6,3 +6,6 @@
|
||||
/.git
|
||||
/logs/
|
||||
/telezab.db
|
||||
/db/
|
||||
/db/telezab.db
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM python:3.11.9-slim
|
||||
FROM python:3.12.3-slim
|
||||
LABEL authors="UdoChudo"
|
||||
# Установим необходимые пакеты
|
||||
RUN apt-get update && apt-get install -y \
|
||||
|
||||
157
log_manager.py
Normal file
157
log_manager.py
Normal file
@ -0,0 +1,157 @@
|
||||
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'),
|
||||
}
|
||||
|
||||
# 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',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'log_manager.UTF8StreamHandler',
|
||||
'stream': 'ext://sys.stdout',
|
||||
'formatter': 'default',
|
||||
},
|
||||
'flask_file': {
|
||||
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
'filename': self.log_files['flask'],
|
||||
'when': 'midnight',
|
||||
'backupCount': self.retention_days,
|
||||
'formatter': 'default',
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
'flask_error_file': {
|
||||
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
'filename': self.log_files['flask_error'],
|
||||
'when': 'midnight',
|
||||
'backupCount': self.retention_days,
|
||||
'formatter': 'error',
|
||||
'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',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'flask': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['flask_file', 'flask_error_file', 'console'],
|
||||
'propagate': False,
|
||||
},
|
||||
'telebot': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['app_file', 'app_error_file', 'console'],
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['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 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
|
||||
|
||||
73
region_api.py
Normal file
73
region_api.py
Normal file
@ -0,0 +1,73 @@
|
||||
# region_api.py
|
||||
|
||||
import sqlite3
|
||||
from threading import Lock
|
||||
|
||||
db_lock = Lock()
|
||||
|
||||
class RegionAPI:
|
||||
def __init__(self, db_path):
|
||||
self.db_path = db_path
|
||||
|
||||
def add_region(self, region_id: int, region_name: str):
|
||||
if not region_id.isdigit() == True:
|
||||
return {"status": "failure", "message": "Region_id must be digit only"}
|
||||
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
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()
|
||||
return {"status": "success", "message": "Region added successfully"}
|
||||
else:
|
||||
return {"status": "error", "message": "Region already exists"}
|
||||
|
||||
def remove_region(self, region_id):
|
||||
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
|
||||
count = cursor.fetchone()[0]
|
||||
if count == 0:
|
||||
return {"status": "error", "message": "Region not found"}
|
||||
else:
|
||||
cursor.execute('DELETE FROM regions WHERE region_id = ?', (region_id,))
|
||||
conn.commit()
|
||||
return {"status": "success", "message": "Region removed successfully"}
|
||||
|
||||
def get_regions(self):
|
||||
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT region_id, region_name, active FROM regions')
|
||||
regions = cursor.fetchall()
|
||||
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):
|
||||
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
if count == 0:
|
||||
return {"status": "error", "message": "Region not found"}
|
||||
else:
|
||||
cursor.execute('UPDATE regions SET active = ? WHERE region_id = ?', (active, region_id))
|
||||
conn.commit()
|
||||
return {"status": "success", "message": "Region status updated successfully"}
|
||||
|
||||
def update_region_status(self, region_id, active):
|
||||
with db_lock, sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверяем существование региона
|
||||
cursor.execute("SELECT region_name FROM regions WHERE region_id = ?", (region_id,))
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
return {"status": "error", "message": "Регион не найден"}
|
||||
|
||||
# Обновляем статус активности региона
|
||||
cursor.execute("UPDATE regions SET active = ? WHERE region_id = ?", (int(active), region_id))
|
||||
conn.commit()
|
||||
|
||||
action = "Активирован" if active else "Отключён"
|
||||
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>');
|
||||
}
|
||||
});
|
||||
});
|
||||
1801
telezab.py
1801
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