05/22 Enhance codes and fix bugs 3

This commit is contained in:
2026-05-22 15:19:34 -04:00
parent 12b5fd9ce0
commit 5c9a124a71
11 changed files with 665 additions and 36 deletions
+167
View File
@@ -546,4 +546,171 @@ def send_satisfaction_survey(ticket):
from flask import has_request_context
t = Thread(target=_send, daemon=has_request_context())
t.start()
# ─── Watcher Notifications ───────────────────────────────────────────────────
def notify_watchers(ticket, event_title, event_message, exclude_user_id=None):
"""Send in-app notifications to all watchers of a ticket.
Watchers are notified of status changes, new comments, and assignments.
The user who triggered the event (exclude_user_id) is skipped so they
don't receive a notification about their own action.
"""
from app.models import TicketWatcher, NotificationType
watchers = TicketWatcher.query.filter_by(ticket_id=ticket.id).all()
base_url = current_app.config.get('APP_BASE_URL', '')
link = f'{base_url}/tickets/{ticket.id}'
for w in watchers:
if w.user_id == exclude_user_id:
continue
create_notification(
user_id = w.user_id,
notif_type = NotificationType.TICKET_UPDATE,
title = event_title,
message = event_message,
ticket_id = ticket.id,
link = link,
)
# ─── Weekly IT Digest ─────────────────────────────────────────────────────────
def send_weekly_digest(app):
"""Send a weekly summary email to all active IT staff / admin users.
Summarises the past 7 days: new tickets, resolved tickets, still-open
tickets with priority breakdown, and any SLA-overdue tickets.
Intended to be called once a week by an APScheduler job.
"""
from datetime import datetime, timedelta
from app.models import Ticket, TicketStatus, TicketPriority
with app.app_context():
try:
now = datetime.utcnow()
week_ago = now - timedelta(days=7)
base_url = app.config.get('APP_BASE_URL', '').rstrip('/')
new_tickets = Ticket.query.filter(Ticket.created_at >= week_ago).all()
resolved = Ticket.query.filter(
Ticket.resolved_at >= week_ago
).all()
open_tickets = Ticket.query.filter(
Ticket.status.notin_([TicketStatus.RESOLVED, TicketStatus.CLOSED])
).order_by(Ticket.created_at.asc()).all()
overdue = [t for t in open_tickets if t.due_date and t.due_date < now]
# Priority breakdown for open tickets
prio_counts = {}
for t in open_tickets:
prio_counts[t.priority] = prio_counts.get(t.priority, 0) + 1
# Build ticket rows HTML
def _ticket_rows(tickets, limit=10):
rows = ''
for t in tickets[:limit]:
prio_color = _priority_badge_color(t.priority)
url = f'{base_url}/tickets/{t.id}'
rows += (
f'<tr>'
f'<td style="padding:6px 8px;font-family:monospace;font-size:12px;color:#0891b2;">'
f'<a href="{url}" style="color:#0891b2;text-decoration:none;">{t.ticket_number}</a></td>'
f'<td style="padding:6px 8px;font-size:13px;">{t.title[:60]}</td>'
f'<td style="padding:6px 8px;"><span style="background:{prio_color};color:#fff;'
f'padding:1px 7px;border-radius:4px;font-size:11px;">{t.priority.upper()}</span></td>'
f'<td style="padding:6px 8px;font-size:12px;color:#64748b;">'
f'{t.assignee.full_name if t.assignee else ""}</td>'
f'</tr>'
)
if len(tickets) > limit:
rows += (
f'<tr><td colspan="4" style="padding:6px 8px;font-size:12px;color:#64748b;text-align:center;">'
f'… and {len(tickets) - limit} more</td></tr>'
)
return rows or '<tr><td colspan="4" style="padding:6px 8px;color:#94a3b8;font-size:13px;">None</td></tr>'
dashboard_url = f'{base_url}/admin/'
html = (
'<html><body style="font-family:Arial,sans-serif;background:#f0f4f8;padding:20px;">'
'<div style="max-width:680px;margin:0 auto;background:#fff;border-radius:10px;'
'overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.08);">'
'<div style="background:#1e293b;padding:24px 32px;">'
f'<h1 style="color:#fff;margin:0;font-size:20px;">&#128202; Weekly IT Summary</h1>'
f'<p style="color:#94a3b8;margin:6px 0 0;font-size:13px;">'
f'{week_ago.strftime("%b %d")} {now.strftime("%b %d, %Y")}</p>'
'</div>'
'<div style="padding:28px 32px;">'
# KPI row
'<div style="display:flex;gap:16px;margin-bottom:24px;">'
f'<div style="flex:1;background:#f0fdf4;border-radius:8px;padding:16px;text-align:center;">'
f'<div style="font-size:28px;font-weight:700;color:#059669;">{len(new_tickets)}</div>'
f'<div style="font-size:12px;color:#64748b;">New this week</div></div>'
f'<div style="flex:1;background:#eff6ff;border-radius:8px;padding:16px;text-align:center;">'
f'<div style="font-size:28px;font-weight:700;color:#2563eb;">{len(resolved)}</div>'
f'<div style="font-size:12px;color:#64748b;">Resolved this week</div></div>'
f'<div style="flex:1;background:#fef2f2;border-radius:8px;padding:16px;text-align:center;">'
f'<div style="font-size:28px;font-weight:700;color:#dc2626;">{len(open_tickets)}</div>'
f'<div style="font-size:12px;color:#64748b;">Still open</div></div>'
f'<div style="flex:1;background:#fffbeb;border-radius:8px;padding:16px;text-align:center;">'
f'<div style="font-size:28px;font-weight:700;color:#d97706;">{len(overdue)}</div>'
f'<div style="font-size:12px;color:#64748b;">Overdue</div></div>'
'</div>'
# Open tickets table
'<h3 style="font-size:14px;font-weight:700;color:#1e293b;margin:0 0 10px;">Open Tickets</h3>'
'<table style="width:100%;border-collapse:collapse;margin-bottom:24px;">'
'<thead><tr style="background:#f8fafc;">'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">TICKET</th>'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">TITLE</th>'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">PRIORITY</th>'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">ASSIGNED</th>'
'</tr></thead>'
f'<tbody>{_ticket_rows(open_tickets)}</tbody>'
'</table>'
# Overdue section (only if any)
+ (
'<h3 style="font-size:14px;font-weight:700;color:#dc2626;margin:0 0 10px;">'
'&#9888; Overdue Tickets</h3>'
'<table style="width:100%;border-collapse:collapse;margin-bottom:24px;">'
'<thead><tr style="background:#fef2f2;">'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">TICKET</th>'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">TITLE</th>'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">PRIORITY</th>'
'<th style="padding:6px 8px;text-align:left;font-size:11px;color:#64748b;">ASSIGNED</th>'
'</tr></thead>'
f'<tbody>{_ticket_rows(overdue)}</tbody>'
'</table>'
if overdue else ''
) +
f'<a href="{dashboard_url}" style="display:inline-block;background:#2563eb;color:#fff;'
f'padding:11px 22px;border-radius:6px;text-decoration:none;font-size:13px;">Open Dashboard</a>'
'</div>'
'<div style="background:#f8fafc;padding:14px 32px;text-align:center;'
'color:#94a3b8;font-size:11px;border-top:1px solid #e2e8f0;">'
'TechDesk IT Helpdesk &bull; Weekly digest &bull; Automated message'
'</div></div></body></html>'
)
staff = User.query.filter(
User.role.in_([UserRole.IT_STAFF, UserRole.ADMIN]),
User.is_active == True,
User.email_notif == True,
).all()
if not staff:
logger.info('[DIGEST] No staff with email notifications enabled — skipping digest')
return
subject = f'[TechDesk] Weekly IT Summary — {now.strftime("%b %d, %Y")}'
for member in staff:
send_email(subject, [member.email], html)
logger.info(f'[DIGEST] Sent weekly digest to {member.email}')
except Exception as exc:
logger.error(f'[DIGEST] Failed to send weekly digest: {exc}', exc_info=True)
logger.info(f'[SURVEY] Email thread started for ticket_id={ticket_id}')