From d6d9b5e9dccd61ef18bf436eea4ec5fed945d5bc Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 17 Apr 2026 15:52:44 -0400 Subject: [PATCH] 04/17 Update to show number of unread notifications on the title --- app/templates/base.html | 2080 +++++++++++++++++++++++++++------------ 1 file changed, 1459 insertions(+), 621 deletions(-) diff --git a/app/templates/base.html b/app/templates/base.html index 38549b8..2af853e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,705 +1,1543 @@ + - - - - {% block title %}IT Helpdesk{% endblock %} — {{ branding.app_name }} - - - - - - + + + + {% block title %}IT Helpdesk{% endblock %} — {{ branding.app_name }} + + + + + + {% block head %}{% endblock %} + -{% if current_user.is_authenticated %} - - - - -
-
- -
{% block page_title %}{% endblock %}
- - - - {{ current_user.full_name.split()[0] }} - -
- -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% for cat, msg in messages %} - - {% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
-
- - -
-
-
Notifications
- -
-
-
Loading...
-
-
- View all notifications → -
-
- - -
- -
-
-
-
🤖
-
-
IT Assistant
-
● Online
-
- -
-
-
- Hi! I'm your IT Assistant 👋 I can help you report issues, create tickets, or answer IT questions.

- What can I help you with today? -
-
-
- - -
-
- -{% else %} - -
-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% for cat, msg in messages %} - - {% endfor %} - {% endwith %} - {{ self.content() }} -
-
-{% endif %} - - - - + + - -{% block scripts %}{% endblock %} + + function _bindNotifClick(el) { + el.addEventListener('click', async () => { + const id = el.dataset.id; + const link = el.dataset.link; + // Mark as read in DB + if (el.classList.contains('unread')) { + try { await csrfPost(`/api/notifications/${id}/read`); } catch (_) { } + el.classList.remove('unread'); + const dot = el.querySelector('.notif-dot'); + if (dot) dot.remove(); + // Decrement badge + const badge = document.getElementById('notif-badge-count'); + if (badge) { + const cur = parseInt(badge.textContent) || 0; + const next = Math.max(0, cur - 1); + badge.textContent = next > 0 ? next : ''; + badge.classList.toggle('show', next > 0); + } + } + if (link) window.location.href = link; + }); + } + + async function loadNotifications() { + const list = document.getElementById('notif-list'); + list.innerHTML = '
Loading…
'; + try { + const r = await fetch('/api/notifications'); + if (!r.ok) throw new Error('HTTP ' + r.status); + const data = await r.json(); + const notifs = data.notifications || []; + + // Update badge from live count + const unreadCount = notifs.filter(n => !n.is_read).length; + updateBadge(unreadCount); + + if (notifs.length === 0) { + list.innerHTML = '
' + + '' + + 'No notifications yet.
'; + return; + } + + list.innerHTML = notifs.map(_notifItemHtml).join(''); + list.querySelectorAll('.notif-item').forEach(_bindNotifClick); + + } catch (e) { + list.innerHTML = '
Unable to load notifications.
'; + } + } + + function prependNotif(n) { + // Add new real-time notification to the top of the panel if it's open + const list = document.getElementById('notif-list'); + // Remove "no notifications" placeholder if present + if (list.querySelector('.bi-bell-slash')) list.innerHTML = ''; + const tmp = document.createElement('div'); + tmp.innerHTML = _notifItemHtml({ + id: n.id, title: n.title, message: n.message || '', + link: n.link || '', is_read: false, created_at: 'Just now' + }); + const el = tmp.firstElementChild; + _bindNotifClick(el); + list.prepend(el); + } + + async function markAllRead() { + try { await csrfPost('/api/notifications/mark-all-read'); } catch (_) { } + updateBadge(0); + document.querySelectorAll('.notif-item.unread').forEach(el => { + el.classList.remove('unread'); + const dot = el.querySelector('.notif-dot'); + if (dot) dot.remove(); + }); + } + + function showToast(title, msg) { + const t = document.createElement('div'); + t.style.cssText = 'position:fixed;bottom:90px;right:28px;background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--accent3);border-radius:8px;padding:12px 16px;z-index:300;max-width:300px;box-shadow:0 4px 16px rgba(0,0,0,.4);animation:slideIn .25s ease;'; + t.innerHTML = `
${title}
${msg || ''}
`; + document.body.appendChild(t); + setTimeout(() => t.remove(), 5000); + } + + // ── Chatbot ─────────────────────────────────────────────────────────────────── + let chatHistory = []; + let chatOpen = false; + function toggleChat() { + chatOpen = !chatOpen; + document.getElementById('chat-window').classList.toggle('show', chatOpen); + if (chatOpen) document.getElementById('chat-input').focus(); + } + async function sendChat() { + const input = document.getElementById('chat-input'); + const msg = input.value.trim(); + if (!msg) return; + input.value = ''; + appendBubble(msg, 'user'); + chatHistory.push({ role: 'user', content: msg }); + const typing = appendTyping(); + try { + const r = await csrfPost('/chatbot/message', { message: msg, history: chatHistory }); + const d = await r.json(); + typing.remove(); + const reply = d.reply || 'Sorry, I encountered an error.'; + appendBubble(reply, 'bot'); + if (d.ticket) { + // Clear history after ticket creation — keeping the prior conversation + // (which contains the AI's JSON action block) causes the model to + // re-trigger ticket creation on every subsequent message. + chatHistory = []; + const notif = document.createElement('div'); + notif.className = 'chat-bubble bot'; + notif.style.cssText = 'background:rgba(45,212,191,.1);border:1px solid rgba(45,212,191,.3);'; + notif.innerHTML = `🎫 ${d.ticket.ticket_number}View Ticket`; + document.getElementById('chat-messages').appendChild(notif); + } else { + chatHistory.push({ role: 'assistant', content: reply }); + } + } catch (e) { typing.remove(); appendBubble('Sorry, something went wrong.', 'bot'); } + scrollChat(); + } + function appendBubble(text, role) { + const el = document.createElement('div'); + el.className = `chat-bubble ${role}`; + el.innerHTML = text.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\n/g, '
'); + document.getElementById('chat-messages').appendChild(el); + scrollChat(); + return el; + } + function appendTyping() { + const el = document.createElement('div'); + el.className = 'chat-bubble bot'; + el.innerHTML = ''; + document.getElementById('chat-messages').appendChild(el); + scrollChat(); + return el; + } + function scrollChat() { + const m = document.getElementById('chat-messages'); + m.scrollTop = m.scrollHeight; + } + {% endif %} + + + {% block scripts %}{% endblock %} + \ No newline at end of file