Files
site_aegisone/py_service/app/routers/service_pages.py
T

1573 lines
86 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import math
from datetime import datetime
from collections import defaultdict
from fastapi import APIRouter, Request, Depends, Form, Query
from fastapi.responses import RedirectResponse, JSONResponse, HTMLResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, delete, or_, and_, Integer, text
from sqlalchemy.orm import selectinload, joinedload
from app.database import get_db
from app.dependencies import get_current_user, require_role
from app.auth import audit_log
from app.models.models import (
User, Customer, Object, ObjectAssignment, SLAContract,
Task, Incident, Report, SHSRecord, EngineerKPI,
FormulaCoefficient, BlogPost, Case, QuestionnaireItem,
QuestionnaireSession, QuestionnaireAnswer, ObjectPassport,
Idea, QuickMenuItem,
)
from app.calculations import engine as calc_engine
FORMULA_GROUPS = {
"risk_score": {"name": "Оценка рисков", "formula": "Риск = no_archive×25 + no_power_backup×20 + no_regulations×15 + system_failures×20 + no_documentation×10", "result": "0100 баллов"},
"risk_multiplier": {"name": "Множитель риска", "formula": "Множитель = f(уровень риска): низкий→1.0, средний→1.3, высокий→1.6, критический→2.0", "result": "1.02.0 ×"},
"complexity": {"name": "Сложность объекта", "formula": "Сложность = video(based on cameras) + access(based on APs) + fire(type) + IT(bool)", "result": "Простой(<30) / Средний(3070) / Сложный(70120) / Enterprise(>120)"},
"infra": {"name": "Нагрузка на инфраструктуру", "formula": "Нагрузка = server + network + power (по состояниям)", "result": "050 баллов"},
"history": {"name": "История обслуживания", "formula": "Присваивается вес: none→max, sla→min", "result": "Зависит от типа"},
"object_index": {"name": "Индекс объекта", "formula": "I = risk×0.4 + complexity×0.3 + infra×0.2 + history×0.1", "result": "0100 → Класс A/B/C/D"},
"engineer": {"name": "Оценка инженера", "formula": "ES = SLA×0.25 + отклик×0.20 + решение×0.20 + диагноз×0.15 + переоткрытия×0.10 + риски×0.10 → grade", "result": "0100 → Junior/Middle/Strong/Senior"},
"ecs": {"name": "Контроль инженера (ECS)", "formula": "ECS = SLA×0.30 + задачи×0.25 + инциденты×0.20 + команда×0.15 + координация×0.10", "result": "0100"},
"shs": {"name": "SHS (Service Health Score)", "formula": "SHS = SLA×0.22 + выручка×0.18 + удержание×0.18 + инженеры×0.15 + инциденты×0.12 + продажи×0.10 + эффективность×0.05", "result": "0100 → Crisis/Risk/Stable/Growth"},
"ceo_shs": {"name": "CEO SHS", "formula": "CEO SHS = MRR×0.25 + SLA×0.20 + удержание×0.20 + продукт-ть×0.15 + конверсия×0.10 + инциденты×0.10", "result": "0100"},
"ssi": {"name": "SLA Stability Index", "formula": "SSI = SLA_compliance × (1 - Breach_Severity)", "result": "0100"},
"isi": {"name": "Incident Stability Index", "formula": "ISI = max(0, min(100, 100 - weighted_rate×20))", "result": "0100"},
"bonus": {"name": "Бонус инженера", "formula": "% премии = f(оценка): ≥90→20%, 8089→10%, <80→0%", "result": "0%20%"},
"retention": {"name": "Удержание клиентов", "formula": "Удержание = base - lost_key×10% (per key client)", "result": "0100%"},
"rules": {"name": "Правила автоматизации", "formula": "Пороги: SHS<70→Crisis, SLA<90%→Warning, Retention<90%→Critical", "result": "Триггеры алертов"},
}
router = APIRouter()
from app.templating import templates
def now_str():
return datetime.now().strftime("%d.%m.%Y %H:%M")
def ctx(request, user, **kw):
original_user = request.session.get("original_user")
available_roles = {"owner": ["owner", "engineer", "technician"], "engineer": ["engineer", "technician"]}
c = {"request": request, "user": user, "now": now_str(),
"role_permissions": getattr(request.state, 'role_permissions', {}),
"role_override_active": original_user is not None,
"original_role": original_user["role"] if original_user else None,
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else [],
"page_name": kw.get("active_page", "")}
c.update(kw)
return c
async def load_quick_menu(db: AsyncSession, role: str) -> list:
result = await db.execute(
select(QuickMenuItem).where(QuickMenuItem.role == role).order_by(QuickMenuItem.sort_order, QuickMenuItem.id)
)
return result.scalars().all()
# ===================== USERS =====================
@router.get("/service/users")
async def users_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
result = await db.execute(select(User).order_by(User.role, User.full_name))
return templates.TemplateResponse("pages/users.html", ctx(request, user=user, users=result.scalars().all(), active_page="users", title="Сотрудники"))
@router.post("/service/users/create")
async def users_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
login = form.get("login")
password = form.get("password")
if not login or not password:
return RedirectResponse(url="/service/users", status_code=302)
from app.auth import hash_password
max_id_val = form.get("max_user_id")
new_user = User(
login=login,
password_hash=hash_password(password),
full_name=form.get("full_name"), role=form.get("role", "technician"),
phone=form.get("phone", ""), email=form.get("email", ""),
max_user_id=int(max_id_val) if max_id_val and max_id_val.strip() else None,
is_active=form.get("is_active", "1") == "1",
)
db.add(new_user); await db.flush()
await audit_log(db, user["id"], "user_create", f"Создан пользователь {login}", request.client.host or "")
return RedirectResponse(url="/service/users", status_code=302)
@router.post("/service/users/edit")
async def users_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
user_id = form.get("id")
if not user_id:
return RedirectResponse(url="/service/users", status_code=302)
try:
u = await db.get(User, int(user_id))
except (TypeError, ValueError):
return RedirectResponse(url="/service/users", status_code=302)
if not u: return RedirectResponse(url="/service/users", status_code=302)
u.full_name = form.get("full_name", u.full_name)
u.role = form.get("role", u.role)
u.phone = form.get("phone", u.phone)
u.email = form.get("email", u.email)
max_id_val = form.get("max_user_id")
u.max_user_id = int(max_id_val) if max_id_val and max_id_val.strip() else None
if "is_active" in form:
u.is_active = form["is_active"] == "1"
from app.auth import hash_password
pwd = form.get("password", "")
if pwd: u.password_hash = hash_password(pwd)
await db.flush()
await audit_log(db, user["id"], "user_edit", f"Изменён пользователь {u.login}", request.client.host or "")
return RedirectResponse(url="/service/users", status_code=302)
@router.post("/service/users/delete")
async def users_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
user_id = form.get("id")
if not user_id:
return RedirectResponse(url="/service/users", status_code=302)
try:
u = await db.get(User, int(user_id))
except (TypeError, ValueError):
return RedirectResponse(url="/service/users", status_code=302)
if u and u.role != "owner": await db.delete(u); await db.flush()
return RedirectResponse(url="/service/users", status_code=302)
# ===================== CUSTOMERS =====================
@router.get("/service/customers")
async def customers_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), search: str = Query(""), error: str = Query("")):
sql = select(Customer)
if search: sql = sql.where(or_(Customer.name.ilike(f"%{search}%"), Customer.inn.ilike(f"%{search}%")))
result = await db.execute(sql.order_by(Customer.name))
customers = result.scalars().all()
data = []
for c in customers:
oc = (await db.execute(select(func.count(Object.id)).where(Object.customer_id == c.id))).scalar() or 0
sc = (await db.execute(select(func.count(SLAContract.id)).where(and_(SLAContract.customer_id == c.id, SLAContract.status == "active")))).scalar() or 0
data.append({"c": c, "objects_count": oc, "active_contracts": sc})
err_msg = {"inn_invalid": "ИНН должен содержать 10 или 12 цифр"}.get(error, "")
return templates.TemplateResponse("pages/customers.html", ctx(request, user=user, customers=data, search=search, active_page="customers", title="Клиенты", error=err_msg))
@router.post("/service/customers/create")
async def customers_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
inn = form.get("inn", "").strip()
if not inn or not inn.isdigit() or len(inn) not in (10, 12):
return RedirectResponse(url="/service/customers?error=inn_invalid", status_code=302)
c = Customer(name=form.get("name"), inn=inn, kpp=form.get("kpp",""), legal_address=form.get("legal_address",""),
contact_person=form.get("contact_person",""), contact_phone=form.get("contact_phone",""), contact_email=form.get("contact_email",""),
status=form.get("status","active"), notes=form.get("notes",""), created_by=user["id"])
db.add(c); await db.flush()
return RedirectResponse(url="/service/customers", status_code=302)
@router.post("/service/customers/edit")
async def customers_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
c = await db.get(Customer, int(form.get("id")))
if not c: return RedirectResponse(url="/service/customers", status_code=302)
for f in ("name","inn","kpp","legal_address","contact_person","contact_phone","contact_email","status","notes"):
if form.get(f) is not None: setattr(c, f, form.get(f))
await db.flush()
return RedirectResponse(url="/service/customers", status_code=302)
@router.post("/service/customers/delete")
async def customers_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
c = await db.get(Customer, int((await request.form()).get("id")))
if c: await db.delete(c); await db.flush()
return RedirectResponse(url="/service/customers", status_code=302)
# ===================== OBJECTS =====================
@router.get("/service/objects")
async def objects_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), search: str = Query(""), customer_id: int = Query(None)):
sql = select(Object).options(joinedload(Object.customer))
if search: sql = sql.where(or_(Object.name.ilike(f"%{search}%"), Object.address.ilike(f"%{search}%")))
if customer_id: sql = sql.where(Object.customer_id == customer_id)
result = await db.execute(sql.order_by(Object.name))
objects = result.unique().scalars().all()
cust_res = await db.execute(select(Customer).where(Customer.status == "active").order_by(Customer.name))
customers = cust_res.scalars().all()
return templates.TemplateResponse("pages/objects.html", ctx(request, user=user, objects=objects, customers_list=customers, search=search, customer_id=customer_id, active_page="objects", title="Объекты"))
@router.post("/service/objects/create")
async def objects_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
o = Object(name=form.get("name"), customer_id=int(form.get("customer_id")) if form.get("customer_id") else None,
object_type=form.get("object_type",""), address=form.get("address",""), contact_person=form.get("contact_person",""),
contact_phone=form.get("contact_phone",""), status=form.get("status","active"), notes=form.get("notes",""), created_by=user["id"])
db.add(o); await db.flush()
return RedirectResponse(url="/service/objects", status_code=302)
@router.post("/service/objects/edit")
async def objects_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
try:
oid = int(form.get("id"))
except (ValueError, TypeError):
return RedirectResponse(url="/service/objects", status_code=302)
o = await db.get(Object, oid)
if not o: return RedirectResponse(url="/service/objects", status_code=302)
for f in ("name","customer_id","object_type","address","contact_person","contact_phone","status","notes"):
val = form.get(f)
if val is not None:
if f == "customer_id":
try:
val = int(val) if val else None
except (ValueError, TypeError):
val = None
setattr(o, f, val)
await db.flush()
return RedirectResponse(url="/service/objects", status_code=302)
@router.post("/service/objects/delete")
async def objects_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
o = await db.get(Object, int((await request.form()).get("id")))
if o: await db.delete(o); await db.flush()
return RedirectResponse(url="/service/objects", status_code=302)
# ===================== ASSIGNMENTS =====================
@router.get("/service/assignments")
async def assignments_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
objects = (await db.execute(select(Object).where(Object.status == "active").order_by(Object.name))).scalars().all()
users = (await db.execute(select(User).where(and_(User.is_active, User.role.in_(["engineer","technician"]))).order_by(User.full_name))).scalars().all()
result = await db.execute(
select(ObjectAssignment).options(
joinedload(ObjectAssignment.object),
joinedload(ObjectAssignment.user)
).where(ObjectAssignment.unassigned_at.is_(None)).order_by(ObjectAssignment.assigned_at.desc())
)
assignments = result.unique().scalars().all()
return templates.TemplateResponse("pages/assignments.html", ctx(request, user=user, objects=objects, users=users, assignments=assignments, active_page="assignments", title="Назначение сотрудников"))
@router.post("/service/assignments/create")
async def assignments_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
u = await db.get(User, int(form.get("user_id")))
a = ObjectAssignment(object_id=int(form.get("object_id")), user_id=int(form.get("user_id")), role=u.role if u else "technician")
db.add(a); await db.flush()
return RedirectResponse(url="/service/assignments", status_code=302)
@router.post("/service/assignments/unassign")
async def assignments_unassign(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
a = await db.get(ObjectAssignment, int((await request.form()).get("id")))
if a: a.unassigned_at = datetime.now(); await db.flush()
return RedirectResponse(url="/service/assignments", status_code=302)
# ===================== SLA =====================
@router.get("/service/sla")
async def sla_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), object_id: int = Query(None)):
sql = select(SLAContract).options(joinedload(SLAContract.object), joinedload(SLAContract.customer))
if object_id: sql = sql.where(SLAContract.object_id == object_id)
result = await db.execute(sql.order_by(SLAContract.created_at.desc()))
contracts = result.unique().scalars().all()
objects = (await db.execute(select(Object).where(Object.status.in_(["active","audit"])).order_by(Object.name))).scalars().all()
return templates.TemplateResponse("pages/sla.html", ctx(request, user=user, contracts=contracts, objects=objects, sel_object_id=object_id, active_page="sla", title="SLA контракты"))
@router.post("/service/sla/create")
async def sla_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
try:
oid = int(form.get("object_id")) if form.get("object_id") else None
except (ValueError, TypeError):
oid = None
if not oid:
return RedirectResponse(url="/service/sla", status_code=302)
try:
price = float(form.get("sla_price_monthly", 0)) if form.get("sla_price_monthly") else 0
except (ValueError, TypeError):
price = 0
try:
start = datetime.strptime(form.get("start_date"), "%Y-%m-%d").date() if form.get("start_date") else None
except (ValueError, TypeError):
start = None
try:
end = datetime.strptime(form.get("end_date"), "%Y-%m-%d").date() if form.get("end_date") else None
except (ValueError, TypeError):
end = None
sc = SLAContract(object_id=oid, service_level=form.get("service_level", "start"),
sla_price_monthly=price, start_date=start, end_date=end, status=form.get("status", "negotiation"))
db.add(sc); await db.flush()
return RedirectResponse(url="/service/sla", status_code=302)
@router.post("/service/sla/edit")
async def sla_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
try:
sc_id = int(form.get("id"))
except (ValueError, TypeError):
return RedirectResponse(url="/service/sla", status_code=302)
sc = await db.get(SLAContract, sc_id)
if not sc: return RedirectResponse(url="/service/sla", status_code=302)
for f in ("object_id","service_level","sla_price_monthly","start_date","end_date","status"):
val = form.get(f)
if val is not None:
if f == "object_id":
try:
val = int(val) if val else None
except (ValueError, TypeError):
val = None
elif f == "sla_price_monthly":
try:
val = float(val) if val else 0
except (ValueError, TypeError):
val = 0
elif f in ("start_date","end_date"):
try:
val = datetime.strptime(val,"%Y-%m-%d").date() if val else None
except (ValueError, TypeError):
val = None
setattr(sc, f, val)
await db.flush()
return RedirectResponse(url="/service/sla", status_code=302)
# ===================== CEO Dashboard =====================
@router.get("/service/ceo")
async def ceo_dashboard(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
shs_history = (await db.execute(select(SHSRecord).order_by(SHSRecord.id.desc()).limit(12))).scalars().all()
latest_shs = shs_history[0] if shs_history else None
mrr = float((await db.execute(select(func.sum(SLAContract.sla_price_monthly)).where(SLAContract.status == "active"))).scalar() or 0)
active_objects = (await db.execute(select(func.count(Object.id)).where(Object.status == "active"))).scalar() or 0
active_contracts = (await db.execute(select(func.count(SLAContract.id)).where(SLAContract.status == "active"))).scalar() or 0
active_engineers = (await db.execute(select(func.count(User.id)).where(and_(User.is_active, User.role.in_(["engineer","technician"]))))).scalar() or 0
compl_res = await db.execute(select(func.count(Task.id), func.sum(Task.sla_compliant.cast(Integer))).where(Task.closed_at != None))
total_c, compliant_c = compl_res.first() or (0,0)
sla_comp = round((compliant_c / total_c) * 100, 1) if total_c > 0 else 100
incidents_30 = (await db.execute(select(func.count(Incident.id)).where(Incident.created_at > datetime.now().replace(day=1)))).scalar() or 0
kpi_data = (await db.execute(
select(EngineerKPI, User.full_name).join(User, EngineerKPI.engineer_id == User.id).order_by(EngineerKPI.period_end.desc()).limit(10)
)).all()
avg_es = (await db.execute(select(func.avg(EngineerKPI.engineer_score)).where(EngineerKPI.period_end > datetime.now().replace(day=1)))).scalar() or 0
return templates.TemplateResponse("pages/ceo.html", ctx(request, user=user, shs_history=shs_history, latest_shs=latest_shs, mrr=mrr,
active_objects=active_objects, active_contracts=active_contracts, active_engineers=active_engineers,
sla_compliance=sla_comp, incidents_30=incidents_30, kpi_data=kpi_data, avg_es=round(float(avg_es),2),
active_page="ceo", title="CEO дашборд"))
# ===================== COEFFICIENTS =====================
@router.get("/service/coefficients")
async def coefficients_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
coeffs = (await db.execute(select(FormulaCoefficient).order_by(FormulaCoefficient.key))).scalars().all()
groups = defaultdict(list)
for c in coeffs:
prefix = c.formula_ref.split(".")[0] if c.formula_ref else "_other"
groups[prefix].append(c)
grouped = {}
for prefix in sorted(groups.keys()):
info = FORMULA_GROUPS.get(prefix, {"name": prefix, "formula": "", "result": ""})
grouped[prefix] = {"info": info, "coeffs": groups[prefix]}
return templates.TemplateResponse("pages/coefficients.html", ctx(request, user=user, groups=grouped, active_page="coefficients", title="Коэффициенты"))
@router.post("/service/coefficients/edit")
async def coefficients_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
c = await db.get(FormulaCoefficient, int(form.get("id")))
if c:
try: c.value = float(form.get("value"))
except: c.value = form.get("value")
c.updated_by = user["id"]; await db.flush()
return RedirectResponse(url="/service/coefficients", status_code=302)
@router.post("/service/coefficients/toggle")
async def coefficients_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
c = await db.get(FormulaCoefficient, int((await request.form()).get("id")))
if c: c.is_active = not c.is_active; c.updated_by = user["id"]; await db.flush()
return RedirectResponse(url="/service/coefficients", status_code=302)
@router.post("/service/coefficients/create")
async def coefficients_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
db.add(FormulaCoefficient(key=form.get("key"), value=float(form.get("value")), description=form.get("description",""),
formula_ref=form.get("formula_ref",""), updated_by=user["id"]))
await db.flush()
return RedirectResponse(url="/service/coefficients", status_code=302)
@router.post("/service/coefficients/delete")
async def coefficients_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
c = await db.get(FormulaCoefficient, int((await request.form()).get("id")))
if c: await db.delete(c); await db.flush()
return RedirectResponse(url="/service/coefficients", status_code=302)
# ===================== FORMULA TEST =====================
FORMULA_SANDBOX = {
"risk_score": {
"name": "Оценка рисков",
"inputs": [
{"key": "no_archive", "label": "Нет архивации", "type": "checkbox"},
{"key": "no_power_backup", "label": "Нет резервного питания", "type": "checkbox"},
{"key": "no_regulations", "label": "Нет регламентов", "type": "checkbox"},
{"key": "system_failures", "label": "Отказы системы", "type": "checkbox"},
{"key": "no_documentation", "label": "Нет документации", "type": "checkbox"},
],
},
"complexity": {
"name": "Сложность объекта",
"inputs": [
{"key": "cameras", "label": "Кол-во камер", "type": "number", "default": 20},
{"key": "access_points", "label": "Кол-во СКУД точек", "type": "number", "default": 5},
{"key": "fire_type", "label": "Тип пожарной системы", "type": "select", "options": {"none": "Нет", "simple": "Простая", "medium": "Средняя", "complex": "Комплексная"}},
{"key": "has_it", "label": "Есть IT-инфраструктура", "type": "checkbox"},
],
},
"object_index": {
"name": "Индекс объекта",
"inputs": [
{"key": "risk_score", "label": "Риск (0100)", "type": "number", "default": 50},
{"key": "complexity_score", "label": "Сложность (0150)", "type": "number", "default": 50},
{"key": "infra_score", "label": "Инфраструктура (0–50)", "type": "number", "default": 10},
{"key": "history_score", "label": "История (030)", "type": "number", "default": 15},
],
},
"engineer": {
"name": "Оценка инженера (Engineer Score)",
"inputs": [
{"key": "sla_compliance", "label": "SLA Compliance", "type": "number", "default": 90},
{"key": "response_time_score", "label": "Response Time Score", "type": "number", "default": 80},
{"key": "resolution_time_score", "label": "Resolution Time Score", "type": "number", "default": 80},
{"key": "diagnosis_accuracy", "label": "Diagnosis Accuracy", "type": "number", "default": 85},
{"key": "reopen_rate_score", "label": "Reopen Rate Score", "type": "number", "default": 90},
{"key": "risk_coverage_score", "label": "Risk Coverage Score", "type": "number", "default": 75},
],
},
"ecs": {
"name": "Контроль инженера (ECS)",
"inputs": [
{"key": "sla_control", "label": "SLA Control", "type": "number", "default": 85},
{"key": "task_dist", "label": "Task Distribution", "type": "number", "default": 80},
{"key": "incident_red", "label": "Incident Reduction", "type": "number", "default": 70},
{"key": "team_perf", "label": "Team Performance", "type": "number", "default": 75},
{"key": "response_coord", "label": "Response Coordination", "type": "number", "default": 80},
],
},
"shs": {
"name": "Service Health Score (SHS)",
"inputs": [
{"key": "sla_stability", "label": "SLA Stability", "type": "number", "default": 85},
{"key": "revenue_stability", "label": "Revenue Stability", "type": "number", "default": 80},
{"key": "retention", "label": "Retention", "type": "number", "default": 90},
{"key": "engineer_performance", "label": "Engineer Performance", "type": "number", "default": 75},
{"key": "incident_stability", "label": "Incident Stability", "type": "number", "default": 80},
{"key": "sales_flow", "label": "Sales Flow", "type": "number", "default": 70},
{"key": "operational_efficiency", "label": "Operational Efficiency", "type": "number", "default": 75},
],
},
"ceo_shs": {
"name": "CEO SHS",
"inputs": [
{"key": "mrr_growth", "label": "MRR Growth", "type": "number", "default": 80},
{"key": "sla_compliance", "label": "SLA Compliance", "type": "number", "default": 90},
{"key": "retention", "label": "Retention", "type": "number", "default": 85},
{"key": "productivity", "label": "Productivity", "type": "number", "default": 75},
{"key": "conversion", "label": "Conversion", "type": "number", "default": 70},
{"key": "incident_stability", "label": "Incident Stability", "type": "number", "default": 80},
],
},
}
@router.get("/service/coefficients/test")
async def formula_test_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")), group: str = Query("")):
coeffs = (await db.execute(select(FormulaCoefficient).order_by(FormulaCoefficient.key))).scalars().all()
groups = defaultdict(list)
for c in coeffs:
prefix = c.formula_ref.split(".")[0] if c.formula_ref else "_other"
groups[prefix].append(c)
formula_list = []
for prefix in sorted(groups.keys()):
info = FORMULA_GROUPS.get(prefix, {"name": prefix, "formula": "", "result": ""})
sb = FORMULA_SANDBOX.get(prefix)
formula_list.append({
"key": prefix,
"name": info["name"],
"formula": info["formula"],
"result": info["result"],
"sandbox": sb,
"coeffs": groups[prefix],
})
selected = group if group else (formula_list[0]["key"] if formula_list else "")
return templates.TemplateResponse("pages/formula_test.html", ctx(request, user=user, formulas=formula_list, selected=selected, active_page="coefficients", title="Тест формул"))
@router.get("/service/formulas")
async def formulas_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
coeffs = (await db.execute(select(FormulaCoefficient).order_by(FormulaCoefficient.key))).scalars().all()
groups = defaultdict(list)
for c in coeffs:
prefix = c.formula_ref.split(".")[0] if c.formula_ref else "_other"
groups[prefix].append(c)
grouped = {}
for prefix in sorted(groups.keys()):
info = FORMULA_GROUPS.get(prefix, {"name": prefix, "formula": "", "result": ""})
grouped[prefix] = {"info": info, "coeffs": groups[prefix]}
formula_list = []
for prefix in sorted(groups.keys()):
info = FORMULA_GROUPS.get(prefix, {"name": prefix, "formula": "", "result": ""})
sb = FORMULA_SANDBOX.get(prefix)
formula_list.append({
"key": prefix,
"name": info["name"],
"formula": info["formula"],
"result": info["result"],
"sandbox": sb,
"coeffs": groups[prefix],
})
selected = formula_list[0]["key"] if formula_list else ""
return templates.TemplateResponse("pages/formulas.html", ctx(request, user=user, formulas=formula_list, selected=selected, groups=grouped, active_page="formulas", title="Формулы"))
@router.post("/service/coefficients/test/{formula_name}")
async def formula_test_calc(request: Request, formula_name: str, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
try:
if formula_name == "risk_score":
result = await calc_engine.calc_risk_score(
db,
no_archive=form.get("no_archive") == "1",
no_power_backup=form.get("no_power_backup") == "1",
no_regulations=form.get("no_regulations") == "1",
system_failures=form.get("system_failures") == "1",
no_documentation=form.get("no_documentation") == "1",
)
label = await calc_engine.risk_label(db, result)
return JSONResponse({"result": f"{result:.1f} баллов — {label}"})
elif formula_name == "complexity":
result = await calc_engine.calc_complexity(
db,
cameras=int(form.get("cameras", 0)),
access_points=int(form.get("access_points", 0)),
fire_type=form.get("fire_type", "none"),
has_it=form.get("has_it") == "1",
)
label = calc_engine.complexity_label(result)
return JSONResponse({"result": f"{result:.1f} баллов — {label}"})
elif formula_name == "object_index":
# These are pre-calculated scores, not the same as coefficient-based.
# We use the weighted formula directly.
risk = float(form.get("risk_score", 50))
complexity = float(form.get("complexity_score", 50))
infra = float(form.get("infra_score", 10))
history = float(form.get("history_score", 15))
result = await calc_engine.calc_object_index(db, risk, complexity, infra, history)
cls = calc_engine.object_class(result)
label = calc_engine.object_class_label(result)
return JSONResponse({"result": f"Индекс: {result} — Класс {cls} ({label})"})
elif formula_name == "engineer":
result = await calc_engine.calc_engineer_score(
db,
sla_compliance=float(form.get("sla_compliance", 90)),
response_time_score=float(form.get("response_time_score", 80)),
resolution_time_score=float(form.get("resolution_time_score", 80)),
diagnosis_accuracy=float(form.get("diagnosis_accuracy", 85)),
reopen_rate_score=float(form.get("reopen_rate_score", 90)),
risk_coverage_score=float(form.get("risk_coverage_score", 75)),
)
grade = await calc_engine.engineer_grade(db, result)
return JSONResponse({"result": f"Engineer Score: {result}{grade}"})
elif formula_name == "ecs":
result = await calc_engine.calc_engineer_control_score(
db,
sla_control=float(form.get("sla_control", 85)),
task_dist=float(form.get("task_dist", 80)),
incident_red=float(form.get("incident_red", 70)),
team_perf=float(form.get("team_perf", 75)),
response_coord=float(form.get("response_coord", 80)),
)
return JSONResponse({"result": f"ECS: {result} баллов"})
elif formula_name == "shs":
result = await calc_engine.calc_shs(
db,
sla_stability=float(form.get("sla_stability", 85)),
revenue_stability=float(form.get("revenue_stability", 80)),
retention=float(form.get("retention", 90)),
engineer_performance=float(form.get("engineer_performance", 75)),
incident_stability=float(form.get("incident_stability", 80)),
sales_flow=float(form.get("sales_flow", 70)),
operational_efficiency=float(form.get("operational_efficiency", 75)),
)
zone = await calc_engine.shs_zone(db, result)
return JSONResponse({"result": f"SHS: {result}{zone['label']} ({zone['zone']})"})
elif formula_name == "ceo_shs":
result = await calc_engine.calc_ceo_shs(
db,
mrr_growth=float(form.get("mrr_growth", 80)),
sla_compliance=float(form.get("sla_compliance", 90)),
retention=float(form.get("retention", 85)),
productivity=float(form.get("productivity", 75)),
conversion=float(form.get("conversion", 70)),
incident_stability=float(form.get("incident_stability", 80)),
)
return JSONResponse({"result": f"CEO SHS: {result} баллов"})
return JSONResponse({"result": "Формула не поддерживается в песочнице"})
except Exception as e:
return JSONResponse({"result": f"Ошибка: {str(e)}"})
# ===================== TASKS =====================
@router.get("/service/tasks")
async def tasks_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician")), status_filter: str = Query(""), search: str = Query("")):
sql = select(Task).options(joinedload(Task.object), joinedload(Task.assignee))
if user["role"] == "technician": sql = sql.where(Task.assigned_to == user["id"])
if status_filter: sql = sql.where(Task.status == status_filter)
if search: sql = sql.where(or_(Task.title.ilike(f"%{search}%")))
result = await db.execute(sql.order_by(Task.created_at.desc()))
tasks = result.unique().scalars().all()
objects = (await db.execute(select(Object).where(Object.status == "active").order_by(Object.name))).scalars().all()
users = (await db.execute(select(User).where(and_(User.is_active, User.role.in_(["engineer","technician"]))).order_by(User.full_name))).scalars().all()
return templates.TemplateResponse("pages/tasks.html", ctx(request, user=user, tasks=tasks, objects=objects, users=users, status_filter=status_filter, search=search, active_page="tasks", title="Задачи"))
@router.post("/service/tasks/create")
async def tasks_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
db.add(Task(title=form.get("title"), description=form.get("description",""), object_id=int(form.get("object_id")),
assigned_to=int(form.get("assigned_to")) if form.get("assigned_to") else None,
priority=form.get("priority","P3"),
deadline=datetime.strptime(form.get("deadline"),"%Y-%m-%d").date() if form.get("deadline") else None,
created_by=user["id"]))
await db.flush()
return RedirectResponse(url="/service/tasks", status_code=302)
@router.post("/service/tasks/close")
async def tasks_close(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician"))):
t = await db.get(Task, int((await request.form()).get("id")))
if t and (user["role"] in ("owner","engineer") or t.assigned_to == user["id"]):
t.status = "completed"; t.closed_at = datetime.now(); await db.flush()
return RedirectResponse(url="/service/tasks", status_code=302)
# ===================== REPORTS =====================
@router.get("/service/reports")
async def reports_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician")), type_filter: str = Query("")):
sql = select(Report).options(joinedload(Report.object))
if user["role"] in ("engineer","technician"): sql = sql.where(Report.created_by == user["id"])
if type_filter: sql = sql.where(Report.report_type == type_filter)
result = await db.execute(sql.order_by(Report.created_at.desc()))
reports = result.unique().scalars().all()
objects = (await db.execute(select(Object).where(Object.status.in_(["active","audit"])).order_by(Object.name))).scalars().all()
return templates.TemplateResponse("pages/reports.html", ctx(request, user=user, reports=reports, objects=objects, type_filter=type_filter, active_page="reports", title="Отчёты"))
@router.post("/service/reports/create")
async def reports_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician"))):
form = await request.form()
db.add(Report(object_id=int(form.get("object_id")), report_type=form.get("report_type","inspection"), title=form.get("title"),
description=form.get("description",""), findings=form.get("findings",""), recommendations=form.get("recommendations",""),
work_done=form.get("work_done",""), cause_classification=form.get("cause_classification",""),
status=form.get("status","draft"), result_status=form.get("result_status","ok"), created_by=user["id"]))
await db.flush()
return RedirectResponse(url="/service/reports", status_code=302)
# ===================== INCIDENTS =====================
@router.get("/service/incidents")
async def incidents_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician")), severity: str = Query(""), status_filter: str = Query("")):
sql = select(Incident).options(joinedload(Incident.object), joinedload(Incident.assignee))
if user["role"] == "technician": sql = sql.where(Incident.assigned_to == user["id"])
if severity: sql = sql.where(Incident.severity == severity)
if status_filter: sql = sql.where(Incident.status == status_filter)
result = await db.execute(sql.order_by(Incident.created_at.desc()))
incidents = result.unique().scalars().all()
objects = (await db.execute(select(Object).where(Object.status == "active").order_by(Object.name))).scalars().all()
users = (await db.execute(select(User).where(User.is_active).order_by(User.full_name))).scalars().all()
return templates.TemplateResponse("pages/incidents.html", ctx(request, user=user, incidents=incidents, objects=objects, users=users, severity=severity, status_filter=status_filter, active_page="incidents", title="Инциденты"))
@router.post("/service/incidents/create")
async def incidents_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
db.add(Incident(title=form.get("title"), description=form.get("description",""), object_id=int(form.get("object_id")),
assigned_to=int(form.get("assigned_to")) if form.get("assigned_to") else None,
severity=form.get("severity","P3"), reported_by=user["id"]))
await db.flush()
return RedirectResponse(url="/service/incidents", status_code=302)
@router.post("/service/incidents/resolve")
async def incidents_resolve(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer", "technician"))):
form = await request.form()
i = await db.get(Incident, int(form.get("id")))
if i: i.status = "resolved"; i.resolution = form.get("resolution",""); i.resolved_at = datetime.now(); await db.flush()
return RedirectResponse(url="/service/incidents", status_code=302)
# ===================== QUESTIONNAIRE =====================
@router.get("/service/questionnaire")
async def questionnaire_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
result = await db.execute(
select(QuestionnaireSession, User.full_name).join(User, QuestionnaireSession.created_by == User.id).order_by(QuestionnaireSession.created_at.desc()).limit(20)
)
return templates.TemplateResponse("pages/questionnaire.html", ctx(request, user=user, sessions=result.all(), active_page="questionnaire", title="Опросник"))
@router.get("/service/questionnaire/step/{step}")
async def questionnaire_step(request: Request, step: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer")), session_id: int = Query(None)):
items = (await db.execute(
select(QuestionnaireItem).where(and_(QuestionnaireItem.is_active, QuestionnaireItem.step == step)).order_by(QuestionnaireItem.sort_order, QuestionnaireItem.id)
)).scalars().all()
return templates.TemplateResponse("pages/questionnaire_step.html", ctx(request, user=user, items=items, step=step, session_id=session_id, active_page="questionnaire", title=f"Опросник — шаг {step}"))
@router.post("/service/questionnaire/save/{step}")
async def questionnaire_save(step: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
form = await request.form()
session_id = form.get("session_id")
if session_id:
sess = await db.get(QuestionnaireSession, int(session_id))
else:
sess = QuestionnaireSession(created_by=user["id"], status="draft", current_step=step)
db.add(sess); await db.flush()
sess.current_step = step
items = (await db.execute(
select(QuestionnaireItem).where(and_(QuestionnaireItem.is_active, QuestionnaireItem.step == step))
)).scalars().all()
for item in items:
val = form.get(item.question_key)
if val is not None:
existing = (await db.execute(
select(QuestionnaireAnswer).where(and_(
QuestionnaireAnswer.session_id == sess.id,
QuestionnaireAnswer.question_key == item.question_key
))
)).scalar_one_or_none()
if existing:
existing.answer_value = val
else:
db.add(QuestionnaireAnswer(session_id=sess.id, step=step, question_key=item.question_key, answer_value=val))
await db.flush()
next_step = step + 1
if step >= 5 or form.get("action") == "complete":
sess.status = "completed"
await db.flush()
return RedirectResponse(url="/service/questionnaire", status_code=302)
return RedirectResponse(url=f"/service/questionnaire/step/{next_step}", status_code=302)
# ===================== QUESTIONNAIRE CONFIG =====================
@router.get("/service/questionnaire-config")
async def questionnaire_config(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")), step: int = Query(None)):
sql = select(QuestionnaireItem)
if step: sql = sql.where(QuestionnaireItem.step == step)
items = (await db.execute(sql.order_by(QuestionnaireItem.step, QuestionnaireItem.sort_order, QuestionnaireItem.id))).scalars().all()
return templates.TemplateResponse("pages/questionnaire_config.html", ctx(request, user=user, items=items, sel_step=step, active_page="questionnaire-config", title="Настройка опросника"))
@router.post("/service/questionnaire-config/create")
async def questionnaire_config_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
db.add(QuestionnaireItem(step=int(form.get("step",1)), section=form.get("section",""), question_key=form.get("question_key"),
label=form.get("label"), type=form.get("type","text"), options="{}", required=form.get("required","0")=="1",
sort_order=int(form.get("sort_order",0)), help_text=form.get("help_text","")))
await db.flush()
return RedirectResponse(url="/service/questionnaire-config", status_code=302)
@router.post("/service/questionnaire-config/edit")
async def questionnaire_config_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
qi = await db.get(QuestionnaireItem, int(form.get("id")))
if qi:
for f in ("step","section","question_key","label","type","options","required","sort_order","help_text"):
val = form.get(f)
if val is not None:
if f in ("step","sort_order"): val = int(val)
elif f == "required": val = val == "1"
setattr(qi, f, val)
await db.flush()
return RedirectResponse(url="/service/questionnaire-config", status_code=302)
@router.post("/service/questionnaire-config/toggle")
async def questionnaire_config_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
qi = await db.get(QuestionnaireItem, int((await request.form()).get("id")))
if qi: qi.is_active = not qi.is_active; await db.flush()
return RedirectResponse(url="/service/questionnaire-config", status_code=302)
@router.post("/service/questionnaire-config/delete")
async def questionnaire_config_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
qi = await db.get(QuestionnaireItem, int((await request.form()).get("id")))
if qi: await db.delete(qi); await db.flush()
return RedirectResponse(url="/service/questionnaire-config", status_code=302)
# ===================== PASSPORTS =====================
@router.get("/service/passports")
async def passports_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner", "engineer"))):
result = await db.execute(
select(ObjectPassport).options(
joinedload(ObjectPassport.object),
joinedload(ObjectPassport.creator)
).order_by(ObjectPassport.created_at.desc())
)
return templates.TemplateResponse("pages/passports.html", ctx(request, user=user, passports=result.unique().scalars().all(), active_page="passports", title="Паспорта объектов"))
# ===================== CHECKLIST =====================
@router.get("/service/checklist")
async def checklist_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("technician", "owner"))):
result = await db.execute(
select(Task).options(joinedload(Task.object)).where(
and_(Task.assigned_to == user["id"], Task.status.in_(["open","in_progress"]))
).order_by(Task.created_at.desc())
)
return templates.TemplateResponse("pages/checklist.html", ctx(request, user=user, tasks=result.unique().scalars().all(), active_page="checklist", title="Чек-лист"))
@router.post("/service/checklist/complete")
async def checklist_complete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("technician", "owner"))):
form = await request.form()
t = await db.get(Task, int(form.get("task_id")))
if t and t.assigned_to == user["id"]:
checks = {k: form.get(k) == "1" for k in ("arrival_fixed","photo_before","power_check","cameras_check","acs_check","fire_check","fault_found","fault_fixed","system_test","photo_after")}
t.status = "completed"; t.closed_at = datetime.now(); t.checklist_data = checks; t.result_notes = form.get("notes","")
await db.flush()
return RedirectResponse(url="/service/checklist", status_code=302)
# ===================== BLOG =====================
@router.get("/service/blog")
async def blog_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
result = await db.execute(select(BlogPost, User.full_name).join(User, BlogPost.author_id == User.id).order_by(BlogPost.created_at.desc()))
return templates.TemplateResponse("pages/blog.html", ctx(request, user=user, posts=result.all(), active_page="blog", title="Управление блогом"))
@router.post("/service/blog/create")
async def blog_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
db.add(BlogPost(title=form.get("title"), slug=form.get("slug"), category=form.get("category",""),
status=form.get("status","draft"), excerpt=form.get("excerpt",""), content=form.get("content",""), author_id=user["id"]))
await db.flush()
return RedirectResponse(url="/service/blog", status_code=302)
@router.post("/service/blog/edit")
async def blog_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
bp = await db.get(BlogPost, int(form.get("id")))
if bp:
for f in ("title","slug","category","status","excerpt","content"):
if form.get(f) is not None: setattr(bp, f, form.get(f))
if form.get("status") == "published" and not bp.published_at: bp.published_at = datetime.now()
await db.flush()
return RedirectResponse(url="/service/blog", status_code=302)
@router.post("/service/blog/delete")
async def blog_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
bp = await db.get(BlogPost, int((await request.form()).get("id")))
if bp: await db.delete(bp); await db.flush()
return RedirectResponse(url="/service/blog", status_code=302)
# ===================== QUICK MENU =====================
@router.get("/service/api/quick-menu")
async def quick_menu_api(request: Request, db: AsyncSession = Depends(get_db)):
user = request.session.get("user")
if not user:
return JSONResponse([])
items = await load_quick_menu(db, user["role"])
return JSONResponse([{"id": i.id, "label": i.label, "url": i.url} for i in items])
@router.get("/service/quick-menu")
async def quick_menu_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
if user["role"] not in ("owner", "engineer"):
return HTMLResponse("Доступ запрещён", status_code=403)
items = await load_quick_menu(db, user["role"])
return templates.TemplateResponse("pages/quickmenu.html", ctx(request, user=user, items=items, active_page="quickmenu", title="Быстрое меню"))
@router.post("/service/quick-menu/save")
async def quick_menu_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
if user["role"] not in ("owner", "engineer"):
return HTMLResponse("Доступ запрещён", status_code=403)
form = await request.form()
await db.execute(delete(QuickMenuItem).where(QuickMenuItem.role == user["role"], QuickMenuItem.created_by == user["id"]))
labels = form.getlist("label[]")
urls = form.getlist("url[]")
for i, (label, url) in enumerate(zip(labels, urls)):
if label.strip() and url.strip():
db.add(QuickMenuItem(role=user["role"], label=label.strip(), url=url.strip(), sort_order=i, created_by=user["id"]))
await db.flush()
return RedirectResponse(url="/service/quick-menu", status_code=302)
@router.get("/service/quick-menu/tech")
async def quick_menu_tech_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("engineer"))):
items = await load_quick_menu(db, "technician")
return templates.TemplateResponse("pages/quickmenu.html", ctx(request, user=user, items=items, editing_tech=True, active_page="quickmenu-tech", title="Меню для техников"))
@router.post("/service/quick-menu/tech-save")
async def quick_menu_tech_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("engineer"))):
form = await request.form()
await db.execute(delete(QuickMenuItem).where(QuickMenuItem.role == "technician"))
labels = form.getlist("label[]")
urls = form.getlist("url[]")
for i, (label, url) in enumerate(zip(labels, urls)):
if label.strip() and url.strip():
db.add(QuickMenuItem(role="technician", label=label.strip(), url=url.strip(), sort_order=i, created_by=user["id"]))
await db.flush()
return RedirectResponse(url="/service/quick-menu/tech", status_code=302)
# ===================== IDEAS =====================
@router.get("/service/ideas")
async def ideas_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
result = await db.execute(select(Idea).order_by(Idea.status, Idea.updated_at.desc()))
return templates.TemplateResponse("pages/ideas.html", ctx(request, user=user, ideas=result.scalars().all(), active_page="ideas", title="Идеи"))
@router.post("/service/ideas/create")
async def ideas_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
db.add(Idea(title=form.get("title"), description=form.get("description",""), created_by=user["id"]))
await db.flush()
return RedirectResponse(url="/service/ideas", status_code=302)
@router.post("/service/ideas/edit")
async def ideas_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
try:
idea_id = int(form.get("id"))
except (ValueError, TypeError):
return RedirectResponse(url="/service/ideas", status_code=302)
idea = await db.get(Idea, idea_id)
if idea:
idea.title = form.get("title", idea.title)
idea.description = form.get("description", idea.description)
await db.flush()
return RedirectResponse(url="/service/ideas", status_code=302)
@router.post("/service/ideas/status")
async def ideas_status(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
try:
idea_id = int(form.get("id"))
except (TypeError, ValueError):
return RedirectResponse(url="/service/ideas", status_code=302)
idea = await db.get(Idea, idea_id)
if idea:
statuses = {"normal": "in_progress", "in_progress": "completed", "completed": "normal"}
idea.status = statuses.get(idea.status, "normal")
await db.flush()
return RedirectResponse(url="/service/ideas", status_code=302)
@router.post("/service/ideas/delete")
async def ideas_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
idea = await db.get(Idea, int((await request.form()).get("id")))
if idea:
await db.delete(idea)
await db.flush()
return RedirectResponse(url="/service/ideas", status_code=302)
# ===================== CEO CALC STUBS =====================
@router.post("/service/ceo/calc")
async def ceo_calc(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
# SHS stub — until the calculation pipeline is designed
return JSONResponse({"status": "stub", "message": "Расчёт SHS будет доработан после согласования формул с заказчиком"})
@router.post("/service/ceo/kpi")
async def ceo_kpi(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
# KPI stub — until the calculation pipeline is designed
return JSONResponse({"status": "stub", "message": "Расчёт KPI будет доработан после согласования формул с заказчиком"})
# ===================== CHARTS =====================
import calendar
from collections import defaultdict
@router.get("/service/charts")
async def charts_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/charts.html", ctx(request, user=user, active_page="charts", title="Графики и диаграммы"))
@router.get("/service/api/charts/data")
async def charts_data(
metric: str = Query("tasks"),
group: str = Query("month"),
db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner")),
):
from app.models.models import Task, Incident, SHSRecord, EngineerKPI, SLAContract, Object
import datetime
labels = []
values = []
datasets = []
if metric == "tasks":
result = await db.execute(
select(func.date_trunc(group, Task.created_at).label("period"), func.count(Task.id))
.where(Task.created_at != None)
.group_by(text("period"))
.order_by(text("period"))
)
rows = result.all()
labels = [r[0].strftime("%Y-%m") if r[0] else "" for r in rows]
values = [r[1] for r in rows]
datasets = [
{"label": "Все задачи", "data": values},
]
# Also get by status
for status in ("open", "in_progress", "completed"):
r2 = await db.execute(
select(func.date_trunc(group, Task.created_at).label("period"), func.count(Task.id))
.where(Task.status == status, Task.created_at != None)
.group_by(text("period"))
.order_by(text("period"))
)
rows2 = r2.all()
if rows2:
datasets.append({"label": status, "data": [r[1] for r in rows2]})
elif metric == "incidents":
for sev in ("P1", "P2", "P3"):
result = await db.execute(
select(func.date_trunc(group, Incident.created_at).label("period"), func.count(Incident.id))
.where(Incident.severity == sev, Incident.created_at != None)
.group_by(text("period"))
.order_by(text("period"))
)
rows = result.all()
if rows:
if not labels:
labels = [r[0].strftime("%Y-%m") if r[0] else "" for r in rows]
datasets.append({"label": sev, "data": [r[1] for r in rows]})
elif metric == "shs":
result = await db.execute(
select(SHSRecord).order_by(SHSRecord.id.asc())
)
records = result.scalars().all()
labels = [r.period_end.strftime("%Y-%m") if r.period_end else "" for r in records]
datasets = [
{"label": "SHS Score", "data": [float(r.shs_score) for r in records]},
{"label": "SLA Stability", "data": [float(r.sla_stability) for r in records]},
{"label": "Revenue Stability", "data": [float(r.revenue_stability) for r in records]},
]
elif metric == "kpi":
result = await db.execute(
select(EngineerKPI).order_by(EngineerKPI.period_end.asc()).limit(50)
)
kpis = result.scalars().all()
labels = [r.period_end.strftime("%Y-%m") for r in kpis]
datasets = [
{"label": "Engineer Score", "data": [float(r.engineer_score or 0) for r in kpis]},
{"label": "SLA Compliance", "data": [float(r.sla_compliance or 0) for r in kpis]},
]
elif metric == "mrr":
result = await db.execute(
select(SLAContract.status, func.sum(SLAContract.sla_price_monthly))
.group_by(SLAContract.status)
)
rows = result.all()
labels = [r[0] for r in rows]
values = [float(r[1] or 0) for r in rows]
datasets = [{"label": "MRR", "data": values}]
elif metric == "objects":
result = await db.execute(
select(Object.status, func.count(Object.id)).group_by(Object.status)
)
rows = result.all()
labels = [r[0] for r in rows]
values = [r[1] for r in rows]
datasets = [{"label": "Объекты", "data": values}]
return JSONResponse({"labels": labels, "datasets": datasets})
# ===================== CASES =====================
@router.get("/service/cases")
async def cases_list(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
cases = (await db.execute(select(Case).order_by(Case.sort_order, Case.id))).scalars().all()
return templates.TemplateResponse("pages/cases.html", ctx(request, user=user, cases=cases, active_page="cases", title="Примеры из практики"))
@router.post("/service/cases/create")
async def cases_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
db.add(Case(title=form.get("title"), text=form.get("text"), effect=form.get("effect",""), sort_order=int(form.get("sort_order",0)), is_active=form.get("is_active","1")=="1"))
await db.flush()
return RedirectResponse(url="/service/cases", status_code=302)
@router.post("/service/cases/edit")
async def cases_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
form = await request.form()
c = await db.get(Case, int(form.get("id")))
if c:
for f in ("title","text","effect","sort_order","is_active"):
val = form.get(f)
if val is not None:
if f == "sort_order": val = int(val)
elif f == "is_active": val = val == "1"
setattr(c, f, val)
await db.flush()
return RedirectResponse(url="/service/cases", status_code=302)
@router.post("/service/cases/delete")
async def cases_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
c = await db.get(Case, int((await request.form()).get("id")))
if c: await db.delete(c); await db.flush()
return RedirectResponse(url="/service/cases", status_code=302)
# ===================== BOT SETTINGS =====================
@router.get("/service/bot-settings")
async def bot_settings(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_settings.html", ctx(request, user=user, active_page="bot-settings", title="Настройки бота"))
@router.get("/service/bot-settings/responses")
async def bot_responses(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_responses.html", ctx(request, user=user, active_page="bot-responses", title="Шаблоны ответов бота"))
@router.get("/service/bot-settings/features")
async def bot_features(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_features.html", ctx(request, user=user, active_page="bot-features", title="Функции бота"))
@router.get("/service/bot-settings/categories")
async def bot_categories(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_categories.html", ctx(request, user=user, active_page="bot-categories", title="Категории обращений"))
@router.get("/service/bot-settings/kb")
async def bot_kb(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_kb.html", ctx(request, user=user, active_page="bot-kb", title="База знаний бота"))
@router.get("/service/bot-settings/conversations")
async def bot_conversations(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_conversations.html", ctx(request, user=user, active_page="bot-conversations", title="Диалоги бота"))
@router.get("/service/bot-settings/users")
async def bot_users(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_users.html", ctx(request, user=user, active_page="bot-users", title="Пользователи бота"))
@router.get("/service/bot-settings/test-runner")
async def bot_test_runner(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_test_runner.html", ctx(request, user=user, active_page="bot-test-runner", title="Тестовый прогон бота"))
@router.get("/service/bot-settings/analytics")
async def bot_analytics(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_analytics.html", ctx(request, user=user, active_page="bot-analytics", title="Аналитика бота"))
@router.get("/service/portal-settings")
async def portal_settings_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/portal_settings.html", ctx(request, user=user, active_page="portal-settings", title="Настройка портала"))
@router.get("/service/role-settings")
async def role_settings_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/role_settings.html", ctx(request, user=user, active_page="role-settings", title="Настройка Ролей"))
# ---- Role permissions API ----
@router.get("/service/api/role-permissions")
async def api_role_permissions_get(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
from app.models.models import RoleMenuPermission
from sqlalchemy import select
result = {"engineer": [], "technician": []}
rows = (await db.execute(select(RoleMenuPermission))).scalars().all()
for row in rows:
result.setdefault(row.role, []).append({"menu_key": row.menu_key, "visible": row.visible})
return JSONResponse(result)
@router.post("/service/api/role-permissions/save")
async def api_role_permissions_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
from app.models.models import RoleMenuPermission
from sqlalchemy import delete
body = await request.json()
role = body.get("role")
permissions = body.get("permissions", [])
if role not in ("engineer", "technician"):
return JSONResponse({"ok": False, "error": "Invalid role"}, status_code=400)
await db.execute(delete(RoleMenuPermission).where(RoleMenuPermission.role == role))
for p in permissions:
db.add(RoleMenuPermission(role=role, menu_key=p["menu_key"], visible=p["visible"]))
await db.commit()
return JSONResponse({"ok": True})
# ===================== ROLE SWITCH =====================
@router.post("/service/api/role/switch")
async def api_role_switch(request: Request, user: dict = Depends(get_current_user)):
if not user:
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
body = await request.json()
target_role = body.get("role")
current_role = user["role"]
allowed_roles = {"owner": ["owner", "engineer", "technician"], "engineer": ["engineer", "technician"]}
if current_role not in allowed_roles or target_role not in allowed_roles.get(current_role, []):
return JSONResponse({"ok": False, "error": "Forbidden"}, status_code=403)
if "original_user" not in request.session:
request.session["original_user"] = dict(user)
request.session["user"] = {**user, "role": target_role}
return JSONResponse({"ok": True, "role": target_role, "original_role": request.session["original_user"]["role"]})
@router.post("/service/api/role/reset")
async def api_role_reset(request: Request, user: dict = Depends(get_current_user)):
if not user:
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
if "original_user" in request.session:
request.session["user"] = request.session["original_user"]
del request.session["original_user"]
return JSONResponse({"ok": True, "role": request.session["user"]["role"]})
# ===================== BOT API (proxied from max_bot) =====================
import aiohttp
MAX_BOT_URL = "http://127.0.0.1:8002"
async def _proxy(method, path, json_body=None):
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
async with session.request(method, f"{MAX_BOT_URL}{path}", json=json_body) as resp:
return JSONResponse(await resp.json())
except aiohttp.ClientError as e:
return JSONResponse({"error": f"Bot service unavailable: {e}", "ok": False}, status_code=503)
except Exception as e:
return JSONResponse({"error": str(e), "ok": False}, status_code=500)
@router.get("/service/api/bot/settings")
async def api_bot_settings(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/settings")
@router.post("/service/api/bot/settings/save")
async def api_bot_settings_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/settings/save", body)
@router.get("/service/api/bot/templates")
async def api_bot_templates(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/templates")
@router.post("/service/api/bot/templates/save")
async def api_bot_templates_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/templates/save", body)
@router.get("/service/api/bot/features")
async def api_bot_features(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/features")
@router.post("/service/api/bot/features/toggle")
async def api_bot_features_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/features/toggle", body)
@router.get("/service/api/bot/categories")
async def api_bot_categories(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/categories")
@router.post("/service/api/bot/categories/create")
async def api_bot_categories_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/categories/create", body)
@router.post("/service/api/bot/categories/edit")
async def api_bot_categories_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/categories/edit", body)
@router.post("/service/api/bot/categories/delete")
async def api_bot_categories_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/categories/delete", body)
@router.get("/service/api/bot/kb")
async def api_bot_kb(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/kb")
@router.post("/service/api/bot/kb/create")
async def api_bot_kb_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/kb/create", body)
@router.post("/service/api/bot/kb/edit")
async def api_bot_kb_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/kb/edit", body)
@router.post("/service/api/bot/kb/delete")
async def api_bot_kb_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/kb/delete", body)
@router.get("/service/api/bot/conversations")
async def api_bot_conversations(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/conversations")
@router.get("/service/api/bot/users")
async def api_bot_users(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
page: int = Query(1), limit: int = Query(20),
sort_by: str = Query("created_at"), sort_dir: str = Query("desc"),
consent: str = Query(None)):
params = f"?page={page}&limit={limit}&sort_by={sort_by}&sort_dir={sort_dir}"
if consent:
params += f"&consent={consent}"
return await _proxy("GET", f"/api/bot/users{params}")
@router.get("/service/api/bot/analytics")
async def api_bot_analytics(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/analytics")
@router.post("/service/api/bot/test-run")
async def api_bot_test_run(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/test-run", body)
@router.get("/service/api/bot/broadcast/recipients")
async def api_bot_broadcast_recipients(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
q: str = Query(None), page: int = Query(1), limit: int = Query(200)):
params = f"?page={page}&limit={limit}"
if q:
params += f"&q={q}"
return await _proxy("GET", f"/api/bot/broadcast/recipients{params}")
@router.get("/service/api/bot/broadcast/history")
async def api_bot_broadcast_history(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
page: int = Query(1), limit: int = Query(20)):
return await _proxy("GET", f"/api/bot/broadcast/history?page={page}&limit={limit}")
@router.post("/service/api/bot/broadcast")
async def api_bot_broadcast(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/broadcast", body)
@router.get("/service/bot-settings/handoffs")
async def bot_handoffs_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_handoffs.html", ctx(request, user=user, active_page="bot-handoffs", title="Handoff-диалоги"))
@router.get("/service/bot-settings/handoffs/poll")
async def bot_handoffs_poll(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/handoffs")
@router.post("/service/bot-settings/handoffs/{handoff_id}/take")
async def bot_handoff_take(handoff_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/take")
@router.get("/service/bot-settings/notifications")
async def bot_notifications_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_notifications.html", ctx(request, user=user, active_page="bot-notifications", title="Уведомления бота"))
@router.get("/service/bot-settings/notifications/data")
async def bot_notifications_data(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/notifications")
@router.post("/service/bot-settings/notifications/{notif_id}/read")
async def bot_notification_read(notif_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("POST", f"/api/bot/notifications/{notif_id}/read")
# ===================== BOT RISK QUESTIONS =====================
@router.get("/service/bot-settings/risk-questions")
async def bot_risk_questions_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_risk_questions.html", ctx(request, user=user, active_page="bot-risk-questions", title="Вопросы риск-инжиниринга"))
@router.get("/service/bot-settings/storage")
async def bot_storage_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_storage.html", ctx(request, user=user, active_page="bot-storage", title="Яндекс.Диск"))
@router.get("/service/bot-settings/broadcasts")
async def bot_broadcasts_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_broadcasts.html", ctx(request, user=user, active_page="bot-broadcasts", title="Рассылки"))
@router.get("/service/api/bot/risk-questions")
async def api_bot_risk_questions(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/risk-questions")
@router.post("/service/api/bot/risk-questions/create")
async def api_bot_risk_questions_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/create", body)
@router.post("/service/api/bot/risk-questions/edit")
async def api_bot_risk_questions_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/edit", body)
@router.post("/service/api/bot/risk-questions/toggle")
async def api_bot_risk_questions_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/toggle", body)
@router.post("/service/api/bot/risk-questions/delete")
async def api_bot_risk_questions_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/delete", body)
# ===================== BOT TICKETS =====================
@router.get("/service/bot-settings/tickets")
async def bot_tickets_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_tickets.html", ctx(request, user=user, active_page="bot-tickets", title="Заявки"))
@router.get("/service/api/bot/tickets")
async def api_bot_tickets(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
status: str = Query(None), priority: str = Query(None)):
params = ""
if status: params += f"status={status}&"
if priority: params += f"priority={priority}"
return await _proxy("GET", f"/api/bot/tickets?{params}")
@router.get("/service/api/bot/tickets/{ticket_id}")
async def api_bot_ticket_get(ticket_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", f"/api/bot/tickets/{ticket_id}")
@router.post("/service/api/bot/tickets/create")
async def api_bot_tickets_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/tickets/create", body)
@router.post("/service/api/bot/tickets/{ticket_id}/message")
async def api_bot_tickets_message(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/message", body)
@router.post("/service/api/bot/tickets/{ticket_id}/status")
async def api_bot_tickets_status(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/status", body)
@router.get("/service/api/bot/engineers")
async def api_bot_engineers(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/engineers")
# ===================== BOT STORAGE (Yandex Disk) =====================
async def _get_yandex_client(db: AsyncSession):
from app.yandex_disk import YandexDiskClient
result = await db.execute(text("SELECT value FROM bot_settings WHERE key = 'yandex_disk_token'"))
row = result.scalar_one_or_none()
if not row:
return None
return YandexDiskClient(row)
@router.get("/service/api/bot/storage/list")
async def api_bot_storage_list(path: str = Query(""), db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
items = await client.list_folder(path)
return JSONResponse(items)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
@router.get("/service/api/bot/storage/search")
async def api_bot_storage_search(query: str = Query(""), db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
items = await client.search(query)
return JSONResponse(items)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
@router.get("/service/api/bot/storage/public-link")
async def api_bot_storage_public_link(path: str = Query(""), expiry: int = Query(30),
db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
url = await client.get_public_url(path, expiry)
return JSONResponse({"url": url})
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
@router.get("/service/api/bot/storage/download")
async def api_bot_storage_download(path: str = Query(""), db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
info = await client.get_file_info(path)
dl_url = info.get("public_url", "")
if not dl_url:
dl_info = await client._request("GET", "/resources/download", params={"path": path})
dl_url = dl_info.get("href", "")
return JSONResponse({"url": dl_url})
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
# ===================== HANDOFF CLOSE =====================
@router.post("/service/api/bot/handoffs/{handoff_id}/close")
async def api_bot_handoff_close(handoff_id: int, db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/close")