Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit 67e411eea8
246 changed files with 23152 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
"""Обратная связь по порталу: отправить может любой сотрудник, читать и отвечать — только администраторы портала."""
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from .. import config, push, schemas as S
from ..db import get_db
from ..models import Feedback, User
from ..security import current_user, is_portal_admin, require_portal_admin
from .catalog import commit, get_or_404
from .inventory import user_names
router = APIRouter(tags=["feedback"])
PER_HOUR = 20 # не больше сообщений в час от одного сотрудника
KIND_RU = {"idea": "Идея", "bug": "Ошибка", "other": "Другое"}
def fb_out(f: Feedback, names: dict) -> S.FeedbackOut:
o = S.FeedbackOut.model_validate(f)
o.user_name, o.answered_by_name = names.get(f.user_id), names.get(f.answered_by_id)
return o
@router.post("/feedback", status_code=201)
def send(data: S.FeedbackIn, db: Session = Depends(get_db), u: User = Depends(current_user)):
since = config.now() - timedelta(hours=1)
if db.scalar(select(func.count()).select_from(Feedback).where(Feedback.user_id == u.id, Feedback.created_at >= since)) >= PER_HOUR:
raise HTTPException(429, "Слишком много сообщений, попробуйте позже")
f = Feedback(user_id=u.id, kind=data.kind, text=data.text.strip(), page=data.page.strip()[:200], client=data.client)
db.add(f)
commit(db)
admins = [x.id for x in db.scalars(select(User).where(User.is_active.is_(True), User.is_admin.is_(True))) if is_portal_admin(x) and x.id != u.id]
push.notify(admins, f"Обратная связь: {KIND_RU.get(f.kind, f.kind)}", f"{u.full_name or u.username}: {f.text[:100]}", "/#/feedback")
return {"ok": True, "id": f.id}
@router.get("/feedback", response_model=list[S.FeedbackOut])
def list_(status: Optional[str] = Query(None, pattern="^(new|taken|done|rejected|open)$"), limit: int = Query(300, ge=1, le=2000),
db: Session = Depends(get_db), _: User = Depends(require_portal_admin)):
stmt = select(Feedback).order_by(Feedback.created_at.desc(), Feedback.id.desc()).limit(limit)
if status == "open":
stmt = stmt.where(Feedback.status.in_(("new", "taken")))
elif status:
stmt = stmt.where(Feedback.status == status)
names = user_names(db)
return [fb_out(f, names) for f in db.scalars(stmt)]
@router.patch("/feedback/{id}", response_model=S.FeedbackOut)
def update(id: int, data: S.FeedbackPatch, db: Session = Depends(get_db), u: User = Depends(require_portal_admin)):
f = get_or_404(db, Feedback, id)
if data.status is not None:
f.status = data.status
if data.answer is not None:
f.answer = data.answer.strip()
f.answered_by_id, f.answered_at = u.id, config.now()
commit(db)
return fb_out(f, user_names(db))
@router.delete("/feedback/{id}", status_code=204)
def delete(id: int, db: Session = Depends(get_db), _: User = Depends(require_portal_admin)):
db.delete(get_or_404(db, Feedback, id))
commit(db)