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": "0–100 баллов"}, "risk_multiplier": {"name": "Множитель риска", "formula": "Множитель = f(уровень риска): низкий→1.0, средний→1.3, высокий→1.6, критический→2.0", "result": "1.0–2.0 ×"}, "complexity": {"name": "Сложность объекта", "formula": "Сложность = video(based on cameras) + access(based on APs) + fire(type) + IT(bool)", "result": "Простой(<30) / Средний(30–70) / Сложный(70–120) / Enterprise(>120)"}, "infra": {"name": "Нагрузка на инфраструктуру", "formula": "Нагрузка = server + network + power (по состояниям)", "result": "0–50 баллов"}, "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": "0–100 → Класс A/B/C/D"}, "engineer": {"name": "Оценка инженера", "formula": "ES = SLA×0.25 + отклик×0.20 + решение×0.20 + диагноз×0.15 + переоткрытия×0.10 + риски×0.10 → grade", "result": "0–100 → Junior/Middle/Strong/Senior"}, "ecs": {"name": "Контроль инженера (ECS)", "formula": "ECS = SLA×0.30 + задачи×0.25 + инциденты×0.20 + команда×0.15 + координация×0.10", "result": "0–100"}, "shs": {"name": "SHS (Service Health Score)", "formula": "SHS = SLA×0.22 + выручка×0.18 + удержание×0.18 + инженеры×0.15 + инциденты×0.12 + продажи×0.10 + эффективность×0.05", "result": "0–100 → 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": "0–100"}, "ssi": {"name": "SLA Stability Index", "formula": "SSI = SLA_compliance × (1 - Breach_Severity)", "result": "0–100"}, "isi": {"name": "Incident Stability Index", "formula": "ISI = max(0, min(100, 100 - weighted_rate×20))", "result": "0–100"}, "bonus": {"name": "Бонус инженера", "formula": "% премии = f(оценка): ≥90→20%, 80–89→10%, <80→0%", "result": "0%–20%"}, "retention": {"name": "Удержание клиентов", "formula": "Удержание = base - lost_key×10% (per key client)", "result": "0–100%"}, "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 []} 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 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", ""), 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) 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": "Риск (0–100)", "type": "number", "default": 50}, {"key": "complexity_score", "label": "Сложность (0–150)", "type": "number", "default": 50}, {"key": "infra_score", "label": "Инфраструктура (0–50)", "type": "number", "default": 10}, {"key": "history_score", "label": "История (0–30)", "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" @router.get("/service/api/bot/settings") async def api_bot_settings(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/settings") as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/settings/save", json=body) as resp: return JSONResponse(await resp.json()) @router.get("/service/api/bot/templates") async def api_bot_templates(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/templates") as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/templates/save", json=body) as resp: return JSONResponse(await resp.json()) @router.get("/service/api/bot/features") async def api_bot_features(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/features") as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/features/toggle", json=body) as resp: return JSONResponse(await resp.json()) @router.get("/service/api/bot/categories") async def api_bot_categories(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/categories") as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/categories/create", json=body) as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/categories/edit", json=body) as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/categories/delete", json=body) as resp: return JSONResponse(await resp.json()) @router.get("/service/api/bot/kb") async def api_bot_kb(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/kb") as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/kb/create", json=body) as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/kb/edit", json=body) as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/kb/delete", json=body) as resp: return JSONResponse(await resp.json()) @router.get("/service/api/bot/conversations") async def api_bot_conversations(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/conversations") as resp: return JSONResponse(await resp.json()) @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}" async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/users{params}") as resp: return JSONResponse(await resp.json()) @router.get("/service/api/bot/analytics") async def api_bot_analytics(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/analytics") as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/test-run", json=body) as resp: return JSONResponse(await resp.json()) @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}" async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/broadcast/recipients{params}") as resp: return JSONResponse(await resp.json()) @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)): async with aiohttp.ClientSession() as session: async with session.get(f"{MAX_BOT_URL}/api/bot/broadcast/history?page={page}&limit={limit}") as resp: return JSONResponse(await resp.json()) @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() async with aiohttp.ClientSession() as session: async with session.post(f"{MAX_BOT_URL}/api/bot/broadcast", json=body) as resp: return JSONResponse(await resp.json())