beta 2.0
This commit is contained in:
@@ -1,8 +1,25 @@
|
|||||||
from flask import Flask, request, jsonify, render_template
|
from flask import Flask, redirect, request, jsonify, render_template, url_for
|
||||||
from config import Config
|
from config import Config
|
||||||
from db import PostScheduler, UsersMedods, VkAPI, VkPost, db, MedodsAPI, ApiEndpoint
|
from db import (
|
||||||
from medods_handler import updateMedodsUsers
|
BirthdateScheduler,
|
||||||
from scheduler import get_scheduler_status, init_scheduler, enable_publish_job
|
PostScheduler,
|
||||||
|
Protection,
|
||||||
|
UsersBirthdate,
|
||||||
|
UsersMedods,
|
||||||
|
VkAPI,
|
||||||
|
VkPost,
|
||||||
|
db,
|
||||||
|
MedodsAPI,
|
||||||
|
ApiEndpoint,
|
||||||
|
)
|
||||||
|
from medods_handler import updateMedodsUsers, updateUsersBirthdate
|
||||||
|
from scheduler import (
|
||||||
|
enable_birthdate_job,
|
||||||
|
get_birthdate_scheduler_status,
|
||||||
|
get_scheduler_status,
|
||||||
|
init_scheduler,
|
||||||
|
enable_publish_job,
|
||||||
|
)
|
||||||
from http_client import send_request
|
from http_client import send_request
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -139,7 +156,7 @@ def vk():
|
|||||||
@app.route("/posts", methods=["GET"])
|
@app.route("/posts", methods=["GET"])
|
||||||
def posts():
|
def posts():
|
||||||
medodsUsers = UsersMedods.query.all()
|
medodsUsers = UsersMedods.query.all()
|
||||||
if len(medodsUsers) > 0:
|
if medodsUsers:
|
||||||
medodsUsers = [user.toDict() for user in medodsUsers]
|
medodsUsers = [user.toDict() for user in medodsUsers]
|
||||||
vkPost = VkPost.query.first()
|
vkPost = VkPost.query.first()
|
||||||
if vkPost:
|
if vkPost:
|
||||||
@@ -159,6 +176,21 @@ def posts():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/birthdate", methods=["GET"])
|
||||||
|
def birthdate():
|
||||||
|
schedulerStatus = get_birthdate_scheduler_status()
|
||||||
|
schedulerSettings = BirthdateScheduler.query.first()
|
||||||
|
if schedulerSettings:
|
||||||
|
schedulerSettings = schedulerSettings.toDict()
|
||||||
|
return render_template(
|
||||||
|
"birthdate.html",
|
||||||
|
data={
|
||||||
|
"schedulerStatus": schedulerStatus,
|
||||||
|
"schedulerSettings": schedulerSettings,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/medods", methods=["POST"])
|
@app.route("/api/medods", methods=["POST"])
|
||||||
def api_medods():
|
def api_medods():
|
||||||
try:
|
try:
|
||||||
@@ -273,6 +305,7 @@ def api_requests():
|
|||||||
logger.info("Получен список запросов")
|
logger.info("Получен список запросов")
|
||||||
|
|
||||||
requestsDB = ApiEndpoint.query.all()
|
requestsDB = ApiEndpoint.query.all()
|
||||||
|
if requestsDB:
|
||||||
requestsList = [r.toDict() for r in requestsDB]
|
requestsList = [r.toDict() for r in requestsDB]
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
@@ -286,6 +319,79 @@ def api_requests():
|
|||||||
return jsonify({"status": "error"}), 405
|
return jsonify({"status": "error"}), 405
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/birthdate", methods=["GET", "POST", "PATCH"])
|
||||||
|
def api_birthdate():
|
||||||
|
match request.method:
|
||||||
|
case "POST":
|
||||||
|
reqData = request.json
|
||||||
|
userUpdate = reqData["userUpdate"]
|
||||||
|
if userUpdate:
|
||||||
|
logger.info("Обновлен пользователь")
|
||||||
|
userId = userUpdate["userId"]
|
||||||
|
userData = userUpdate["userData"]
|
||||||
|
try:
|
||||||
|
userDB = UsersBirthdate.query.filter_by(id=userId).first()
|
||||||
|
userDB.photo_link = userData["photoLink"]
|
||||||
|
userDB.congratulations = userData["congratulations"]
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при обновлении пользователя: {e}")
|
||||||
|
return jsonify({"status": "error"}), 500
|
||||||
|
scheduleSettings = reqData["scheduleSettings"]
|
||||||
|
if scheduleSettings:
|
||||||
|
logger.info("Обновлены настройки расписания")
|
||||||
|
try:
|
||||||
|
scheduleDB = BirthdateScheduler.query.first()
|
||||||
|
if scheduleDB:
|
||||||
|
scheduleDB.hour = scheduleSettings["hour"]
|
||||||
|
scheduleDB.minute = scheduleSettings["minute"]
|
||||||
|
scheduleDB.enabled = scheduleSettings["enabled"]
|
||||||
|
else:
|
||||||
|
scheduleDB = BirthdateScheduler(
|
||||||
|
hour=scheduleSettings["hour"],
|
||||||
|
minute=scheduleSettings["minute"],
|
||||||
|
enabled=scheduleSettings["enabled"],
|
||||||
|
)
|
||||||
|
db.session.add(scheduleDB)
|
||||||
|
db.session.commit()
|
||||||
|
enable_birthdate_job()
|
||||||
|
scheduleInfo = get_birthdate_scheduler_status()
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"next_run_time": scheduleInfo.get("next_run_time"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при обновлении настройки расписания: {e}")
|
||||||
|
return jsonify({"status": "error"}), 500
|
||||||
|
|
||||||
|
case "GET":
|
||||||
|
logger.info("Получен список пользователей")
|
||||||
|
|
||||||
|
users = UsersBirthdate.query.all()
|
||||||
|
if users:
|
||||||
|
users = [u.toDict() for u in users]
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"users": users,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
case "PATCH":
|
||||||
|
success = updateUsersBirthdate()
|
||||||
|
if success:
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
else:
|
||||||
|
return jsonify({"status": "error"}), 500
|
||||||
|
|
||||||
|
case _:
|
||||||
|
logger.error("Неверный метод запроса")
|
||||||
|
return jsonify({"status": "error"}), 405
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/vk", methods=["POST"])
|
@app.route("/api/vk", methods=["POST"])
|
||||||
def api_vk():
|
def api_vk():
|
||||||
requestData = request.json
|
requestData = request.json
|
||||||
@@ -410,13 +516,84 @@ def api_posts():
|
|||||||
return jsonify({"status": "error"}), 405
|
return jsonify({"status": "error"}), 405
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/login", methods=["GET", "POST", "PATCH"])
|
||||||
|
def login():
|
||||||
|
match request.method:
|
||||||
|
case "GET":
|
||||||
|
return render_template("login.html")
|
||||||
|
case "POST":
|
||||||
|
data = request.get_json()
|
||||||
|
password = data.get("password")
|
||||||
|
|
||||||
|
protection = Protection.query.first()
|
||||||
|
if protection:
|
||||||
|
if not protection.verify_password(password):
|
||||||
|
return (
|
||||||
|
jsonify({"status": "error", "errorMessage": "Неверный пароль"}),
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
protection.generate_token()
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
protection = Protection()
|
||||||
|
protection.set_password(password)
|
||||||
|
protection.generate_token()
|
||||||
|
|
||||||
|
db.session.add(protection)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({"status": "ok", "token": protection.token}), 200
|
||||||
|
case "PATCH":
|
||||||
|
data = request.get_json()
|
||||||
|
new_password = data.get("password")
|
||||||
|
|
||||||
|
if not new_password:
|
||||||
|
return {"status": "error", "errorMessage": "Пароль не задан"}, 400
|
||||||
|
|
||||||
|
protection = Protection.query.first()
|
||||||
|
if not protection:
|
||||||
|
protection = Protection()
|
||||||
|
protection.set_password(password)
|
||||||
|
protection.generate_token()
|
||||||
|
|
||||||
|
db.session.add(protection)
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
protection.set_password(password)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {"status": "ok"}
|
||||||
|
case _:
|
||||||
|
logger.error("Неверный метод запроса")
|
||||||
|
return jsonify({"status": "error"}), 405
|
||||||
|
|
||||||
|
|
||||||
def init_app():
|
def init_app():
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
enable_publish_job()
|
enable_publish_job()
|
||||||
|
enable_birthdate_job()
|
||||||
logger.info("Приложение запущено")
|
logger.info("Приложение запущено")
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def check_auth_cookie():
|
||||||
|
endpoint = request.endpoint
|
||||||
|
|
||||||
|
if endpoint is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if endpoint == "login" or endpoint.startswith("static"):
|
||||||
|
return
|
||||||
|
|
||||||
|
p = Protection.query.first()
|
||||||
|
|
||||||
|
if not p or not p.verify_token(request.cookies.get("auth_token")):
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init_app()
|
init_app()
|
||||||
app.run(host="0.0.0.0", port=80)
|
app.run(debug=True, host="0.0.0.0", port=80)
|
||||||
|
# app.run(host="0.0.0.0", port=80)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import secrets
|
||||||
|
from argon2 import PasswordHasher
|
||||||
|
from argon2.exceptions import VerifyMismatchError
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
ph = PasswordHasher()
|
||||||
|
|
||||||
|
|
||||||
class MedodsAPI(db.Model):
|
class MedodsAPI(db.Model):
|
||||||
@@ -67,6 +71,66 @@ class VkAPI(db.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UsersBirthdate(db.Model):
|
||||||
|
__tablename__ = "users_birthdate"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
enabled = db.Column(db.Boolean, default=True)
|
||||||
|
name = db.Column(db.Text)
|
||||||
|
short_name = db.Column(db.Text)
|
||||||
|
sex = db.Column(db.Text)
|
||||||
|
birthdate = db.Column(db.Date)
|
||||||
|
photo_link = db.Column(db.Text, nullable=True)
|
||||||
|
congratulations = db.Column(db.Text, nullable=True)
|
||||||
|
specialties = db.Column(db.JSON)
|
||||||
|
post_link = db.Column(db.Text, nullable=True)
|
||||||
|
publish_at = db.Column(db.DateTime, nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"name": self.name,
|
||||||
|
"shortName": self.short_name,
|
||||||
|
"sex": self.sex,
|
||||||
|
"birthdate": self.birthdate.strftime("%Y-%m-%d"),
|
||||||
|
"photoLink": self.photo_link,
|
||||||
|
"congratulations": self.congratulations,
|
||||||
|
"specialties": self.specialties,
|
||||||
|
"postLink": self.post_link,
|
||||||
|
"publishAt": (
|
||||||
|
self.publish_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if self.publish_at
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BirthdateScheduler(db.Model):
|
||||||
|
__tablename__ = "birthdate_scheduler"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
hour = db.Column(db.Integer)
|
||||||
|
minute = db.Column(db.Integer)
|
||||||
|
enabled = db.Column(db.Boolean)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"hour": self.hour,
|
||||||
|
"minute": self.minute,
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UsersMedods(db.Model):
|
class UsersMedods(db.Model):
|
||||||
__tablename__ = "users_medods"
|
__tablename__ = "users_medods"
|
||||||
|
|
||||||
@@ -138,3 +202,47 @@ class PostScheduler(db.Model):
|
|||||||
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Protection(db.Model):
|
||||||
|
__tablename__ = "protection"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
password = db.Column(db.Text, nullable=False) # hash
|
||||||
|
token = db.Column(db.Text, nullable=False)
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Пароль
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def set_password(self, raw_password: str) -> None:
|
||||||
|
"""
|
||||||
|
Хэширует и сохраняет пароль (Argon2)
|
||||||
|
"""
|
||||||
|
self.password = ph.hash(raw_password)
|
||||||
|
|
||||||
|
def verify_password(self, raw_password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет пароль
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return ph.verify(self.password, raw_password)
|
||||||
|
except VerifyMismatchError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Token
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
def generate_token(self) -> str:
|
||||||
|
"""
|
||||||
|
Генерирует новый token и сохраняет его
|
||||||
|
"""
|
||||||
|
self.token = secrets.token_urlsafe(32)
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def verify_token(self, token: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет token
|
||||||
|
"""
|
||||||
|
return bool(token) and secrets.compare_digest(self.token, token)
|
||||||
|
|||||||
+76
-1
@@ -1,8 +1,83 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from db import ApiEndpoint, MedodsAPI, UsersMedods, VkPost, db
|
from db import ApiEndpoint, MedodsAPI, UsersBirthdate, UsersMedods, VkPost, db
|
||||||
from http_client import send_request
|
from http_client import send_request
|
||||||
|
|
||||||
|
|
||||||
|
def updateUsersBirthdate() -> bool:
|
||||||
|
from app import logger
|
||||||
|
|
||||||
|
try:
|
||||||
|
medodsApi = MedodsAPI.query.first()
|
||||||
|
if not medodsApi:
|
||||||
|
return False
|
||||||
|
requestParams = ApiEndpoint.query.filter_by(title="Список сотрудников").first()
|
||||||
|
if not requestParams:
|
||||||
|
return False
|
||||||
|
response = send_request(
|
||||||
|
requestParams.method,
|
||||||
|
f"{medodsApi.url}{requestParams.url_path}",
|
||||||
|
params=requestParams.query_params,
|
||||||
|
)
|
||||||
|
if not response:
|
||||||
|
logger.error("Ответ не получен")
|
||||||
|
return False
|
||||||
|
responseData = response.json()
|
||||||
|
usersFromDB = []
|
||||||
|
for user in responseData["data"]:
|
||||||
|
if user["birthdate"] is None:
|
||||||
|
continue
|
||||||
|
userDict = {
|
||||||
|
"id": int(user["id"]),
|
||||||
|
"name": f"{user['surname']} {user['name']} {user['secondName']}",
|
||||||
|
"short_name": f"{user['surname']} {user['name'][:1]}. {user['secondName'][:1]}.",
|
||||||
|
"sex": user["sex"],
|
||||||
|
"birthdate": datetime.date.fromisoformat(user["birthdate"]),
|
||||||
|
"specialties": [spec["title"] for spec in user["specialties"]],
|
||||||
|
}
|
||||||
|
usersFromDB.append(userDict)
|
||||||
|
actualUsersIds = [user["id"] for user in usersFromDB]
|
||||||
|
allExistingUsers = UsersBirthdate.query.all()
|
||||||
|
for user in allExistingUsers:
|
||||||
|
if user.id not in actualUsersIds:
|
||||||
|
logger.info(f"Удален сотрудник {user.name} {user.surname}")
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
for user in usersFromDB:
|
||||||
|
existingUser = UsersMedods.query.filter_by(id=user["id"]).first()
|
||||||
|
if existingUser:
|
||||||
|
changes = False
|
||||||
|
if existingUser.name != user["name"]:
|
||||||
|
existingUser.name = user["name"]
|
||||||
|
existingUser.short_name = user["short_name"]
|
||||||
|
changes = True
|
||||||
|
if existingUser.birthdate != user["birthdate"]:
|
||||||
|
existingUser.birthdate = user["birthdate"]
|
||||||
|
changes = True
|
||||||
|
if existingUser.specialties != user["specialties"]:
|
||||||
|
existingUser.specialties = user["specialties"]
|
||||||
|
changes = True
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
db.session.commit()
|
||||||
|
else:
|
||||||
|
newUser = UsersMedods(
|
||||||
|
id=user["id"],
|
||||||
|
name=user["name"],
|
||||||
|
short_name=user["short_name"],
|
||||||
|
sex=user["sex"],
|
||||||
|
birthdate=user["birthdate"],
|
||||||
|
specialties=user["specialties"],
|
||||||
|
)
|
||||||
|
db.session.add(newUser)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при обновлении списка сотрудников: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def updateMedodsUsers() -> bool:
|
def updateMedodsUsers() -> bool:
|
||||||
from app import logger
|
from app import logger
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"apscheduler>=3.11.1",
|
"apscheduler>=3.11.1",
|
||||||
|
"argon2-cffi>=25.1.0",
|
||||||
"flask>=3.1.2",
|
"flask>=3.1.2",
|
||||||
"flask-sqlalchemy>=3.1.1",
|
"flask-sqlalchemy>=3.1.1",
|
||||||
"pyjwt>=2.10.1",
|
"pyjwt>=2.10.1",
|
||||||
|
|||||||
+61
-1
@@ -1,7 +1,7 @@
|
|||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
from db import PostScheduler
|
from db import BirthdateScheduler, PostScheduler
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Flask app (будет установлен из app.py)
|
# Flask app (будет установлен из app.py)
|
||||||
@@ -13,6 +13,7 @@ flask_app = None
|
|||||||
# =========================
|
# =========================
|
||||||
scheduler: BackgroundScheduler | None = None
|
scheduler: BackgroundScheduler | None = None
|
||||||
JOB_ID = "vk_publish_job"
|
JOB_ID = "vk_publish_job"
|
||||||
|
BIRTHDATE_JOB_ID = "vk_birthdate_job"
|
||||||
|
|
||||||
|
|
||||||
def clearLog():
|
def clearLog():
|
||||||
@@ -60,6 +61,19 @@ def vk_publish_job():
|
|||||||
handle_vk_post()
|
handle_vk_post()
|
||||||
|
|
||||||
|
|
||||||
|
def vk_birthdate_job():
|
||||||
|
"""
|
||||||
|
Обёртка для APScheduler
|
||||||
|
"""
|
||||||
|
if flask_app is None:
|
||||||
|
raise RuntimeError("Scheduler is not initialized with Flask app")
|
||||||
|
|
||||||
|
from vk_handler import handle_vk_birthdate
|
||||||
|
|
||||||
|
with flask_app.app_context():
|
||||||
|
handle_vk_birthdate()
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Enable job
|
# Enable job
|
||||||
# =========================
|
# =========================
|
||||||
@@ -89,6 +103,32 @@ def enable_publish_job():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_birthdate_job():
|
||||||
|
"""
|
||||||
|
Включает выполнение публикации постов
|
||||||
|
"""
|
||||||
|
if not scheduler:
|
||||||
|
return
|
||||||
|
|
||||||
|
scheduleData = BirthdateScheduler.query.first()
|
||||||
|
if not scheduleData or not scheduleData.enabled:
|
||||||
|
disable_birthdate_job()
|
||||||
|
return
|
||||||
|
|
||||||
|
trigger = CronTrigger(
|
||||||
|
hour=f"{scheduleData.hour}",
|
||||||
|
minute=f"{scheduleData.minute}",
|
||||||
|
day="*",
|
||||||
|
)
|
||||||
|
|
||||||
|
scheduler.add_job(
|
||||||
|
vk_birthdate_job,
|
||||||
|
trigger=trigger,
|
||||||
|
id=BIRTHDATE_JOB_ID,
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Disable job
|
# Disable job
|
||||||
# =========================
|
# =========================
|
||||||
@@ -97,6 +137,11 @@ def disable_publish_job():
|
|||||||
scheduler.remove_job(JOB_ID)
|
scheduler.remove_job(JOB_ID)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_birthdate_job():
|
||||||
|
if scheduler and scheduler.get_job(BIRTHDATE_JOB_ID):
|
||||||
|
scheduler.remove_job(BIRTHDATE_JOB_ID)
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Status
|
# Status
|
||||||
# =========================
|
# =========================
|
||||||
@@ -113,3 +158,18 @@ def get_scheduler_status() -> dict:
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_birthdate_scheduler_status() -> dict:
|
||||||
|
scheduler_running = bool(scheduler and scheduler.running)
|
||||||
|
job = scheduler.get_job(BIRTHDATE_JOB_ID) if scheduler_running else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"scheduler": scheduler_running,
|
||||||
|
"vk_birthdate_job": job is not None,
|
||||||
|
"next_run_time": (
|
||||||
|
job.next_run_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if job and job.next_run_time
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
|
||||||
|
/* Цвет для темы дней рождения */
|
||||||
|
.bg-pink {
|
||||||
|
background-color: #e83e8c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-pink {
|
||||||
|
color: #e83e8c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Карточки */
|
||||||
|
.card {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таблица сотрудников */
|
||||||
|
.table-responsive {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
z-index: 10;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: rgba(232, 62, 140, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr.selected {
|
||||||
|
background-color: rgba(232, 62, 140, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконки статуса */
|
||||||
|
.status-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-enabled {
|
||||||
|
background-color: rgba(25, 135, 84, 0.1);
|
||||||
|
color: #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disabled {
|
||||||
|
background-color: rgba(108, 117, 125, 0.1);
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-data {
|
||||||
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-nodata {
|
||||||
|
background-color: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Бейджи для специализаций */
|
||||||
|
.specialty-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
margin: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Поля ввода */
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: #e83e8c;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(232, 62, 140, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Свитч */
|
||||||
|
.form-switch .form-check-input {
|
||||||
|
width: 3em;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch .form-check-input:checked {
|
||||||
|
background-color: #e83e8c;
|
||||||
|
border-color: #e83e8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопки */
|
||||||
|
.btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary:hover {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Дата рождения */
|
||||||
|
.birthdate-cell {
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, 'Courier New', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 0.2em 0.6em;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Пол */
|
||||||
|
.sex-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sex-male {
|
||||||
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sex-female {
|
||||||
|
background-color: rgba(232, 62, 140, 0.1);
|
||||||
|
color: #e83e8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Уведомления */
|
||||||
|
.alert-fixed {
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1050;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Модальное окно */
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Анимации */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.table-responsive {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specialty-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Пустая таблица */
|
||||||
|
.text-center.py-5 {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Информация о посте */
|
||||||
|
#postInfo .alert {
|
||||||
|
border-left: 4px solid #0dcaf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконки в таблице */
|
||||||
|
.icon-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Месяц и дата рождения */
|
||||||
|
.month-day {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Зодиак (опционально) */
|
||||||
|
.zodiac-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скроллбар */
|
||||||
|
.table-responsive::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
function deleteAuthToken() {
|
||||||
|
document.cookie = "auth_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT;";
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательные функции для уведомлений
|
||||||
|
function showAlert(type, message) {
|
||||||
|
const alertContainer = document.getElementById('alertContainer');
|
||||||
|
|
||||||
|
// Очищаем старые алерты
|
||||||
|
alertContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Создаем новый алерт
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
|
||||||
|
|
||||||
|
// Иконка для типа алерта
|
||||||
|
const icon = getAlertIcon(type);
|
||||||
|
|
||||||
|
alert.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="bi ${icon} me-2 fs-5"></i>
|
||||||
|
<div class="flex-grow-1">${message}</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
alertContainer.appendChild(alert);
|
||||||
|
|
||||||
|
// Автоматическое удаление через 5 секунд
|
||||||
|
setTimeout(() => {
|
||||||
|
if (alert.parentNode) {
|
||||||
|
alert.classList.remove('show');
|
||||||
|
setTimeout(() => alert.remove(), 150);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlertIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
'success': 'bi-check-circle-fill',
|
||||||
|
'warning': 'bi-exclamation-triangle-fill',
|
||||||
|
'danger': 'bi-x-circle-fill',
|
||||||
|
'info': 'bi-info-circle-fill'
|
||||||
|
};
|
||||||
|
return icons[type] || 'bi-info-circle-fill';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("changePasswordForm").addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault(); // ❌ перезагрузка страницы
|
||||||
|
|
||||||
|
const passwordInput = document.getElementById("newPassword");
|
||||||
|
const newPassword = passwordInput.value.trim();
|
||||||
|
|
||||||
|
if (!newPassword) {
|
||||||
|
showAlert("warning", "Введите новый пароль");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/login", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password: newPassword })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "ok") {
|
||||||
|
showAlert("success", "Пароль успешно изменён");
|
||||||
|
|
||||||
|
// очистить поле
|
||||||
|
passwordInput.value = "";
|
||||||
|
|
||||||
|
// закрыть dropdown
|
||||||
|
const btn = document.getElementById("changePasswordBtn");
|
||||||
|
const dropdown = bootstrap.Dropdown.getOrCreateInstance(btn);
|
||||||
|
dropdown.hide();
|
||||||
|
} else {
|
||||||
|
showAlert("danger", data.errorMessage || "Ошибка изменения пароля");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showAlert("danger", "Ошибка соединения с сервером");
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
// Глобальные переменные
|
||||||
|
let usersData = [];
|
||||||
|
let selectedUserId = null;
|
||||||
|
let selectedUserName = '';
|
||||||
|
let userFormChanged = false;
|
||||||
|
let schedulerFormChanged = false;
|
||||||
|
let originalUserData = null;
|
||||||
|
let originalSchedulerData = null;
|
||||||
|
let pendingUserSwitch = null;
|
||||||
|
|
||||||
|
// Инициализация при загрузке страницы
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Загружаем список сотрудников
|
||||||
|
loadUsersList();
|
||||||
|
|
||||||
|
// Сохраняем оригинальные настройки планировщика
|
||||||
|
saveOriginalSchedulerData();
|
||||||
|
|
||||||
|
// Устанавливаем обработчики событий
|
||||||
|
setupEventListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Загрузка списка сотрудников
|
||||||
|
async function loadUsersList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/birthdate');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'ok') {
|
||||||
|
usersData = data.users;
|
||||||
|
renderUsersTable();
|
||||||
|
} else {
|
||||||
|
showAlert('danger', 'Ошибка загрузки списка сотрудников');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
showAlert('danger', 'Ошибка загрузки списка сотрудников');
|
||||||
|
renderUsersTable(); // Рендерим пустую таблицу
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображение таблицы сотрудников
|
||||||
|
function renderUsersTable() {
|
||||||
|
const tbody = document.getElementById('usersTableBody');
|
||||||
|
|
||||||
|
if (!usersData || usersData.length === 0) {
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-5">
|
||||||
|
<i class="bi bi-people display-1 text-muted mb-3 d-block"></i>
|
||||||
|
<h5 class="text-muted">Нет данных о сотрудниках</h5>
|
||||||
|
<p class="text-muted">Нажмите "Обновить список" для загрузки данных</p>
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="refreshUsersList()">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i>Обновить список
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем по дате рождения (месяц и день)
|
||||||
|
usersData.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.birthdate);
|
||||||
|
const dateB = new Date(b.birthdate);
|
||||||
|
|
||||||
|
// Извлекаем месяц и день
|
||||||
|
const monthA = dateA.getMonth();
|
||||||
|
const dayA = dateA.getDate();
|
||||||
|
const monthB = dateB.getMonth();
|
||||||
|
const dayB = dateB.getDate();
|
||||||
|
|
||||||
|
// Сначала сравниваем месяцы, потом дни
|
||||||
|
return monthA !== monthB ? monthA - monthB : dayA - dayB;
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
usersData.forEach(user => {
|
||||||
|
const birthdate = new Date(user.birthdate);
|
||||||
|
const now = new Date();
|
||||||
|
const age = now.getFullYear() - birthdate.getFullYear();
|
||||||
|
const month = birthdate.getMonth() + 1;
|
||||||
|
const day = birthdate.getDate();
|
||||||
|
const fullDate = birthdate.toLocaleDateString('ru-RU');
|
||||||
|
|
||||||
|
// Форматируем месяц и день (двузначные)
|
||||||
|
const monthStr = month.toString().padStart(2, '0');
|
||||||
|
const dayStr = day.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
// Определяем пол
|
||||||
|
const sexBadge = user.sex === 'male' ?
|
||||||
|
'<span class="badge sex-badge sex-male">М</span>' :
|
||||||
|
'<span class="badge sex-badge sex-female">Ж</span>';
|
||||||
|
|
||||||
|
// Специальности
|
||||||
|
const specialtiesHtml = user.specialties && user.specialties.length > 0 ?
|
||||||
|
user.specialties.map(s => `<span class="badge bg-light text-dark specialty-badge">${s}</span>`).join('') :
|
||||||
|
'<span class="text-muted">-</span>';
|
||||||
|
|
||||||
|
// Статус enabled
|
||||||
|
const enabledStatus = user.enabled ?
|
||||||
|
'<div class="status-icon status-enabled" title="Включен"><i class="bi bi-check-lg"></i></div>' :
|
||||||
|
'<div class="status-icon status-disabled" title="Отключен"><i class="bi bi-x-lg"></i></div>';
|
||||||
|
|
||||||
|
// Статус данных (фото и текст)
|
||||||
|
const hasPhoto = user.photoLink && user.photoLink.trim() !== '';
|
||||||
|
const hasText = user.congratulations && user.congratulations.trim() !== '';
|
||||||
|
const hasData = hasPhoto && hasText;
|
||||||
|
|
||||||
|
const dataStatus = hasData ?
|
||||||
|
'<div class="status-icon status-data" title="Данные для поздравления заполнены"><i class="bi bi-check-lg"></i></div>' :
|
||||||
|
'<div class="status-icon status-nodata" title="Нет данных для поздравления"><i class="bi bi-x-lg"></i></div>';
|
||||||
|
|
||||||
|
// Определяем класс строки (выделение выбранного)
|
||||||
|
const isSelected = selectedUserId === user.id ? 'selected' : '';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr class="${isSelected}" onclick="selectUser(${user.id})" data-user-id="${user.id}">
|
||||||
|
<td>
|
||||||
|
<div class="birthdate-cell">
|
||||||
|
<span class="month-day">${monthStr}.${dayStr}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="fw-semibold">${user.name}</span>
|
||||||
|
</td>
|
||||||
|
<td>${user.shortName}</td>
|
||||||
|
<td>
|
||||||
|
<div>${fullDate}</div>
|
||||||
|
<small class="age-badge">${age} лет</small>
|
||||||
|
</td>
|
||||||
|
<td>${sexBadge}</td>
|
||||||
|
<td>${specialtiesHtml}</td>
|
||||||
|
<td class="text-center">${enabledStatus}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="icon-group">${dataStatus}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выбор сотрудника
|
||||||
|
function selectUser(userId) {
|
||||||
|
// Если есть несохраненные изменения
|
||||||
|
if (userFormChanged && selectedUserId !== null) {
|
||||||
|
pendingUserSwitch = userId;
|
||||||
|
document.getElementById('modalUserName').textContent = selectedUserName;
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
||||||
|
modal.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем переключение
|
||||||
|
performUserSwitch(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполнение переключения сотрудника
|
||||||
|
function performUserSwitch(userId) {
|
||||||
|
const user = usersData.find(u => u.id === userId);
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
// Обновляем выделение в таблице
|
||||||
|
document.querySelectorAll('#usersTableBody tr').forEach(row => {
|
||||||
|
row.classList.remove('selected');
|
||||||
|
});
|
||||||
|
document.querySelector(`tr[data-user-id="${userId}"]`).classList.add('selected');
|
||||||
|
|
||||||
|
// Сохраняем данные
|
||||||
|
selectedUserId = userId;
|
||||||
|
selectedUserName = user.name;
|
||||||
|
|
||||||
|
// Обновляем заголовок
|
||||||
|
document.getElementById('selectedUserName').textContent = user.name;
|
||||||
|
|
||||||
|
// Заполняем форму
|
||||||
|
document.getElementById('userId').value = user.id;
|
||||||
|
document.getElementById('user_enabled').checked = user.enabled;
|
||||||
|
document.getElementById('photo_link').value = user.photoLink || '';
|
||||||
|
document.getElementById('congratulations').value = user.congratulations || '';
|
||||||
|
|
||||||
|
// Показываем информацию о посте, если есть
|
||||||
|
const postInfo = document.getElementById('postInfo');
|
||||||
|
const postLinkContainer = document.getElementById('postLinkContainer');
|
||||||
|
const publishTime = document.getElementById('publishTime');
|
||||||
|
|
||||||
|
if (user.postLink) {
|
||||||
|
postInfo.style.display = 'block';
|
||||||
|
postLinkContainer.innerHTML = `
|
||||||
|
<a href="${user.postLink}" target="_blank" class="btn btn-sm btn-outline-info">
|
||||||
|
<i class="bi bi-box-arrow-up-right me-1"></i>Перейти к посту в VK
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
publishTime.innerHTML = `<i class="bi bi-clock me-1"></i>Опубликовано: ${user.publishAt || 'неизвестно'}`;
|
||||||
|
} else {
|
||||||
|
postInfo.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем оригинальные данные для сравнения
|
||||||
|
saveOriginalUserData();
|
||||||
|
|
||||||
|
// Сбрасываем флаг изменений
|
||||||
|
userFormChanged = false;
|
||||||
|
updateSaveUserButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отметка изменений в форме сотрудника
|
||||||
|
function markUserChanged() {
|
||||||
|
userFormChanged = true;
|
||||||
|
updateSaveUserButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отметка изменений в форме планировщика
|
||||||
|
function markSchedulerChanged() {
|
||||||
|
schedulerFormChanged = true;
|
||||||
|
updateSaveSchedulerButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновление кнопки сохранения сотрудника
|
||||||
|
function updateSaveUserButton() {
|
||||||
|
const button = document.getElementById('saveUserButton');
|
||||||
|
button.disabled = !userFormChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновление кнопки сохранения планировщика
|
||||||
|
function updateSaveSchedulerButton() {
|
||||||
|
const button = document.getElementById('saveSchedulerButton');
|
||||||
|
button.disabled = !schedulerFormChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение оригинальных данных сотрудника
|
||||||
|
function saveOriginalUserData() {
|
||||||
|
if (!selectedUserId) return;
|
||||||
|
|
||||||
|
originalUserData = {
|
||||||
|
enabled: document.getElementById('user_enabled').checked,
|
||||||
|
photoLink: document.getElementById('photo_link').value,
|
||||||
|
congratulations: document.getElementById('congratulations').value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение оригинальных данных планировщика
|
||||||
|
function saveOriginalSchedulerData() {
|
||||||
|
originalSchedulerData = {
|
||||||
|
hour: parseInt(document.getElementById('scheduler_hour').value) || 9,
|
||||||
|
minute: parseInt(document.getElementById('scheduler_minute').value) || 0,
|
||||||
|
enabled: document.getElementById('scheduler_enabled').checked
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сброс формы сотрудника
|
||||||
|
function resetUserForm() {
|
||||||
|
if (!selectedUserId) return;
|
||||||
|
|
||||||
|
if (userFormChanged && confirm('Отменить изменения?')) {
|
||||||
|
const user = usersData.find(u => u.id === selectedUserId);
|
||||||
|
if (user) {
|
||||||
|
document.getElementById('user_enabled').checked = user.enabled;
|
||||||
|
document.getElementById('photo_link').value = user.photoLink || '';
|
||||||
|
document.getElementById('congratulations').value = user.congratulations || '';
|
||||||
|
|
||||||
|
userFormChanged = false;
|
||||||
|
updateSaveUserButton();
|
||||||
|
saveOriginalUserData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сброс формы планировщика
|
||||||
|
function resetSchedulerForm() {
|
||||||
|
if (schedulerFormChanged && confirm('Отменить изменения в настройках планировщика?')) {
|
||||||
|
if (originalSchedulerData) {
|
||||||
|
document.getElementById('scheduler_hour').value = originalSchedulerData.hour;
|
||||||
|
document.getElementById('scheduler_minute').value = originalSchedulerData.minute;
|
||||||
|
document.getElementById('scheduler_enabled').checked = originalSchedulerData.enabled;
|
||||||
|
|
||||||
|
schedulerFormChanged = false;
|
||||||
|
updateSaveSchedulerButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение данных сотрудника
|
||||||
|
async function saveUserData() {
|
||||||
|
function normalizeVkPhotoLink(link) {
|
||||||
|
if (!link) return link;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(link);
|
||||||
|
const z = url.searchParams.get('z');
|
||||||
|
|
||||||
|
if (z && z.includes('%2F')) {
|
||||||
|
url.searchParams.set('z', z.split('%2F')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// если это невалидный URL — возвращаем как есть
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!selectedUserId) {
|
||||||
|
showAlert('warning', 'Выберите сотрудника для сохранения');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
enabled: document.getElementById('user_enabled').checked,
|
||||||
|
photoLink: normalizeVkPhotoLink(
|
||||||
|
document.getElementById('photo_link').value.trim()
|
||||||
|
),
|
||||||
|
congratulations: document.getElementById('congratulations').value.trim()
|
||||||
|
};
|
||||||
|
// Проверка длины поздравления
|
||||||
|
if (userData.congratulations.length > 2000) {
|
||||||
|
showAlert('warning', 'Длина поздравления не должна превышать 2000 символов, сейчас - ' + userData.congratulations.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка: если фото или текст заполнены, оба должны быть заполнены
|
||||||
|
if ((userData.photoLink && !userData.congratulations) || (!userData.photoLink && userData.congratulations)) {
|
||||||
|
showAlert('warning', 'Для поздравления должны быть заполнены и фото, и текст');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postData = {
|
||||||
|
userUpdate: {
|
||||||
|
userId: selectedUserId,
|
||||||
|
userData: userData
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/birthdate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(postData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.status === 'ok') {
|
||||||
|
showAlert('success', 'Данные сотрудника сохранены!');
|
||||||
|
|
||||||
|
// Обновляем данные в локальном массиве
|
||||||
|
const userIndex = usersData.findIndex(u => u.id === selectedUserId);
|
||||||
|
if (userIndex !== -1) {
|
||||||
|
usersData[userIndex].enabled = userData.enabled;
|
||||||
|
usersData[userIndex].photoLink = userData.photoLink;
|
||||||
|
usersData[userIndex].congratulations = userData.congratulations;
|
||||||
|
|
||||||
|
// Перерисовываем таблицу
|
||||||
|
renderUsersTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбрасываем флаг изменений
|
||||||
|
userFormChanged = false;
|
||||||
|
updateSaveUserButton();
|
||||||
|
saveOriginalUserData();
|
||||||
|
} else {
|
||||||
|
showAlert('danger', 'Ошибка сохранения данных сотрудника');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
showAlert('danger', 'Ошибка сохранения данных сотрудника');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение настроек планировщика
|
||||||
|
async function saveSchedulerSettings() {
|
||||||
|
const scheduleSettings = {
|
||||||
|
hour: parseInt(document.getElementById('scheduler_hour').value),
|
||||||
|
minute: parseInt(document.getElementById('scheduler_minute').value),
|
||||||
|
enabled: document.getElementById('scheduler_enabled').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
// Валидация
|
||||||
|
if (scheduleSettings.hour < 0 || scheduleSettings.hour > 23) {
|
||||||
|
showAlert('warning', 'Час должен быть от 0 до 23');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleSettings.minute < 0 || scheduleSettings.minute > 59) {
|
||||||
|
showAlert('warning', 'Минута должна быть от 0 до 59');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postData = {
|
||||||
|
scheduleSettings: scheduleSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/birthdate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(postData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.status === 'ok') {
|
||||||
|
showAlert('success', 'Настройки планировщика сохранены!');
|
||||||
|
|
||||||
|
// Обновляем информацию о следующем запуске
|
||||||
|
if (data.next_run_time) {
|
||||||
|
const schedulerStatus = document.getElementById('schedulerStatus');
|
||||||
|
schedulerStatus.innerHTML = `<span class="badge bg-success"><i class="bi bi-play-circle me-1"></i>Активен</span>`;
|
||||||
|
|
||||||
|
// Обновляем отображение следующего запуска (если есть соответствующий элемент)
|
||||||
|
const nextRunAlert = document.querySelector('.alert-success');
|
||||||
|
if (nextRunAlert) {
|
||||||
|
nextRunAlert.querySelector('p').textContent = data.next_run_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбрасываем флаг изменений
|
||||||
|
schedulerFormChanged = false;
|
||||||
|
updateSaveSchedulerButton();
|
||||||
|
saveOriginalSchedulerData();
|
||||||
|
} else {
|
||||||
|
showAlert('danger', 'Ошибка сохранения настроек планировщика');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
showAlert('danger', 'Ошибка сохранения настроек планировщика');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновление списка сотрудников
|
||||||
|
async function refreshUsersList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/birthdate?action=update', {
|
||||||
|
method: 'PATCH'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.status === 'ok') {
|
||||||
|
showAlert('success', 'Список сотрудников обновлен!');
|
||||||
|
// Перезагружаем список
|
||||||
|
await loadUsersList();
|
||||||
|
} else {
|
||||||
|
showAlert('danger', 'Ошибка обновления списка сотрудников');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
showAlert('danger', 'Ошибка обновления списка сотрудников');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Включить всех сотрудников
|
||||||
|
async function enableAllUsers() {
|
||||||
|
if (!confirm('Включить поздравления для всех сотрудников?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Находим всех сотрудников без данных
|
||||||
|
const usersWithoutData = usersData.filter(u =>
|
||||||
|
!u.enabled || !u.photoLink || !u.congratulations
|
||||||
|
);
|
||||||
|
|
||||||
|
if (usersWithoutData.length === 0) {
|
||||||
|
showAlert('info', 'Все сотрудники уже включены');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем уведомление о необходимости заполнения данных
|
||||||
|
showAlert('info', `Включено ${usersWithoutData.length} сотрудников. Не забудьте заполнить данные для поздравлений.`);
|
||||||
|
|
||||||
|
// Обновляем статус в локальном массиве
|
||||||
|
usersData.forEach(user => {
|
||||||
|
user.enabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Перерисовываем таблицу
|
||||||
|
renderUsersTable();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
showAlert('danger', 'Ошибка включения сотрудников');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отключить всех сотрудников
|
||||||
|
async function disableAllUsers() {
|
||||||
|
if (!confirm('Отключить поздравления для всех сотрудников?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Обновляем статус в локальном массиве
|
||||||
|
usersData.forEach(user => {
|
||||||
|
user.enabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Перерисовываем таблицу
|
||||||
|
renderUsersTable();
|
||||||
|
|
||||||
|
showAlert('success', 'Все сотрудники отключены');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
showAlert('danger', 'Ошибка отключения сотрудников');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчики событий для модального окна
|
||||||
|
function cancelSwitchUser() {
|
||||||
|
pendingUserSwitch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function discardUserChanges() {
|
||||||
|
userFormChanged = false;
|
||||||
|
updateSaveUserButton();
|
||||||
|
if (pendingUserSwitch) {
|
||||||
|
performUserSwitch(pendingUserSwitch);
|
||||||
|
pendingUserSwitch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAndSwitchUser() {
|
||||||
|
if (pendingUserSwitch) {
|
||||||
|
saveUserData().then(() => {
|
||||||
|
if (!userFormChanged) {
|
||||||
|
performUserSwitch(pendingUserSwitch);
|
||||||
|
pendingUserSwitch = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройка обработчиков событий
|
||||||
|
function setupEventListeners() {
|
||||||
|
// Валидация полей времени
|
||||||
|
document.getElementById('scheduler_hour').addEventListener('input', function () {
|
||||||
|
let value = parseInt(this.value);
|
||||||
|
if (value < 0) this.value = 0;
|
||||||
|
if (value > 23) this.value = 23;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('scheduler_minute').addEventListener('input', function () {
|
||||||
|
let value = parseInt(this.value);
|
||||||
|
if (value < 0) this.value = 0;
|
||||||
|
if (value > 59) this.value = 59;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Предотвращение закрытия страницы при несохраненных изменениях
|
||||||
|
window.addEventListener('beforeunload', function (e) {
|
||||||
|
if (userFormChanged || schedulerFormChanged) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -127,14 +127,3 @@ async function refreshLogs() {
|
|||||||
console.error('Ошибка при обновлении логов:', error);
|
console.error('Ошибка при обновлении логов:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getAlertIcon(type) {
|
|
||||||
const icons = {
|
|
||||||
'success': 'bi-check-circle-fill',
|
|
||||||
'warning': 'bi-exclamation-triangle-fill',
|
|
||||||
'danger': 'bi-x-circle-fill',
|
|
||||||
'info': 'bi-info-circle-fill'
|
|
||||||
};
|
|
||||||
return icons[type] || 'bi-info-circle-fill';
|
|
||||||
}
|
|
||||||
@@ -618,44 +618,6 @@ function downloadResponse() {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции
|
|
||||||
function showAlert(type, message) {
|
|
||||||
const alertContainer = document.getElementById('alertContainer');
|
|
||||||
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
|
|
||||||
alert.style.minWidth = '300px';
|
|
||||||
alert.style.maxWidth = '400px';
|
|
||||||
|
|
||||||
alert.innerHTML = `
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<i class="bi ${getAlertIcon(type)} me-2"></i>
|
|
||||||
<div class="flex-grow-1">${message}</div>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
alertContainer.appendChild(alert);
|
|
||||||
|
|
||||||
// Автоматическое удаление через 4 секунды
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.classList.remove('show');
|
|
||||||
setTimeout(() => alert.remove(), 150);
|
|
||||||
}
|
|
||||||
}, 4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlertIcon(type) {
|
|
||||||
const icons = {
|
|
||||||
'success': 'bi-check-circle-fill',
|
|
||||||
'warning': 'bi-exclamation-triangle-fill',
|
|
||||||
'danger': 'bi-x-circle-fill',
|
|
||||||
'info': 'bi-info-circle-fill'
|
|
||||||
};
|
|
||||||
return icons[type] || 'bi-info-circle-fill';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoader(show) {
|
function showLoader(show) {
|
||||||
const executeButton = document.getElementById('executeButton');
|
const executeButton = document.getElementById('executeButton');
|
||||||
if (show) {
|
if (show) {
|
||||||
|
|||||||
@@ -305,46 +305,3 @@ async function updateSchedulerStatus() {
|
|||||||
console.error('Ошибка обновления статуса:', error);
|
console.error('Ошибка обновления статуса:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции для уведомлений
|
|
||||||
function showAlert(type, message) {
|
|
||||||
const alertContainer = document.getElementById('alertContainer');
|
|
||||||
|
|
||||||
// Очищаем старые алерты
|
|
||||||
alertContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// Создаем новый алерт
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
|
|
||||||
|
|
||||||
// Иконка для типа алерта
|
|
||||||
const icon = getAlertIcon(type);
|
|
||||||
|
|
||||||
alert.innerHTML = `
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<i class="bi ${icon} me-2 fs-5"></i>
|
|
||||||
<div class="flex-grow-1">${message}</div>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
alertContainer.appendChild(alert);
|
|
||||||
|
|
||||||
// Автоматическое удаление через 5 секунд
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.classList.remove('show');
|
|
||||||
setTimeout(() => alert.remove(), 150);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlertIcon(type) {
|
|
||||||
const icons = {
|
|
||||||
'success': 'bi-check-circle-fill',
|
|
||||||
'warning': 'bi-exclamation-triangle-fill',
|
|
||||||
'danger': 'bi-x-circle-fill',
|
|
||||||
'info': 'bi-info-circle-fill'
|
|
||||||
};
|
|
||||||
return icons[type] || 'bi-info-circle-fill';
|
|
||||||
}
|
|
||||||
@@ -101,46 +101,6 @@ async function saveVkSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции для уведомлений
|
|
||||||
function showAlert(type, message) {
|
|
||||||
const alertContainer = document.getElementById('alertContainer');
|
|
||||||
|
|
||||||
// Создаем алерт
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = `alert alert-${type} alert-dismissible fade show shadow`;
|
|
||||||
|
|
||||||
// Иконка для типа алерта
|
|
||||||
const icon = getAlertIcon(type);
|
|
||||||
|
|
||||||
alert.innerHTML = `
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<i class="bi ${icon} me-2 fs-5"></i>
|
|
||||||
<div class="flex-grow-1">${message}</div>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
alertContainer.appendChild(alert);
|
|
||||||
|
|
||||||
// Автоматическое удаление через 5 секунд
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.classList.remove('show');
|
|
||||||
setTimeout(() => alert.remove(), 150);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlertIcon(type) {
|
|
||||||
const icons = {
|
|
||||||
'success': 'bi-check-circle-fill',
|
|
||||||
'warning': 'bi-exclamation-triangle-fill',
|
|
||||||
'danger': 'bi-x-circle-fill',
|
|
||||||
'info': 'bi-info-circle-fill'
|
|
||||||
};
|
|
||||||
return icons[type] || 'bi-info-circle-fill';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Дополнительная проверка при вводе данных
|
// Дополнительная проверка при вводе данных
|
||||||
document.getElementById('group_id').addEventListener('input', function (e) {
|
document.getElementById('group_id').addEventListener('input', function (e) {
|
||||||
this.value = this.value.replace(/[^\d]/g, '');
|
this.value = this.value.replace(/[^\d]/g, '');
|
||||||
|
|||||||
+40
-4
@@ -34,29 +34,62 @@
|
|||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">
|
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">
|
||||||
<i class="bi bi-speedometer2 me-1"></i> Главная
|
<i class="bi bi-speedometer2 me-2"></i>Главная
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.path.startswith('/medods') %}active{% endif %}" href="/medods">
|
<a class="nav-link {% if request.path.startswith('/medods') %}active{% endif %}" href="/medods">
|
||||||
<i class="bi bi-diagram-3 me-1"></i> Medods
|
<i class="bi bi-plug me-2"></i>Medods
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.path.startswith('/vk') %}active{% endif %}" href="/vk">
|
<a class="nav-link {% if request.path.startswith('/vk') %}active{% endif %}" href="/vk">
|
||||||
<i class="bi bi-chat-dots me-1"></i> VK
|
<i class="bi bi-chat-dots me-2"></i>VK
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.path.startswith('/posts') %}active{% endif %}" href="/posts">
|
<a class="nav-link {% if request.path.startswith('/posts') %}active{% endif %}" href="/posts">
|
||||||
<i class="bi bi-file-earmark-text me-1"></i> Посты
|
<i class="bi bi-file-earmark-text me-2"></i>Посты
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.path.startswith('/birthdate') %}active{% endif %}"
|
||||||
|
href="/birthdate">
|
||||||
|
<i class="bi bi-balloon-heart-fill text-pink me-2"></i>Дни рождения
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div class="dropstart">
|
||||||
|
<button type="button" class="btn btn-danger dropdown-toggle" id="changePasswordBtn"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
|
||||||
|
<i class="bi bi-shield-lock-fill me-1"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu p-4" style="width: 200px;">
|
||||||
|
<button type="button" class="btn btn-danger" onclick="deleteAuthToken()">
|
||||||
|
<i class="bi bi-box-arrow-right me-1"></i>Выйти
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form id="changePasswordForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newPassword" class="form-label">Новый пароль</label>
|
||||||
|
<input type="password" class="form-control" id="newPassword" placeholder="Новый пароль"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-outline-danger"><i
|
||||||
|
class="bi bi-key-fill me-1"></i>Изменить</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -64,8 +97,11 @@
|
|||||||
<main class="container-fluid pt-2">
|
<main class="container-fluid pt-2">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
<!-- Контейнер для уведомлений -->
|
||||||
|
<div id="alertContainer" class="alert-fixed"></div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/static/js/base.js"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Дни рождения{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link href="/static/css/birthdate.css" rel="stylesheet">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Заголовок -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-1"><i class="bi bi-balloon-heart-fill text-pink me-2"></i>Дни рождения</h2>
|
||||||
|
<p class="text-muted mb-0">Управление поздравлениями сотрудников</p>
|
||||||
|
</div>
|
||||||
|
<div class="badge bg-pink fs-6 px-3 py-2">
|
||||||
|
<i class="bi bi-calendar-heart me-1"></i>Поздравления
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Основной контент -->
|
||||||
|
<div class="row g-4 mb-3">
|
||||||
|
<!-- Левая колонка: Список сотрудников -->
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-people-fill me-2"></i>Сотрудники</h5>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshUsersList()">
|
||||||
|
<i class="bi bi-arrow-clockwise me-1"></i>Обновить список
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<!-- Таблица сотрудников -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0" id="usersTable">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th width="120">Дата рождения</th>
|
||||||
|
<th>Имя</th>
|
||||||
|
<th>Короткое имя</th>
|
||||||
|
<th>Полная дата<br><small>(возраст)</small></th>
|
||||||
|
<th>Пол</th>
|
||||||
|
<th>Специальности</th>
|
||||||
|
<th width="100" class="text-center">Статус</th>
|
||||||
|
<th width="80" class="text-center">Данные</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="usersTableBody">
|
||||||
|
<!-- Данные будут загружены через JS -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success w-100" onclick="enableAllUsers()">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Включить всех
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary w-100"
|
||||||
|
onclick="disableAllUsers()">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Отключить всех
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Правая колонка: Данные сотрудника и планировщик -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<!-- Данные выбранного сотрудника -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-person-badge me-2"></i>
|
||||||
|
<span id="selectedUserName">Выберите сотрудника</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="userForm">
|
||||||
|
<input type="hidden" id="userId">
|
||||||
|
|
||||||
|
<!-- Переключатель enabled -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check form-switch d-flex align-items-center gap-2 p-0">
|
||||||
|
<input class="form-check-input m-0" type="checkbox" role="switch" id="user_enabled"
|
||||||
|
onchange="markUserChanged()">
|
||||||
|
<label class="form-check-label fw-semibold mb-0" for="user_enabled">
|
||||||
|
Включить поздравление для этого сотрудника
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ссылка на фото -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">
|
||||||
|
<i class="bi bi-image me-2"></i>Ссылка на фото для поздравления
|
||||||
|
</label>
|
||||||
|
<input type="url" class="form-control" id="photo_link"
|
||||||
|
placeholder="https://vk.com/groupname?z=photo-27937673_457248154"
|
||||||
|
oninput="markUserChanged()">
|
||||||
|
<div class="form-text">
|
||||||
|
Ссылка на изображение для поздравления. Должна быть доступна для всех. Лишние символы в
|
||||||
|
ссылке будут удалены автоматически.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Текст поздравления -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">
|
||||||
|
<i class="bi bi-chat-heart me-2"></i>Текст поздравления
|
||||||
|
</label>
|
||||||
|
<textarea class="form-control" id="congratulations" rows="5"
|
||||||
|
placeholder="Дорогой(ая) [Имя], поздравляем с днем рождения! 🎉"
|
||||||
|
oninput="markUserChanged()"></textarea>
|
||||||
|
<div class="form-text">
|
||||||
|
Не поддерживается никакая разметка. Только текст и эмоджи. Максимум 2000 символов.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Информация о посте -->
|
||||||
|
<div id="postInfo" style="display: none;">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading"><i class="bi bi-info-circle me-2"></i>Информация о посте</h6>
|
||||||
|
<div class="mb-2" id="postLinkContainer">
|
||||||
|
<!-- Ссылка на пост будет здесь -->
|
||||||
|
</div>
|
||||||
|
<div class="small" id="publishTime">
|
||||||
|
<!-- Время публикации будет здесь -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка сохранения -->
|
||||||
|
<div class="d-flex gap-2 pt-3 border-top">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="resetUserForm()">
|
||||||
|
<i class="bi bi-x-lg me-1"></i>Отмена
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="saveUserButton" onclick="saveUserData()"
|
||||||
|
disabled>
|
||||||
|
<i class="bi bi-save me-1"></i>Сохранить данные сотрудника
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Настройки планировщика -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-clock me-2"></i>Настройки планировщика</h5>
|
||||||
|
<span id="schedulerStatus" class="badge">
|
||||||
|
{% if data.schedulerStatus.scheduler %}
|
||||||
|
<span class="badge bg-success"><i class="bi bi-play-circle me-1"></i>Активен</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary"><i class="bi bi-stop-circle me-1"></i>Неактивен</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="schedulerForm">
|
||||||
|
<!-- Время запуска -->
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Час запуска</label>
|
||||||
|
<input type="number" class="form-control" id="scheduler_hour" min="0" max="23"
|
||||||
|
placeholder="9"
|
||||||
|
value="{{ data.schedulerSettings.hour if data.schedulerSettings else 9 }}"
|
||||||
|
oninput="markSchedulerChanged()">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Минута запуска</label>
|
||||||
|
<input type="number" class="form-control" id="scheduler_minute" min="0" max="59"
|
||||||
|
placeholder="0"
|
||||||
|
value="{{ data.schedulerSettings.minute if data.schedulerSettings else 0 }}"
|
||||||
|
oninput="markSchedulerChanged()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Включение планировщика -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check form-switch d-flex align-items-center gap-2 p-0">
|
||||||
|
<input class="form-check-input m-0" type="checkbox" role="switch" id="scheduler_enabled" {%
|
||||||
|
if data.schedulerSettings and data.schedulerSettings.enabled %}checked{% endif %}
|
||||||
|
onchange="markSchedulerChanged()">
|
||||||
|
<label class="form-check-label fw-semibold mb-0" for="scheduler_enabled">
|
||||||
|
Включить автоматическую публикацию
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Планировщик будет проверять дни рождения и публиковать поздравления ежедневно в
|
||||||
|
указанное время.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Информация о следующем запуске -->
|
||||||
|
{% if data.schedulerStatus.next_run_time %}
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="bi bi-calendar-event fs-5 me-3"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="alert-heading mb-1">Следующий запуск</h6>
|
||||||
|
<p class="mb-0">{{ data.schedulerStatus.next_run_time }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Кнопка сохранения -->
|
||||||
|
<div class="d-flex gap-2 pt-3 border-top">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="resetSchedulerForm()">
|
||||||
|
<i class="bi bi-x-lg me-1"></i>Сбросить
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" id="saveSchedulerButton"
|
||||||
|
onclick="saveSchedulerSettings()" disabled>
|
||||||
|
<i class="bi bi-save me-1"></i>Сохранить настройки планировщика
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно для подтверждения сохранения -->
|
||||||
|
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-exclamation-triangle text-warning me-2"></i>Несохраненные
|
||||||
|
изменения</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>У вас есть несохраненные изменения для сотрудника <span id="modalUserName"
|
||||||
|
class="fw-semibold"></span>.</p>
|
||||||
|
<p>Вы хотите сохранить изменения перед переходом к другому сотруднику?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="cancelSwitchUser()">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Отмена
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
|
||||||
|
onclick="discardUserChanges()">
|
||||||
|
<i class="bi bi-trash me-1"></i>Не сохранять
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="saveAndSwitchUser()">
|
||||||
|
<i class="bi bi-save me-1"></i>Сохранить и перейти
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="/static/js/birthdate.js"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -421,8 +421,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Контейнер для уведомлений -->
|
|
||||||
<div id="alertContainer" class="alert-fixed"></div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Авторизация</title>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="card login-card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="text-center mb-4">Вход</h4>
|
||||||
|
|
||||||
|
<!-- ВАЖНО: form -->
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="password" class="form-control" id="password" placeholder="Введите пароль" autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка может остаться, но теперь не обязательна -->
|
||||||
|
<button type="submit" class="btn btn-success w-100">
|
||||||
|
Войти
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="error" class="alert alert-danger mt-3 d-none"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById("loginForm").addEventListener("submit", function (e) {
|
||||||
|
e.preventDefault(); // НЕ перезагружать страницу
|
||||||
|
login();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const password = document.getElementById("password").value;
|
||||||
|
const errorBox = document.getElementById("error");
|
||||||
|
|
||||||
|
errorBox.classList.add("d-none");
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
errorBox.textContent = "Введите пароль";
|
||||||
|
errorBox.classList.remove("d-none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === "ok" && data.token) {
|
||||||
|
setCookie("auth_token", data.token, 365);
|
||||||
|
window.location.href = "/";
|
||||||
|
} else {
|
||||||
|
errorBox.textContent = data.errorMessage || "Неверный пароль";
|
||||||
|
errorBox.classList.remove("d-none");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorBox.textContent = "Ошибка соединения";
|
||||||
|
errorBox.classList.remove("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value, days) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||||
|
document.cookie = `${name}=${value}; expires=${date.toUTCString()}; path=/`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -255,9 +255,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Всплывающие уведомления -->
|
|
||||||
<div id="alertContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1050;"></div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="mb-1"><i class="bi bi-megaphone-fill text-primary me-2"></i>Управление постами</h2>
|
<h2 class="mb-1"><i class="bi bi-file-earmark-text text-primary me-2"></i>Управление постами</h2>
|
||||||
<p class="text-muted mb-0">Создание и планирование публикаций в VK</p>
|
<p class="text-muted mb-0">Создание и планирование публикаций в VK</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-success btn-lg px-5" onclick="saveSettings()">
|
<button type="button" class="btn btn-success btn-lg px-5" onclick="saveSettings()">
|
||||||
@@ -215,8 +215,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Контейнер для уведомлений -->
|
|
||||||
<div id="alertContainer" class="alert-fixed"></div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|||||||
+1
-3
@@ -9,7 +9,7 @@
|
|||||||
<!-- Заголовок -->
|
<!-- Заголовок -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="mb-1"><i class="bi bi-megaphone vk-icon me-2"></i>VK Настройки</h2>
|
<h2 class="mb-1"><i class="bi bi-chat-dots vk-icon me-2"></i>VK Настройки</h2>
|
||||||
<p class="text-muted mb-0">Настройки для работы с VK API и сообществом</p>
|
<p class="text-muted mb-0">Настройки для работы с VK API и сообществом</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge bg-primary fs-6 px-3 py-2">
|
<div class="badge bg-primary fs-6 px-3 py-2">
|
||||||
@@ -155,8 +155,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Контейнер для уведомлений -->
|
|
||||||
<div id="alertContainer" class="alert-fixed"></div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.14'",
|
||||||
|
"python_full_version < '3.14'",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "apscheduler"
|
name = "apscheduler"
|
||||||
@@ -14,6 +18,49 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/58/9f/d3c76f76c73fcc959d28e9def45b8b1cc3d7722660c5003b19c1022fd7f4/apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2", size = 64278, upload-time = "2025-10-31T18:55:41.186Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/9f/d3c76f76c73fcc959d28e9def45b8b1cc3d7722660c5003b19c1022fd7f4/apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2", size = 64278, upload-time = "2025-10-31T18:55:41.186Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi"
|
||||||
|
version = "25.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "argon2-cffi-bindings" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi-bindings"
|
||||||
|
version = "25.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blinker"
|
name = "blinker"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -32,6 +79,51 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.4.4"
|
version = "3.4.4"
|
||||||
@@ -243,6 +335,7 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "apscheduler" },
|
{ name = "apscheduler" },
|
||||||
|
{ name = "argon2-cffi" },
|
||||||
{ name = "flask" },
|
{ name = "flask" },
|
||||||
{ name = "flask-sqlalchemy" },
|
{ name = "flask-sqlalchemy" },
|
||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
@@ -253,6 +346,7 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "apscheduler", specifier = ">=3.11.1" },
|
{ name = "apscheduler", specifier = ">=3.11.1" },
|
||||||
|
{ name = "argon2-cffi", specifier = ">=25.1.0" },
|
||||||
{ name = "flask", specifier = ">=3.1.2" },
|
{ name = "flask", specifier = ">=3.1.2" },
|
||||||
{ name = "flask-sqlalchemy", specifier = ">=3.1.1" },
|
{ name = "flask-sqlalchemy", specifier = ">=3.1.1" },
|
||||||
{ name = "pyjwt", specifier = ">=2.10.1" },
|
{ name = "pyjwt", specifier = ">=2.10.1" },
|
||||||
@@ -260,6 +354,15 @@ requires-dist = [
|
|||||||
{ name = "vk-api", specifier = ">=11.10.0" },
|
{ name = "vk-api", specifier = ">=11.10.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.23"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyjwt"
|
name = "pyjwt"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
|
|||||||
+66
-14
@@ -1,13 +1,13 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
import vk_api
|
import vk_api
|
||||||
from db import VkAPI, db
|
from db import UsersBirthdate, VkAPI, db
|
||||||
from medods_handler import setDynamicText
|
from medods_handler import setDynamicText
|
||||||
|
|
||||||
|
|
||||||
def handle_vk_post():
|
def handle_vk_post():
|
||||||
from app import logger
|
from app import logger
|
||||||
|
|
||||||
logger.info("Обновление поста")
|
logger.info("Публикация поста")
|
||||||
|
|
||||||
vkApi = VkAPI.query.first()
|
vkApi = VkAPI.query.first()
|
||||||
if not vkApi:
|
if not vkApi:
|
||||||
@@ -20,16 +20,12 @@ def handle_vk_post():
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not vkPost.dynamic_text:
|
if not vkPost.dynamic_text:
|
||||||
# if not vkPost.dynamic_text and not vkPost.post_id:
|
|
||||||
logger.info("Не требуется публикация поста")
|
logger.info("Не требуется публикация поста")
|
||||||
return
|
return
|
||||||
|
|
||||||
vk_session = vk_api.VkApi(token=vkApi.access_token)
|
vk_session = vk_api.VkApi(token=vkApi.access_token)
|
||||||
vk = vk_session.get_api()
|
vk = vk_session.get_api()
|
||||||
|
|
||||||
new_post = {}
|
|
||||||
|
|
||||||
if vkPost.dynamic_text:
|
|
||||||
new_post = vk.wall.post(
|
new_post = vk.wall.post(
|
||||||
owner_id=-vkApi.group_id,
|
owner_id=-vkApi.group_id,
|
||||||
from_group=1,
|
from_group=1,
|
||||||
@@ -38,15 +34,71 @@ def handle_vk_post():
|
|||||||
)
|
)
|
||||||
logger.info(f"Пост #{new_post.get('post_id')} создан")
|
logger.info(f"Пост #{new_post.get('post_id')} создан")
|
||||||
|
|
||||||
# if vkPost.post_id:
|
|
||||||
# logger.info(f"Удаление поста #{vkPost.post_id}")
|
|
||||||
# vk.wall.delete(owner_id=-vkApi.group_id, post_id=vkPost.post_id)
|
|
||||||
# vkPost.post_id = None
|
|
||||||
# vkPost.publish_at = None
|
|
||||||
|
|
||||||
if vkPost.dynamic_text:
|
|
||||||
vkPost.dynamic_text = None
|
vkPost.dynamic_text = None
|
||||||
vkPost.post_id = new_post.get("post_id")
|
vkPost.post_id = new_post.get("post_id")
|
||||||
vkPost.publish_at = datetime.now()
|
vkPost.publish_at = datetime.now()
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_vk_birthdate():
|
||||||
|
from app import logger
|
||||||
|
from sqlalchemy import func, or_, and_
|
||||||
|
|
||||||
|
logger.info("Публикация поста с днем рождения")
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
day = f"{today.day:02d}"
|
||||||
|
month = f"{today.month:02d}"
|
||||||
|
|
||||||
|
def is_leap_year(year: int) -> bool:
|
||||||
|
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
||||||
|
|
||||||
|
# Основное условие: совпадение дня и месяца
|
||||||
|
conditions = [
|
||||||
|
and_(
|
||||||
|
func.strftime("%d", UsersBirthdate.birthdate) == day,
|
||||||
|
func.strftime("%m", UsersBirthdate.birthdate) == month,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Если 28 февраля и год НЕ високосный — добавляем родившихся 29.02
|
||||||
|
if today.month == 2 and today.day == 28 and not is_leap_year(today.year):
|
||||||
|
conditions.append(
|
||||||
|
and_(
|
||||||
|
func.strftime("%d", UsersBirthdate.birthdate) == "29",
|
||||||
|
func.strftime("%m", UsersBirthdate.birthdate) == "02",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conditions.append(UsersBirthdate.enabled == True)
|
||||||
|
|
||||||
|
birthdayUsers = UsersBirthdate.query.filter(or_(*conditions)).all()
|
||||||
|
|
||||||
|
if not birthdayUsers:
|
||||||
|
logger.info("Нет пользователей с днем рождения")
|
||||||
|
return
|
||||||
|
|
||||||
|
vkApi = VkAPI.query.first()
|
||||||
|
if not vkApi:
|
||||||
|
logger.error("Информация для работы не найдена")
|
||||||
|
return
|
||||||
|
|
||||||
|
vk_session = vk_api.VkApi(token=vkApi.access_token)
|
||||||
|
vk = vk_session.get_api()
|
||||||
|
|
||||||
|
for user in birthdayUsers:
|
||||||
|
new_post = vk.wall.post(
|
||||||
|
owner_id=-vkApi.group_id,
|
||||||
|
from_group=1,
|
||||||
|
message=user.congratulations.strip(),
|
||||||
|
attachments=user.photo_link,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Пост #{new_post.get('post_id')} создан")
|
||||||
|
|
||||||
|
user.post_link = (
|
||||||
|
f"https://vk.com/wall-{vkApi.group_id}_{new_post.get('post_id')}"
|
||||||
|
)
|
||||||
|
user.publish_at = datetime.now()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user