05/22 Enhance codes and fix bugs 3
This commit is contained in:
@@ -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;">📊 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;">'
|
||||
'⚠ 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 • Weekly digest • 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}')
|
||||
Reference in New Issue
Block a user