336 lines
12 KiB
Python
336 lines
12 KiB
Python
from .routes_shared import * # noqa: F401,F403
|
|
from .routes_shared import _format_datetime
|
|
|
|
@main_bp.route("/tickets")
|
|
@login_required
|
|
@roles_required("admin", "operator", "viewer")
|
|
def tickets_page():
|
|
tab = (request.args.get("tab") or "tickets").strip().lower()
|
|
if tab not in ("tickets", "remarks"):
|
|
tab = "tickets"
|
|
|
|
active = (request.args.get("active") or "1").strip()
|
|
active_only = active != "0"
|
|
|
|
q = (request.args.get("q") or "").strip()
|
|
try:
|
|
customer_id = int(request.args.get("customer_id") or 0)
|
|
except Exception:
|
|
customer_id = 0
|
|
|
|
backup_software = (request.args.get("backup_software") or "").strip()
|
|
backup_type = (request.args.get("backup_type") or "").strip()
|
|
|
|
customers = Customer.query.order_by(Customer.name.asc()).all()
|
|
|
|
tickets = []
|
|
remarks = []
|
|
|
|
if tab == "tickets":
|
|
query = Ticket.query
|
|
if active_only:
|
|
query = query.filter(Ticket.resolved_at.is_(None))
|
|
if q:
|
|
like_q = f"%{q}%"
|
|
query = query.filter(
|
|
(Ticket.ticket_code.ilike(like_q))
|
|
| (Ticket.description.ilike(like_q))
|
|
)
|
|
|
|
if customer_id or backup_software or backup_type:
|
|
query = query.join(TicketScope, TicketScope.ticket_id == Ticket.id)
|
|
if customer_id:
|
|
query = query.filter(TicketScope.customer_id == customer_id)
|
|
if backup_software:
|
|
query = query.filter(TicketScope.backup_software == backup_software)
|
|
if backup_type:
|
|
query = query.filter(TicketScope.backup_type == backup_type)
|
|
|
|
query = query.order_by(Ticket.resolved_at.isnot(None), Ticket.start_date.desc())
|
|
tickets_raw = query.limit(500).all()
|
|
|
|
ticket_ids = [t.id for t in tickets_raw]
|
|
customer_map = {}
|
|
run_count_map = {}
|
|
|
|
if ticket_ids:
|
|
try:
|
|
rows = (
|
|
db.session.execute(
|
|
text(
|
|
"""
|
|
SELECT ts.ticket_id, c.name
|
|
FROM ticket_scopes ts
|
|
JOIN customers c ON c.id = ts.customer_id
|
|
WHERE ts.ticket_id = ANY(:ids)
|
|
AND ts.customer_id IS NOT NULL
|
|
"""
|
|
),
|
|
{"ids": ticket_ids},
|
|
)
|
|
.fetchall()
|
|
)
|
|
for tid, cname in rows:
|
|
customer_map.setdefault(int(tid), [])
|
|
if cname and cname not in customer_map[int(tid)]:
|
|
customer_map[int(tid)].append(cname)
|
|
except Exception:
|
|
customer_map = {}
|
|
|
|
try:
|
|
rows = (
|
|
db.session.execute(
|
|
text(
|
|
"""
|
|
SELECT ticket_id, COUNT(*)
|
|
FROM ticket_job_runs
|
|
WHERE ticket_id = ANY(:ids)
|
|
GROUP BY ticket_id
|
|
"""
|
|
),
|
|
{"ids": ticket_ids},
|
|
)
|
|
.fetchall()
|
|
)
|
|
for tid, cnt in rows:
|
|
run_count_map[int(tid)] = int(cnt or 0)
|
|
except Exception:
|
|
run_count_map = {}
|
|
|
|
for t in tickets_raw:
|
|
customers_for_ticket = customer_map.get(t.id) or []
|
|
if customers_for_ticket:
|
|
customer_display = customers_for_ticket[0]
|
|
if len(customers_for_ticket) > 1:
|
|
customer_display += f" +{len(customers_for_ticket)-1}"
|
|
else:
|
|
customer_display = "-"
|
|
|
|
# Scope summary: best-effort from first scope
|
|
scope_summary = "-"
|
|
first_job_id = None
|
|
try:
|
|
s = TicketScope.query.filter(TicketScope.ticket_id == t.id).order_by(TicketScope.id.asc()).first()
|
|
if s:
|
|
parts = []
|
|
if s.backup_software:
|
|
parts.append(s.backup_software)
|
|
if s.backup_type:
|
|
parts.append(s.backup_type)
|
|
if s.job_id:
|
|
first_job_id = int(s.job_id)
|
|
job = Job.query.get(s.job_id)
|
|
if job and job.job_name:
|
|
parts.append(job.job_name)
|
|
scope_summary = " / ".join([p for p in parts if p]) or "-"
|
|
except Exception:
|
|
scope_summary = "-"
|
|
|
|
tickets.append(
|
|
{
|
|
"id": t.id,
|
|
"ticket_code": t.ticket_code,
|
|
"description": t.description or "",
|
|
"active_from_date": str(getattr(t, "active_from_date", "") or ""),
|
|
"start_date": _format_datetime(t.start_date),
|
|
"resolved_at": _format_datetime(t.resolved_at) if t.resolved_at else "",
|
|
"active": t.resolved_at is None,
|
|
"customers": customer_display,
|
|
"scope_summary": scope_summary,
|
|
"linked_runs": run_count_map.get(t.id, 0),
|
|
"job_id": first_job_id,
|
|
}
|
|
)
|
|
|
|
else:
|
|
query = Remark.query
|
|
if active_only:
|
|
query = query.filter(Remark.resolved_at.is_(None))
|
|
if q:
|
|
like_q = f"%{q}%"
|
|
query = query.filter(Remark.body.ilike(like_q))
|
|
|
|
if customer_id or backup_software or backup_type:
|
|
query = query.join(RemarkScope, RemarkScope.remark_id == Remark.id)
|
|
if customer_id:
|
|
query = query.filter(RemarkScope.customer_id == customer_id)
|
|
if backup_software:
|
|
query = query.filter(RemarkScope.backup_software == backup_software)
|
|
if backup_type:
|
|
query = query.filter(RemarkScope.backup_type == backup_type)
|
|
|
|
query = query.order_by(Remark.resolved_at.isnot(None), Remark.start_date.desc())
|
|
remarks_raw = query.limit(500).all()
|
|
|
|
remark_ids = [r.id for r in remarks_raw]
|
|
customer_map = {}
|
|
run_count_map = {}
|
|
|
|
if remark_ids:
|
|
try:
|
|
rows = (
|
|
db.session.execute(
|
|
text(
|
|
"""
|
|
SELECT rs.remark_id, c.name
|
|
FROM remark_scopes rs
|
|
JOIN customers c ON c.id = rs.customer_id
|
|
WHERE rs.remark_id = ANY(:ids)
|
|
AND rs.customer_id IS NOT NULL
|
|
"""
|
|
),
|
|
{"ids": remark_ids},
|
|
)
|
|
.fetchall()
|
|
)
|
|
for rid, cname in rows:
|
|
customer_map.setdefault(int(rid), [])
|
|
if cname and cname not in customer_map[int(rid)]:
|
|
customer_map[int(rid)].append(cname)
|
|
except Exception:
|
|
customer_map = {}
|
|
|
|
try:
|
|
rows = (
|
|
db.session.execute(
|
|
text(
|
|
"""
|
|
SELECT remark_id, COUNT(*)
|
|
FROM remark_job_runs
|
|
WHERE remark_id = ANY(:ids)
|
|
GROUP BY remark_id
|
|
"""
|
|
),
|
|
{"ids": remark_ids},
|
|
)
|
|
.fetchall()
|
|
)
|
|
for rid, cnt in rows:
|
|
run_count_map[int(rid)] = int(cnt or 0)
|
|
except Exception:
|
|
run_count_map = {}
|
|
|
|
for r in remarks_raw:
|
|
customers_for_remark = customer_map.get(r.id) or []
|
|
if customers_for_remark:
|
|
customer_display = customers_for_remark[0]
|
|
if len(customers_for_remark) > 1:
|
|
customer_display += f" +{len(customers_for_remark)-1}"
|
|
else:
|
|
customer_display = "-"
|
|
|
|
scope_summary = "-"
|
|
first_job_id = None
|
|
try:
|
|
s = RemarkScope.query.filter(RemarkScope.remark_id == r.id).order_by(RemarkScope.id.asc()).first()
|
|
if s:
|
|
parts = []
|
|
if s.backup_software:
|
|
parts.append(s.backup_software)
|
|
if s.backup_type:
|
|
parts.append(s.backup_type)
|
|
if s.job_id:
|
|
first_job_id = int(s.job_id)
|
|
job = Job.query.get(s.job_id)
|
|
if job and job.job_name:
|
|
parts.append(job.job_name)
|
|
scope_summary = " / ".join([p for p in parts if p]) or "-"
|
|
except Exception:
|
|
scope_summary = "-"
|
|
|
|
preview = (r.body or "")
|
|
if len(preview) > 80:
|
|
preview = preview[:77] + "..."
|
|
|
|
remarks.append(
|
|
{
|
|
"id": r.id,
|
|
"preview": preview,
|
|
"start_date": _format_datetime(r.start_date) if r.start_date else "-",
|
|
"resolved_at": _format_datetime(r.resolved_at) if r.resolved_at else "",
|
|
"active": r.resolved_at is None,
|
|
"customers": customer_display,
|
|
"scope_summary": scope_summary,
|
|
"linked_runs": run_count_map.get(r.id, 0),
|
|
"job_id": first_job_id,
|
|
}
|
|
)
|
|
|
|
return render_template(
|
|
"main/tickets.html",
|
|
tab=tab,
|
|
active_only=active_only,
|
|
q=q,
|
|
customer_id=customer_id,
|
|
backup_software=backup_software,
|
|
backup_type=backup_type,
|
|
customers=customers,
|
|
tickets=tickets,
|
|
remarks=remarks,
|
|
)
|
|
|
|
|
|
@main_bp.route("/tickets/<int:ticket_id>", methods=["GET", "POST"])
|
|
@login_required
|
|
@roles_required("admin", "operator", "viewer")
|
|
def ticket_detail(ticket_id: int):
|
|
ticket = Ticket.query.get_or_404(ticket_id)
|
|
|
|
if request.method == "POST":
|
|
if get_active_role() not in ("admin", "operator"):
|
|
abort(403)
|
|
ticket.description = (request.form.get("description") or "").strip() or None
|
|
try:
|
|
db.session.commit()
|
|
flash("Ticket updated.", "success")
|
|
except Exception as exc:
|
|
db.session.rollback()
|
|
flash(f"Failed to update ticket: {exc}", "danger")
|
|
return redirect(url_for("main.ticket_detail", ticket_id=ticket.id))
|
|
|
|
# Scopes
|
|
scopes = TicketScope.query.filter(TicketScope.ticket_id == ticket.id).order_by(TicketScope.id.asc()).all()
|
|
|
|
# Linked runs
|
|
runs = []
|
|
try:
|
|
rows = (
|
|
db.session.execute(
|
|
text(
|
|
"""
|
|
SELECT jr.id, jr.run_at, jr.status, j.job_name, c.name AS customer_name
|
|
FROM ticket_job_runs tjr
|
|
JOIN job_runs jr ON jr.id = tjr.job_run_id
|
|
JOIN jobs j ON j.id = jr.job_id
|
|
LEFT JOIN customers c ON c.id = j.customer_id
|
|
WHERE tjr.ticket_id = :ticket_id
|
|
ORDER BY jr.run_at DESC
|
|
LIMIT 20
|
|
"""
|
|
),
|
|
{"ticket_id": ticket.id},
|
|
)
|
|
.mappings()
|
|
.all()
|
|
)
|
|
for r in rows:
|
|
runs.append(
|
|
{
|
|
"id": r.get("id"),
|
|
"run_at": _format_datetime(r.get("run_at")),
|
|
"status": r.get("status") or "",
|
|
"job_name": r.get("job_name") or "",
|
|
"customer_name": r.get("customer_name") or "",
|
|
}
|
|
)
|
|
except Exception:
|
|
runs = []
|
|
|
|
return render_template(
|
|
"main/ticket_detail.html",
|
|
ticket=ticket,
|
|
scopes=scopes,
|
|
runs=runs,
|
|
)
|
|
|