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): c = {"request": request, "user": user, "now": now_str()} 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() from app.auth import hash_password new_user = User( login=form.get("login"), password_hash=hash_password(form.get("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"Создан пользователь {form.get('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() u = await db.get(User, int(form.get("id"))) 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) u.is_active = form.get("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"))): u = await db.get(User, int((await request.form()).get("id"))) 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.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"))): 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"))): 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)