const { useState, useEffect, useMemo, useRef } = React; // API host resolution: // 1. window.JIRA_INTERFACE_API_URL — explicit override (set in auth-config.js for unusual setups) // 2. Same-origin "" — used when FastAPI serves the SPA (production App Runner, or `uvicorn` dev) // 3. http://hostname:8000 — fallback when SPA is on :8080 (legacy split-port dev workflow) const BASE_API_URL = (() => { if (typeof window === "undefined") return ""; if (typeof window.JIRA_INTERFACE_API_URL === "string") return window.JIRA_INTERFACE_API_URL; if (window.location.port === "8080") return `http://${window.location.hostname}:8000`; return ""; })(); /** Adds Cognito access token when auth-config enables useCognito. On 401, tries refresh_token once (60m access token). */ async function apiFetch(url, options) { const opts = { credentials: "include", ...(options || {}) }; const buildHeaders = () => { const headers = { ...(opts.headers || {}) }; if (typeof window !== "undefined" && window.JIRA_INTERFACE_AUTH && window.JIRA_INTERFACE_AUTH.useCognito) { const t = sessionStorage.getItem("cognito_access_token"); if (t) headers.Authorization = `Bearer ${t}`; } return headers; }; let res = await fetch(url, { ...opts, headers: buildHeaders() }); if ( res.status === 401 && typeof window !== "undefined" && window.JIRA_INTERFACE_AUTH && window.JIRA_INTERFACE_AUTH.useCognito && window.CognitoPkce && typeof window.CognitoPkce.refreshAccessToken === "function" ) { const renewed = await window.CognitoPkce.refreshAccessToken(); if (renewed) res = await fetch(url, { ...opts, headers: buildHeaders() }); } return res; } const STATUS_BACKLOG = "Backlog"; const STATUS_WIP = "Work in progress"; const STATUS_ESCALATED = "Escalated"; const STATUS_WAITING_FOR_CUSTOMER = "Waiting for customer"; const STATUS_OPTIONS = [ "Backlog", "Escalated", "Solution Provided", "Waiting for customer", "Work in progress", "Closed", "Done", "Canceled", ]; function formatCurrency(value) { if (value == null || Number.isNaN(value)) return "—"; return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }).format(value); } function formatResolutionHours(hours) { if (hours == null || Number.isNaN(hours)) return "—"; if (hours < 1) return `${Math.round(hours * 60)} min`; if (hours < 24) return `${hours.toFixed(1)} h`; return `${(hours / 24).toFixed(1)} days`; } function formatDurationSince(isoString) { if (!isoString) return ""; const ms = Date.now() - new Date(isoString).getTime(); if (ms < 0) return ""; const mins = Math.floor(ms / 60000); const hours = Math.floor(ms / 3600000); const days = Math.floor(ms / 86400000); if (mins < 60) return `${mins}m`; if (hours < 24) return `${hours}h`; if (days < 7) return `${days}d`; const weeks = Math.floor(days / 7); return `${weeks}w`; } function getTierValueColorClass(tier, hours) { if (hours == null || Number.isNaN(hours)) return ""; if (tier === "Standard" || tier === "No ARR") return ""; const days = hours / 24; if (tier === "Diamond" || tier === "Platinum" || tier === "Gold") { if (days < 8) return "stat-tier-green"; if (days < 10) return "stat-tier-orange"; return "stat-tier-red"; } if (tier === "Silver" || tier === "Bronze") { if (days < 12) return "stat-tier-green"; if (days < 15) return "stat-tier-orange"; return "stat-tier-red"; } return ""; } function stripHtmlToText(html) { if (!html || !html.trim()) return ""; const div = document.createElement("div"); div.innerHTML = html; return (div.innerText || div.textContent || "").trim(); } function extractWithMarks(el) { const parts = []; const walk = (n, m = []) => { if (n.nodeType === Node.TEXT_NODE) { const t = n.textContent || ""; if (t) parts.push({ text: t, marks: [...m] }); return; } if (n.nodeType !== Node.ELEMENT_NODE) return; const tag = n.tagName ? n.tagName.toLowerCase() : ""; const mentionId = n.getAttribute?.("data-mention-id"); if (tag === "span" && mentionId) { parts.push({ type: "mention", attrs: { id: mentionId, text: n.getAttribute("data-mention-name") || n.textContent || "" } }); return; } const newMarks = [...m]; if (tag === "b" || tag === "strong") newMarks.push({ type: "strong" }); else if (tag === "i" || tag === "em") newMarks.push({ type: "em" }); else if (tag === "u") newMarks.push({ type: "underline" }); else if (tag === "s" || tag === "strike") newMarks.push({ type: "strike" }); else if (tag === "code") newMarks.push({ type: "code" }); else if (tag === "sub") newMarks.push({ type: "subsup", attrs: { type: "sub" } }); else if (tag === "sup") newMarks.push({ type: "subsup", attrs: { type: "sup" } }); else if (tag === "a") newMarks.push({ type: "link", attrs: { href: n.getAttribute("href") || "" } }); else if (tag === "br") parts.push({ text: "\n", marks: [...m] }); for (const c of n.childNodes) walk(c, newMarks); }; walk(el); return parts; } function htmlToAdf(html, issueKey = null) { if (!html || !html.trim()) { return { version: 1, type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "" }] }] }; } const doc = document.createElement("div"); doc.innerHTML = html; const content = []; const addParagraph = (parts) => { if (!parts.length) { content.push({ type: "paragraph", content: [{ type: "text", text: "" }] }); return; } content.push({ type: "paragraph", content: parts.map((p) => { if (p.type === "mention") { const text = p.attrs?.text || ""; return { type: "mention", attrs: { id: p.attrs?.id || "", text: text.startsWith("@") ? text : `@${text}` } }; } const n = { type: "text", text: p.text }; if (p.marks && p.marks.length) n.marks = p.marks; return n; }), }); }; const addCodeBlock = (text) => { content.push({ type: "codeBlock", content: [{ type: "text", text: (text || "").replace(/\n$/, "") }], }); }; const addImageBlock = (img) => { const attId = img.getAttribute("data-attachment-id"); const attUrl = img.getAttribute("data-attachment-url"); const filename = img.getAttribute("data-attachment-filename") || img.alt || "image"; if (!attId && !attUrl) return; const href = attUrl || (issueKey ? `${BASE_API_URL}/api/tickets/${encodeURIComponent(issueKey)}/attachments/${encodeURIComponent(attId)}/content` : ""); if (href) { content.push({ type: "paragraph", content: [{ type: "text", text: `[${filename}]`, marks: [{ type: "link", attrs: { href } }], }], }); } }; const flushInline = (parts) => { if (parts.length) addParagraph(parts); }; let inlineParts = []; const walk = (parent) => { for (const node of parent.childNodes) { if (node.nodeType === Node.TEXT_NODE) { const t = node.textContent || ""; if (t) inlineParts.push({ text: t, marks: [] }); continue; } if (node.nodeType !== Node.ELEMENT_NODE) continue; const tag = node.tagName ? node.tagName.toLowerCase() : ""; if (tag === "img") { flushInline(inlineParts); inlineParts = []; addImageBlock(node); } else if (tag === "pre") { flushInline(inlineParts); inlineParts = []; addCodeBlock(node.textContent || ""); } else if (tag === "p" || tag === "li") { flushInline(inlineParts); inlineParts = []; const parts = extractWithMarks(node); if (parts.length) addParagraph(parts); } else if (tag === "div") { walk(node); } else { inlineParts.push(...extractWithMarks(node)); } } }; walk(doc); flushInline(inlineParts); if (!content.length) { const parts = extractWithMarks(doc); if (parts.length) addParagraph(parts); else { const text = (doc.innerText || doc.textContent || "").trim(); if (text) text.split("\n").forEach((line) => addParagraph([{ text: line, marks: [] }])); else addParagraph([]); } } if (content.length === 0) content.push({ type: "paragraph", content: [{ type: "text", text: "" }] }); return { version: 1, type: "doc", content }; } function adfToHtml(adf, opts = {}) { const { jiraBaseUrl = "", issueKey = "", attachments = [] } = typeof opts === "string" ? { jiraBaseUrl: opts } : opts; if (!adf || !adf.content) return ""; const out = []; for (const block of adf.content) { if (block.type === "mediaSingle") { const media = (block.content || [])[0]; if (media?.type === "media" && media.attrs?.id) { const mediaId = media.attrs.id; const alt = media.attrs.alt || "image"; const byFilename = attachments.find((a) => (a.filename || "").toLowerCase() === (alt || "").toLowerCase()); const byId = attachments.find((a) => String(a.id) === String(mediaId)); const byMediaIdInFilename = attachments.find((a) => (a.filename || "").includes(mediaId)); const attachmentId = byFilename?.id ?? byId?.id ?? byMediaIdInFilename?.id ?? mediaId; const url = issueKey ? `${BASE_API_URL}/api/tickets/${encodeURIComponent(issueKey)}/attachments/${encodeURIComponent(attachmentId)}/content` : jiraBaseUrl ? `${jiraBaseUrl}/rest/api/3/attachment/content/${attachmentId}` : ""; out.push(`

${escapeHtml(alt)}

`); } } else if (block.type === "codeBlock") { const text = (block.content || []).map((c) => c.text || "").join(""); const lines = text.split("\n"); const lineNums = lines.map((_, i) => i + 1).join("\n"); const lineNumsHtml = escapeHtml(lineNums); const codeHtml = lines.map((l) => `${escapeHtml(l)}`).join("\n"); out.push(`
${lineNumsHtml}
${codeHtml}
`); } else if (block.type === "paragraph") { const parts = (block.content || []).map((frag) => { if (frag.type === "mention") { const id = frag.attrs?.id || ""; const text = frag.attrs?.text || ""; return `${escapeHtml(text.startsWith("@") ? text : `@${text}`)}`; } if (frag.type === "inlineCard" || frag.type === "blockCard") { const url = frag.attrs?.url || frag.attrs?.data?.url || ""; const data = frag.attrs?.data || {}; const name = data.name || data.title || data.generator?.name || url; return url ? `${escapeHtml(name)}` : ""; } let t = escapeHtml(frag.text || ""); const marks = frag.marks || []; for (const m of marks) { if (m.type === "strong") t = `${t}`; else if (m.type === "em") t = `${t}`; else if (m.type === "underline") t = `${t}`; else if (m.type === "strike") t = `${t}`; else if (m.type === "code") t = `${t}`; else if (m.type === "subsup" && m.attrs?.type === "sub") t = `${t}`; else if (m.type === "subsup" && m.attrs?.type === "sup") t = `${t}`; else if (m.type === "link" && m.attrs?.href) t = `${t}`; } return t; }); out.push(`

${parts.join("") || "
"}

`); } } return out.join(""); } function escapeHtml(s) { const div = document.createElement("div"); div.textContent = s; return div.innerHTML; } function CommentItem({ comment, selectedKey, updateTicket, fetchTicketDetail, setUpdateBusy, users = [], attachments = [], onImageClick }) { const [editing, setEditing] = useState(false); const [editBody, setEditBody] = useState(""); function handleStartEdit() { const html = comment.body_adf ? adfToHtml(comment.body_adf, { jiraBaseUrl: comment.jiraBaseUrl || "", issueKey: selectedKey || "" }) : `

${escapeHtml(comment.body || "")}

`; setEditBody(html); setEditing(true); } async function handleDelete() { if (!confirm("Delete this comment?")) return; setUpdateBusy(true); try { const res = await apiFetch( `${BASE_API_URL}/api/tickets/${selectedKey}/comments/${comment.id}`, { method: "DELETE" } ); if (!res.ok) throw new Error(await res.text()); await fetchTicketDetail(); } catch (e) { console.error(e); alert("Failed to delete comment."); } finally { setUpdateBusy(false); } } async function handleSaveEdit() { const hasHtml = /<[a-z][\s\S]*>/i.test(editBody); const adf = hasHtml ? htmlToAdf(editBody, selectedKey) : { version: 1, type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: editBody }] }] }; setUpdateBusy(true); try { const res = await apiFetch( `${BASE_API_URL}/api/tickets/${selectedKey}/comments/${comment.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body: adf }), } ); if (!res.ok) throw new Error(await res.text()); setEditing(false); await fetchTicketDetail(); } catch (e) { console.error(e); alert("Failed to update comment."); } finally { setUpdateBusy(false); } } return (
{comment.author || "Unknown"} {comment.created ? new Date(comment.created).toLocaleString() : ""} {comment.internal && · Internal note}
{editing ? (
) : ( <>
{ const img = e.target?.closest?.("img.comment-inline-image"); if (img?.src && onImageClick) onImageClick(img.src); }} role={onImageClick ? "presentation" : undefined} dangerouslySetInnerHTML={{ __html: comment.body_adf ? adfToHtml(comment.body_adf, { jiraBaseUrl: comment.jiraBaseUrl || "", issueKey: selectedKey || "", attachments }) : escapeHtml(stripHtmlToText(comment.body || "") || comment.body || ""), }} />
)}
); } function RichTextEditor({ value, onChange, placeholder, minRows = 5, issueKey = null, users = [] }) { const editorRef = useRef(null); const fileInputRef = useRef(null); const [mentionQuery, setMentionQuery] = useState(""); const [mentionAnchor, setMentionAnchor] = useState(null); const [mentionSelectedIndex, setMentionSelectedIndex] = useState(0); const mentionListRef = useRef(null); useEffect(() => { mentionListRef.current?.querySelector(".selected")?.scrollIntoView({ block: "nearest" }); }, [mentionSelectedIndex]); const filteredUsers = useMemo(() => { if (!mentionQuery.trim()) return users.slice(0, 8); const q = mentionQuery.toLowerCase(); return users .filter((u) => (u.display_name || "").toLowerCase().includes(q)) .slice(0, 8); }, [users, mentionQuery]); useEffect(() => { if (!editorRef.current || !value) return; const html = editorRef.current.innerHTML; const empty = !html || html === "
" || html === "
"; if (empty) editorRef.current.innerHTML = value; }, [value]); const execCmd = (cmd, value = null) => { document.execCommand(cmd, false, value); editorRef.current?.focus(); onChange?.(editorRef.current?.innerHTML || ""); }; const handleInput = () => { onChange?.(editorRef.current?.innerHTML || ""); checkMentionTrigger(); }; function getTextBeforeCaret() { const sel = window.getSelection(); if (!sel.rangeCount || !editorRef.current || !editorRef.current.contains(sel.anchorNode)) return ""; const range = sel.getRangeAt(0).cloneRange(); range.selectNodeContents(editorRef.current); range.setEnd(sel.anchorNode, sel.anchorOffset); return range.toString(); } function checkMentionTrigger() { const textBefore = getTextBeforeCaret(); const match = textBefore.match(/@([\w\s]*)$/); if (match) { setMentionQuery(match[1]); setMentionAnchor({ node: window.getSelection().anchorNode, offset: window.getSelection().anchorOffset }); setMentionSelectedIndex(0); } else { setMentionQuery(""); setMentionAnchor(null); } } function insertMention(user) { if (!editorRef.current) return; const sel = window.getSelection(); if (!sel.rangeCount) return; const textBefore = getTextBeforeCaret(); const match = textBefore.match(/@([\w\s]*)$/); if (!match) return; const range = sel.getRangeAt(0).cloneRange(); range.selectNodeContents(editorRef.current); range.setEnd(sel.anchorNode, sel.anchorOffset); const textBeforeRange = range.toString(); const startOffset = textBeforeRange.length - match[0].length; let charCount = 0; let startNode = null; let startNodeOffset = 0; const walker = document.createTreeWalker(editorRef.current, NodeFilter.SHOW_TEXT, null, false); let n; while ((n = walker.nextNode())) { const len = n.textContent.length; if (charCount + len >= startOffset) { startNode = n; startNodeOffset = startOffset - charCount; break; } charCount += len; } if (!startNode) { startNode = editorRef.current; startNodeOffset = 0; } const rangeToReplace = document.createRange(); rangeToReplace.setStart(startNode, startNodeOffset); rangeToReplace.setEnd(sel.anchorNode, sel.anchorOffset); const span = document.createElement("span"); span.setAttribute("data-mention-id", user.account_id); span.setAttribute("data-mention-name", user.display_name || ""); span.textContent = `@${user.display_name || ""}`; span.contentEditable = "false"; span.className = "mention-chip"; rangeToReplace.deleteContents(); rangeToReplace.insertNode(span); const space = document.createTextNode("\u00A0"); span.parentNode.insertBefore(space, span.nextSibling); rangeToReplace.setStartAfter(space); rangeToReplace.setEndAfter(space); sel.removeAllRanges(); sel.addRange(rangeToReplace); setMentionQuery(""); setMentionAnchor(null); onChange?.(editorRef.current?.innerHTML || ""); } const handleKeyDown = (e) => { if (mentionQuery !== "" && filteredUsers.length > 0) { if (e.key === "ArrowDown") { e.preventDefault(); setMentionSelectedIndex((i) => Math.min(i + 1, filteredUsers.length - 1)); return; } if (e.key === "ArrowUp") { e.preventDefault(); setMentionSelectedIndex((i) => Math.max(i - 1, 0)); return; } if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); const user = filteredUsers[mentionSelectedIndex]; if (user) insertMention(user); return; } if (e.key === "Escape") { e.preventDefault(); setMentionQuery(""); setMentionAnchor(null); return; } } }; async function insertImage(file) { if (!file || !file.type.startsWith("image/")) return; if (!issueKey) { alert("Select a ticket first to add images."); return; } const formData = new FormData(); formData.append("file", file); try { const res = await apiFetch(`${BASE_API_URL}/api/tickets/${encodeURIComponent(issueKey)}/attachments`, { method: "POST", body: formData, }); if (!res.ok) throw new Error(await res.text()); const att = await res.json(); const proxyUrl = `${BASE_API_URL}/api/tickets/${encodeURIComponent(issueKey)}/attachments/${encodeURIComponent(att.id)}/content`; const contentUrl = att.content || att.url; const img = document.createElement("img"); img.src = proxyUrl; img.alt = att.filename || "image"; img.setAttribute("data-attachment-id", String(att.id)); img.setAttribute("data-attachment-url", contentUrl || proxyUrl); img.setAttribute("data-attachment-filename", att.filename || "image.png"); img.style.maxWidth = "100%"; img.style.maxHeight = "200px"; editorRef.current?.focus(); document.execCommand("insertHTML", false, img.outerHTML); onChange?.(editorRef.current?.innerHTML || ""); } catch (e) { console.error(e); alert("Failed to upload image."); } } const handlePaste = (e) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith("image/")) { e.preventDefault(); insertImage(item.getAsFile()); return; } } }; const handleFileSelect = (e) => { const file = e.target.files?.[0]; if (file) insertImage(file); e.target.value = ""; }; return (
{issueKey && ( <> )}
{mentionQuery !== "" && (
{filteredUsers.length === 0 ? (
No users found
) : ( filteredUsers.map((u, i) => ( )) )}
)}
); } function getQuarterLabel(offset) { const now = new Date(); const q = Math.floor(now.getMonth() / 3) + 1; const year = now.getFullYear(); const totalMonths = (year - 1) * 12 + (q - 1) * 3 + 1 + offset * 3; const y = Math.floor((totalMonths - 1) / 12) + 1; const m = ((totalMonths - 1) % 12) + 1; const qLabel = Math.floor((m - 1) / 3) + 1; return `Q${qLabel} ${y}`; } function getQuarterBounds(offset) { const now = new Date(); const q = Math.floor(now.getMonth() / 3) + 1; const year = now.getFullYear(); const startMonth = (q - 1) * 3 + 1; const totalMonths = (year - 1) * 12 + startMonth + offset * 3; const y = Math.floor((totalMonths - 1) / 12) + 1; const m = ((totalMonths - 1) % 12) + 1; const start = new Date(y, m - 1, 1); const qNew = Math.floor((m - 1) / 3) + 1; const endMonth = qNew === 4 ? 12 : qNew * 3 + 1; const end = new Date(y, endMonth - 1, 1); end.setDate(0); end.setHours(23, 59, 59, 999); return [start, end]; } function isTicketInQuarter(ticket, quarterOffset) { const created = ticket.created; if (!created) return true; const [start, end] = getQuarterBounds(quarterOffset); const d = new Date(created); return d >= start && d <= end; } function EscalationPieChart({ escalatedCount, fixedByCodeCount }) { const canvasRef = useRef(null); const chartRef = useRef(null); useEffect(() => { if (!canvasRef.current || escalatedCount === 0) return; const otherCount = escalatedCount - fixedByCodeCount; const fixedPct = escalatedCount ? Math.round(100 * fixedByCodeCount / escalatedCount) : 0; const fixedByCodeColor = fixedPct >= 80 ? "#16a34a" : "#dc2626"; if (chartRef.current) { chartRef.current.destroy(); } chartRef.current = new Chart(canvasRef.current, { type: "pie", data: { labels: [ `Escalated to R&D (fixed_by_code) – ${fixedByCodeCount} (${fixedPct}%)`, `Other escalations – ${otherCount} (${100 - fixedPct}%)`, ], datasets: [{ data: [fixedByCodeCount, otherCount], backgroundColor: [fixedByCodeColor, "#e5e7eb"], borderWidth: 0, }], }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, padding: 16 }, }, }, }, }); return () => { if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } }; }, [escalatedCount, fixedByCodeCount]); if (escalatedCount === 0) { return (
Percentage of bugs escalated to R&D of all escalations It is expected that 80% of escalations will end up with the code fix.
No escalated tickets in this quarter.
); } const fixedPct = Math.round(100 * fixedByCodeCount / escalatedCount); return (
Percentage of bugs escalated to R&D of all escalations It is expected that 80% of escalations will end up with the code fix.
{fixedByCodeCount} of {escalatedCount} escalated tickets ({fixedPct}%) have the "fixed_by_code" label.
); } const STATS_TIER_ORDER = ["Diamond", "Platinum", "Gold", "Silver", "Bronze", "Standard", "No ARR"]; const UNASSIGNED_KEY = "__unassigned__"; function emptyAssigneeStatsRow(accountId, displayName) { return { account_id: accountId, display_name: displayName, total_tickets: 0, knowledge_gap: { count: 0, pct: 0 }, reporter_responsibility: { count: 0, pct: 0 }, known_issue: { count: 0, pct: 0 }, median_resolution_hours: null, median_resolution_by_tier: {}, escalated_count: 0, escalated_fixed_by_code_count: 0, escalated_fixed_by_code_pct: 0, }; } function orderUsersByDisplayNameList(allUsers, nameOrder) { const byNorm = new Map(); for (const u of allUsers || []) { const k = (u.display_name || "").trim().toLowerCase(); if (k) byNorm.set(k, u); } const out = []; for (const n of nameOrder || []) { const u = byNorm.get(String(n).trim().toLowerCase()); if (u) out.push(u); } return out; } /** Teammate stats when COE list is configured: one row per teammate, no Unassigned, no extra assignees. */ function mergeTeamStatsForCoeSubset(apiRows, coeUsersOrdered) { const byKey = new Map(); for (const r of apiRows || []) { if (r.account_id == null) continue; byKey.set(r.account_id, r); } return (coeUsersOrdered || []).map((u) => byKey.get(u.account_id) || emptyAssigneeStatsRow(u.account_id, u.display_name)); } /** No COE list: all assignable users, Unassigned if present, plus assignees on tickets not in assignable list. */ function mergeTeamStatsWithUsers(apiRows, usersList) { const byKey = new Map(); for (const r of apiRows || []) { const k = r.account_id == null ? UNASSIGNED_KEY : r.account_id; byKey.set(k, r); } const sortedUsers = [...(usersList || [])].sort((a, b) => (a.display_name || "").localeCompare(b.display_name || "", undefined, { sensitivity: "base" }) ); const userIds = new Set(sortedUsers.map((u) => u.account_id)); const out = sortedUsers.map((u) => byKey.get(u.account_id) || emptyAssigneeStatsRow(u.account_id, u.display_name)); const un = byKey.get(UNASSIGNED_KEY); if (un) out.unshift(un); for (const r of apiRows || []) { if (r.account_id != null && !userIds.has(r.account_id)) { out.push(r); } } return out; } /** Assignee filter + teammate stats row order. Prefer teamStatsAssigneeAccountIds over teamStatsAssigneeDisplayNames when both are set. */ function resolveAssigneeUiUsers(allUsers, accountIds, displayNames) { if (Array.isArray(accountIds) && accountIds.length > 0) { const map = new Map((allUsers || []).map((u) => [u.account_id, u])); const list = accountIds.map((id) => map.get(id)).filter(Boolean); return { list, coeSubset: true }; } if (Array.isArray(displayNames) && displayNames.length > 0) { const list = orderUsersByDisplayNameList(allUsers, displayNames); return { list, coeSubset: true }; } const list = [...(allUsers || [])].sort((a, b) => (a.display_name || "").localeCompare(b.display_name || "", undefined, { sensitivity: "base" }) ); return { list, coeSubset: false }; } function useTeamStatsCharts(mergedRows, quarterLabel) { const refPct = useRef(null); const refTotal = useRef(null); const refMedian = useRef(null); const refEscPct = useRef(null); const tierRefs = useRef({}); const chartInstances = useRef([]); const tierCharts = useMemo( () => STATS_TIER_ORDER.filter((tier) => mergedRows.some((r) => r.median_resolution_by_tier && r.median_resolution_by_tier[tier] != null) ), [mergedRows] ); useEffect(() => { if (typeof Chart === "undefined") return; chartInstances.current.forEach((c) => { try { c.destroy(); } catch (e) {} }); chartInstances.current = []; const labels = mergedRows.map((r) => r.display_name || "—"); const reg = (c) => { if (c) chartInstances.current.push(c); }; const tooltipPct = (ctx) => { const v = ctx.raw; if (v == null || Number.isNaN(v)) return ""; return `${v}%`; }; if (refPct.current && labels.length) { reg( new Chart(refPct.current, { type: "bar", data: { labels, datasets: [ { label: "Knowledge gap %", data: mergedRows.map((r) => r.knowledge_gap?.pct ?? 0), backgroundColor: "rgba(59, 130, 246, 0.7)" }, { label: "Poor escalation %", data: mergedRows.map((r) => r.reporter_responsibility?.pct ?? 0), backgroundColor: "rgba(245, 158, 11, 0.85)" }, { label: "Known issue %", data: mergedRows.map((r) => r.known_issue?.pct ?? 0), backgroundColor: "rgba(139, 92, 246, 0.75)" }, ], }, options: { indexAxis: "y", responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: `Label mix (% of own tickets) · ${quarterLabel}` }, tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${tooltipPct(ctx)}` } }, }, scales: { x: { beginAtZero: true, max: 100, stacked: false } }, }, }) ); } if (refTotal.current && labels.length) { reg( new Chart(refTotal.current, { type: "bar", data: { labels, datasets: [{ label: "Tickets", data: mergedRows.map((r) => r.total_tickets ?? 0), backgroundColor: "rgba(55, 65, 81, 0.65)" }], }, options: { indexAxis: "y", responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: `Total tickets · ${quarterLabel}` } }, scales: { x: { beginAtZero: true } }, }, }) ); } if (refMedian.current && labels.length) { reg( new Chart(refMedian.current, { type: "bar", data: { labels, datasets: [ { label: "Median resolution (hours)", data: mergedRows.map((r) => (r.median_resolution_hours != null ? r.median_resolution_hours : null)), backgroundColor: "rgba(22, 163, 74, 0.65)", }, ], }, options: { indexAxis: "y", responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: `Median resolution time · ${quarterLabel}` }, tooltip: { callbacks: { label: (ctx) => { const h = ctx.raw; const r = mergedRows[ctx.dataIndex]; return `Median: ${formatResolutionHours(h)} (${r?.total_tickets ?? 0} tickets, resolved subset)`; }, }, }, }, scales: { x: { beginAtZero: true, title: { display: true, text: "Hours" } } }, }, }) ); } if (refEscPct.current && labels.length) { reg( new Chart(refEscPct.current, { type: "bar", data: { labels, datasets: [ { label: "fixed_by_code % of escalations", data: mergedRows.map((r) => (r.escalated_count ? r.escalated_fixed_by_code_pct ?? 0 : null)), backgroundColor: mergedRows.map((r) => { const p = r.escalated_fixed_by_code_pct ?? 0; if (!r.escalated_count) return "rgba(229, 231, 235, 0.9)"; return p >= 80 ? "rgba(22, 163, 74, 0.75)" : "rgba(220, 38, 38, 0.7)"; }), }, ], }, options: { indexAxis: "y", responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: `% escalated to R&D (fixed_by_code) · ${quarterLabel}` }, tooltip: { callbacks: { label: (ctx) => { const r = mergedRows[ctx.dataIndex]; if (!r.escalated_count) return "No escalations"; return `${r.escalated_fixed_by_code_count ?? 0} / ${r.escalated_count} (${r.escalated_fixed_by_code_pct ?? 0}%)`; }, }, }, }, scales: { x: { beginAtZero: true, max: 100, title: { display: true, text: "%" } } }, }, }) ); } return () => { chartInstances.current.forEach((c) => { try { c.destroy(); } catch (e) {} }); chartInstances.current = []; }; }, [mergedRows, quarterLabel]); useEffect(() => { if (typeof Chart === "undefined") return; const tierCanvasMap = tierRefs.current; const charts = []; tierCharts.forEach((tier) => { const canvas = tierCanvasMap[tier]; if (!canvas) return; const dataVals = mergedRows.map((r) => { const v = r.median_resolution_by_tier && r.median_resolution_by_tier[tier]; return v != null ? v : null; }); if (!dataVals.some((v) => v != null)) return; const bg = mergedRows.map((r, i) => { const h = dataVals[i]; const cls = getTierValueColorClass(tier, h); if (cls === "stat-tier-green") return "rgba(22, 163, 74, 0.75)"; if (cls === "stat-tier-orange") return "rgba(234, 179, 8, 0.85)"; if (cls === "stat-tier-red") return "rgba(220, 38, 38, 0.72)"; return "rgba(75, 85, 99, 0.65)"; }); const ch = new Chart(canvas, { type: "bar", data: { labels: mergedRows.map((r) => r.display_name || "—"), datasets: [{ label: `${tier} median (h)`, data: dataVals, backgroundColor: bg }], }, options: { indexAxis: "y", responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: `Median resolution · ${tier} · ${quarterLabel}` }, tooltip: { callbacks: { label: (ctx) => { const h = ctx.raw; return h != null ? formatResolutionHours(h) : "—"; }, }, }, }, scales: { x: { beginAtZero: true, title: { display: true, text: "Hours" } } }, }, }); charts.push(ch); }); return () => { charts.forEach((c) => { try { c.destroy(); } catch (e) {} }); }; }, [mergedRows, quarterLabel, tierCharts]); return { refPct, refTotal, refMedian, refEscPct, tierRefs, tierCharts }; } function useCompareCharts(data, compareA, compareB) { const refTotal = useRef(null); const refTier = useRef(null); const refEscFixed = useRef(null); const refKnowledgeGap = useRef(null); const chartInstances = useRef([]); useEffect(() => { if (typeof Chart === "undefined") return; chartInstances.current.forEach((c) => { try { c.destroy(); } catch (e) {} }); chartInstances.current = []; if (!data || !data.period_a?.stats || !data.period_b?.stats) return; const statsA = data.period_a.stats; const statsB = data.period_b.stats; const labelA = `A · ${compareA.map(getQuarterLabel).join(" + ")}`; const labelB = `B · ${compareB.map(getQuarterLabel).join(" + ")}`; const colorA = "rgba(0, 200, 83, 0.78)"; const colorB = "rgba(148, 163, 184, 0.85)"; const reg = (c) => { if (c) chartInstances.current.push(c); }; if (refTotal.current) { reg( new Chart(refTotal.current, { type: "bar", data: { labels: [labelA, labelB], datasets: [ { label: "Total tickets", data: [statsA.total_tickets ?? 0, statsB.total_tickets ?? 0], backgroundColor: [colorA, colorB], }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: "Total tickets" }, legend: { display: false }, }, scales: { y: { beginAtZero: true, title: { display: true, text: "Tickets" } } }, }, }) ); } if (refTier.current) { const tiers = COMPARE_TIERS.filter( (t) => (statsA.median_resolution_by_tier?.[t] != null) || (statsB.median_resolution_by_tier?.[t] != null) ); reg( new Chart(refTier.current, { type: "bar", data: { labels: tiers, datasets: [ { label: labelA, data: tiers.map((t) => statsA.median_resolution_by_tier?.[t] ?? null), backgroundColor: colorA, }, { label: labelB, data: tiers.map((t) => statsB.median_resolution_by_tier?.[t] ?? null), backgroundColor: colorB, }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: "Median resolution per customer tier (hours)" }, tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${ctx.raw == null ? "—" : ctx.raw.toFixed(1) + "h"}`, }, }, }, scales: { y: { beginAtZero: true, title: { display: true, text: "Hours" } } }, }, }) ); } if (refEscFixed.current) { reg( new Chart(refEscFixed.current, { type: "bar", data: { labels: [labelA, labelB], datasets: [ { label: "fixed_by_code % of escalations", data: [statsA.escalated_fixed_by_code_pct ?? 0, statsB.escalated_fixed_by_code_pct ?? 0], backgroundColor: [colorA, colorB], }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: "Escalated · fixed by code %" }, legend: { display: false }, tooltip: { callbacks: { label: (ctx) => { const stats = ctx.dataIndex === 0 ? statsA : statsB; return `${stats.escalated_fixed_by_code_count ?? 0} / ${stats.escalated_count ?? 0} (${ctx.raw}%)`; }, }, }, }, scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: "%" } } }, }, }) ); } if (refKnowledgeGap.current) { reg( new Chart(refKnowledgeGap.current, { type: "bar", data: { labels: [labelA, labelB], datasets: [ { label: "Knowledge Gap %", data: [statsA.knowledge_gap?.pct ?? 0, statsB.knowledge_gap?.pct ?? 0], backgroundColor: [colorA, colorB], }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: "Knowledge Gap %" }, legend: { display: false }, tooltip: { callbacks: { label: (ctx) => { const stats = ctx.dataIndex === 0 ? statsA : statsB; return `${stats.knowledge_gap?.count ?? 0} / ${stats.total_tickets ?? 0} (${ctx.raw}%)`; }, }, }, }, scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: "%" } } }, }, }) ); } return () => { chartInstances.current.forEach((c) => { try { c.destroy(); } catch (e) {} }); chartInstances.current = []; }; }, [data, compareA, compareB]); return { refTotal, refTier, refEscFixed, refKnowledgeGap }; } function TeamStatsPageView({ mergedRows, quarterLabel, periodText, quarterOffset, onQuarterChange, onBack }) { const { refPct, refTotal, refMedian, refEscPct, tierRefs, tierCharts } = useTeamStatsCharts(mergedRows, quarterLabel); return (
{periodText}

Metrics use each ticket's current Jira assignee (same as the dashboard). Reassignments move counts between people.

{tierCharts.map((tier) => (
{ tierRefs.current[tier] = el; }} />
))}
); } const COMPARE_QUARTERS = [0, -1, -2, -3, -4]; const COMPARE_METRICS = [ { key: "total_tickets", label: "Total tickets", get: (s) => s.total_tickets, format: (v) => String(v ?? 0), direction: "neutral" }, { key: "median_resolution_hours", label: "Median resolution (hours)", get: (s) => s.median_resolution_hours, format: (v) => v == null ? "—" : v.toFixed(1), direction: "lower" }, { key: "knowledge_gap_pct", label: "Knowledge Gap %", get: (s) => s.knowledge_gap?.pct, format: (v) => v == null ? "—" : `${v}%`, direction: "lower" }, { key: "knowledge_gap_count", label: "Knowledge Gap (count)", get: (s) => s.knowledge_gap?.count, format: (v) => String(v ?? 0), direction: "lower" }, { key: "reporter_responsibility_pct", label: "Poor escalation %", get: (s) => s.reporter_responsibility?.pct, format: (v) => v == null ? "—" : `${v}%`, direction: "lower" }, { key: "reporter_responsibility_count", label: "Poor escalation (count)", get: (s) => s.reporter_responsibility?.count, format: (v) => String(v ?? 0), direction: "lower" }, { key: "known_issue_pct", label: "Known issue %", get: (s) => s.known_issue?.pct, format: (v) => v == null ? "—" : `${v}%`, direction: "lower" }, { key: "known_issue_count", label: "Known issue (count)", get: (s) => s.known_issue?.count, format: (v) => String(v ?? 0), direction: "lower" }, { key: "escalated_count", label: "Escalated tickets", get: (s) => s.escalated_count, format: (v) => String(v ?? 0), direction: "lower" }, { key: "escalated_fixed_by_code_pct", label: "Escalated · fixed by code %", get: (s) => s.escalated_fixed_by_code_pct, format: (v) => v == null ? "—" : `${v}%`, direction: "higher" }, ]; const COMPARE_TIERS = ["Diamond", "Platinum", "Gold", "Silver", "Bronze", "Standard", "No ARR"]; const COMPARE_PRESETS = [ { id: "qvq", label: "Q vs Q-1", a: [0], b: [-1] }, { id: "yoy", label: "YoY · Q0 vs Q-4", a: [0], b: [-4] }, { id: "h1h2", label: "Last 2 vs prior 2", a: [0, -1], b: [-2, -3] }, ]; function arraysEqualUnordered(a, b) { if (a.length !== b.length) return false; const sa = [...a].sort(); const sb = [...b].sort(); return sa.every((v, i) => v === sb[i]); } const COMPARE_KPI_CARDS = [ { key: "total_tickets", label: "Total tickets", get: (s) => s.total_tickets, format: (v) => String(v ?? 0), direction: "neutral" }, { key: "knowledge_gap_pct", label: "Knowledge Gap %", get: (s) => s.knowledge_gap?.pct, format: (v) => v == null ? "—" : `${v}%`, direction: "lower" }, { key: "median_resolution_hours", label: "Median resolution (h)", get: (s) => s.median_resolution_hours, format: (v) => v == null ? "—" : v.toFixed(1), direction: "lower" }, { key: "escalated_fixed_by_code_pct", label: "Escalated · fixed by code %", get: (s) => s.escalated_fixed_by_code_pct, format: (v) => v == null ? "—" : `${v}%`, direction: "higher" }, ]; function deltaCell(a, b, direction) { if (a == null || b == null) return { text: "—", cls: "compare-delta-neutral" }; const abs = a - b; let pctText = "—"; if (b !== 0) { const pct = (abs / Math.abs(b)) * 100; pctText = `${pct >= 0 ? "+" : ""}${pct.toFixed(1)}%`; } else if (a !== 0) { pctText = "+∞"; } else { pctText = "0%"; } let cls = "compare-delta-neutral"; if (direction !== "neutral" && abs !== 0) { const better = direction === "lower" ? abs < 0 : abs > 0; cls = better ? "compare-delta-better" : "compare-delta-worse"; } const arrow = abs > 0 ? "▲" : abs < 0 ? "▼" : "·"; return { text: `${arrow} ${pctText}`, cls }; } function quartersLabel(quarters) { if (!quarters || !quarters.length) return "(none)"; return quarters.map(getQuarterLabel).join(" + "); } function ComparePageView({ compareA, compareB, toggleA, toggleB, applyPreset, data, loading, error, onBack, onRefresh, }) { const periodA = data?.period_a; const periodB = data?.period_b; const statsA = periodA?.stats; const statsB = periodB?.stats; const { refTotal, refTier, refEscFixed, refKnowledgeGap } = useCompareCharts(data, compareA, compareB); return (
{quartersLabel(compareA)} vs {quartersLabel(compareB)}
Presets: {COMPARE_PRESETS.map((p) => { const active = arraysEqualUnordered(compareA, p.a) && arraysEqualUnordered(compareB, p.b); return ( ); })}
Period A (recent)
{COMPARE_QUARTERS.map((q) => ( ))}
{compareA.length === 0 ? "Select 1–4 quarters." : `${compareA.length} selected`}
Period B (baseline)
{COMPARE_QUARTERS.map((q) => ( ))}
{compareB.length === 0 ? "Select 1–4 quarters." : `${compareB.length} selected`}
{error &&
{error}
} {loading && !data ? (
{[0, 1, 2, 3].map((i) => (
))}
Loading comparison… each selected quarter is fetched from Jira.
) : data && statsA && statsB ? ( <>
{COMPARE_KPI_CARDS.map((m) => { const va = m.get(statsA); const vb = m.get(statsB); const d = deltaCell(va, vb, m.direction); return (
{m.label}
{m.format(va)}
B · {m.format(vb)}
{d.text}
); })}
{COMPARE_METRICS.map((m) => { const va = m.get(statsA); const vb = m.get(statsB); const d = deltaCell(va, vb, m.direction); return ( ); })} {COMPARE_TIERS.map((tier) => { const va = statsA.median_resolution_by_tier?.[tier]; const vb = statsB.median_resolution_by_tier?.[tier]; const fmt = (v) => (v == null ? "—" : v.toFixed(1)); const d = deltaCell(va, vb, "lower"); return ( ); })}
Metric Period A — {quartersLabel(compareA)} Period B — {quartersLabel(compareB)} Δ (A vs B)
{m.label} {m.format(va)} {m.format(vb)} {d.text}
Median resolution · {tier} (hours) {fmt(va)} {fmt(vb)} {d.text}

Δ = A − B, with % relative to B. Green = better, {" "}red = worse, gray = neutral or no change. "Better" is lower for resolution time / gap percentages / escalation counts, higher for fixed-by-code share.

) : (
Pick at least one quarter on each side.
)}
); } function JiraCredentialsModal({ initialEmail, onClose, onSaved }) { const [email, setEmail] = useState(initialEmail || ""); const [token, setToken] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); async function handleSave() { setError(null); if (!email.trim() || !token.trim()) { setError("Email and API token are required."); return; } setBusy(true); try { const res = await apiFetch(`${BASE_API_URL}/api/auth/jira-credentials`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), apiToken: token.trim() }), }); if (!res.ok) { let detail; try { detail = (await res.json()).detail || `HTTP ${res.status}`; } catch { detail = `HTTP ${res.status}`; } throw new Error(detail); } const out = await res.json(); onSaved && onSaved(out); } catch (e) { setError(e && e.message ? e.message : String(e)); } finally { setBusy(false); } } return (
e.stopPropagation()}>

Jira API credentials

Paste your Atlassian email and an API token. The server will validate against Jira and store the token securely in AWS Secrets Manager (one secret per user). Get an API token at id.atlassian.com → API tokens.

{error &&
{error}
}
); } function TicketApp() { const [tickets, setTickets] = useState([]); const [selectedKey, setSelectedKey] = useState(null); const [selectedDetail, setSelectedDetail] = useState(null); const [tierFilter, setTierFilter] = useState("All"); const [assigneeFilter, setAssigneeFilter] = useState(""); const [loadingTickets, setLoadingTickets] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); const [updateBusy, setUpdateBusy] = useState(false); const [commentDraft, setCommentDraft] = useState(""); const [commentType, setCommentType] = useState("reply"); const [commentEditorKey, setCommentEditorKey] = useState(0); const hasCommentContent = useMemo(() => { const c = commentDraft.trim(); if (!c) return false; if (/<[a-z][\s\S]*>/i.test(c)) { const div = document.createElement("div"); div.innerHTML = c; const hasText = !!(div.innerText || div.textContent || "").trim(); const hasImages = div.querySelectorAll("img").length > 0; return hasText || hasImages; } return true; }, [commentDraft]); const [newLabelDraft, setNewLabelDraft] = useState(""); const [visibleCommentsCount, setVisibleCommentsCount] = useState(10); const [imageLightboxSrc, setImageLightboxSrc] = useState(null); const [closedStatusPrompt, setClosedStatusPrompt] = useState(null); const [closedStatusDialogQueue, setClosedStatusDialogQueue] = useState([]); const [closedStatusPendingLabels, setClosedStatusPendingLabels] = useState([]); const closedDialogAnswersRef = useRef({}); const [users, setUsers] = useState([]); const [projectLabels, setProjectLabels] = useState([]); const [currentUserId, setCurrentUserId] = useState(""); const [userName, setUserName] = useState(""); const [userPrivilege, setUserPrivilege] = useState(""); const [userEmailFromAuth, setUserEmailFromAuth] = useState(""); const [jiraCredsStatus, setJiraCredsStatus] = useState(null); // null | { applicable, configured, privilege } const [jiraCredsModalOpen, setJiraCredsModalOpen] = useState(false); const [jiraBaseUrl, setJiraBaseUrl] = useState(""); const [statusFilter, setStatusFilter] = useState([]); const [stats, setStats] = useState(null); const [loadingStats, setLoadingStats] = useState(false); const [quarterOffset, setQuarterOffset] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [searchSuggestions, setSearchSuggestions] = useState([]); const [searchMode, setSearchMode] = useState(null); const [searchResults, setSearchResults] = useState([]); const [searchSuggestionsOpen, setSearchSuggestionsOpen] = useState(false); const [statsFilterView, setStatsFilterView] = useState(null); const [teamStatsPage, setTeamStatsPage] = useState(false); const [teamStatsRaw, setTeamStatsRaw] = useState(null); const [loadingTeamStats, setLoadingTeamStats] = useState(false); const [comparePage, setComparePage] = useState(false); const [compareA, setCompareA] = useState([0]); const [compareB, setCompareB] = useState([-1]); const [compareData, setCompareData] = useState(null); const [loadingCompare, setLoadingCompare] = useState(false); const [compareError, setCompareError] = useState(null); const [highlights, setHighlights] = useState(null); const [loadingHighlights, setLoadingHighlights] = useState(false); const [geminiApiKey, setGeminiApiKey] = useState(() => typeof localStorage !== "undefined" ? localStorage.getItem("geminiApiKey") || "" : ""); const [aiInsights, setAiInsights] = useState(null); const [loadingAiInsights, setLoadingAiInsights] = useState(false); const searchInputRef = useRef(null); const searchSuggestionsRef = useRef(null); const [coeAssigneeAccountIds, setCoeAssigneeAccountIds] = useState(null); const [coeAssigneeDisplayNames, setCoeAssigneeDisplayNames] = useState(null); const [cognitoAuthEnabled, setCognitoAuthEnabled] = useState(false); const [authRequiredBlock, setAuthRequiredBlock] = useState(false); const [authConfigError, setAuthConfigError] = useState(null); useEffect(() => { if (!teamStatsPage && !comparePage) fetchTickets(); }, [quarterOffset, teamStatsPage, comparePage]); useEffect(() => { const params = new URLSearchParams(window.location.search); const quarterParam = params.get("quarter"); const quarterNum = quarterParam != null && !Number.isNaN(parseInt(quarterParam, 10)) ? parseInt(quarterParam, 10) : null; if (params.get("team_stats") === "1") { setQuarterOffset(quarterNum !== null ? quarterNum : 0); setTeamStatsPage(true); return; } if (params.get("compare") === "1") { const parseList = (raw) => { if (!raw) return null; const out = raw .split(",") .map((p) => parseInt(p, 10)) .filter((n) => !Number.isNaN(n) && n <= 0 && n >= -16); return out.length >= 1 && out.length <= 4 ? out : null; }; const a = parseList(params.get("a")) || [0]; const b = parseList(params.get("b")) || [-1]; setCompareA(a); setCompareB(b); setComparePage(true); return; } if (quarterNum === null || Number.isNaN(quarterNum)) return; const resolutionTier = params.get("resolution_tier"); const validResolutionTiers = [ "Diamond", "Platinum", "Gold", "Silver", "Bronze", "Standard", "No ARR", ]; if (resolutionTier && validResolutionTiers.includes(resolutionTier)) { setQuarterOffset(quarterNum); setStatsFilterView({ resolutionTier, label: `Median resolution · ${resolutionTier} (longest first)`, tickets: null, }); (async () => { try { const res = await apiFetch( `${BASE_API_URL}/api/stats/resolution-by-tier?quarter=${quarterNum}&tier=${encodeURIComponent(resolutionTier)}` ); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); setStatsFilterView((prev) => (prev ? { ...prev, tickets: data } : null)); } catch (e) { console.error(e); setStatsFilterView((prev) => (prev ? { ...prev, tickets: [] } : null)); } })(); return; } const filter = params.get("filter"); if (!filter) return; const filterMap = { knowledge_gap: "Knowledge Gap tickets", "reporter-responsibility": "Poor escalation tickets", "known-issue": "Known issue tickets", escalated: "Escalated tickets", }; const label = filterMap[filter]; if (!label) return; setQuarterOffset(quarterNum); setStatsFilterView({ filter, label, tickets: null }); (async () => { try { const res = await apiFetch( `${BASE_API_URL}/api/stats/tickets?quarter=${quarterNum}&filter_type=${encodeURIComponent(filter)}` ); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); setStatsFilterView((prev) => (prev ? { ...prev, tickets: data } : null)); } catch (e) { console.error(e); setStatsFilterView((prev) => (prev ? { ...prev, tickets: [] } : null)); } })(); }, []); useEffect(() => { if (!teamStatsPage) return; let cancelled = false; (async () => { setLoadingTeamStats(true); try { const res = await apiFetch(`${BASE_API_URL}/api/stats/by-assignee?quarter=${quarterOffset}`); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); if (!cancelled) setTeamStatsRaw(data); } catch (e) { console.warn("Failed to load teammate stats", e); if (!cancelled) setTeamStatsRaw(null); } finally { if (!cancelled) setLoadingTeamStats(false); } })(); return () => { cancelled = true; }; }, [teamStatsPage, quarterOffset]); useEffect(() => { if (!teamStatsPage) return; const u = new URL(window.location.href); u.searchParams.set("team_stats", "1"); u.searchParams.set("quarter", String(quarterOffset)); const qs = u.searchParams.toString(); window.history.replaceState({}, "", u.pathname + (qs ? `?${qs}` : "")); }, [teamStatsPage, quarterOffset]); useEffect(() => { if (!comparePage) return; const u = new URL(window.location.href); u.searchParams.set("compare", "1"); u.searchParams.set("a", compareA.join(",")); u.searchParams.set("b", compareB.join(",")); u.searchParams.delete("team_stats"); u.searchParams.delete("quarter"); u.searchParams.delete("resolution_tier"); u.searchParams.delete("filter"); const qs = u.searchParams.toString(); window.history.replaceState({}, "", u.pathname + (qs ? `?${qs}` : "")); }, [comparePage, compareA, compareB]); useEffect(() => { if (!comparePage) return; if (compareA.length === 0 || compareB.length === 0) return; let cancelled = false; setLoadingCompare(true); setCompareError(null); (async () => { try { const url = `${BASE_API_URL}/api/stats/comparison?a=${compareA.join(",")}&b=${compareB.join(",")}`; const res = await apiFetch(url); if (!res.ok) { const txt = await res.text(); throw new Error(txt || `HTTP ${res.status}`); } const data = await res.json(); if (!cancelled) setCompareData(data); } catch (e) { console.warn("compare fetch", e); if (!cancelled) { setCompareData(null); setCompareError(e && e.message ? e.message : String(e)); } } finally { if (!cancelled) setLoadingCompare(false); } })(); return () => { cancelled = true; }; }, [comparePage, compareA, compareB]); useEffect(() => { if (searchQuery.trim().length < 3) { setSearchSuggestions([]); setSearchSuggestionsOpen(false); return; } const t = setTimeout(async () => { try { const res = await apiFetch( `${BASE_API_URL}/api/tickets/search?q=${encodeURIComponent(searchQuery.trim())}&limit=10` ); if (res.ok) { const data = await res.json(); setSearchSuggestions(data); setSearchSuggestionsOpen(true); } } catch (e) { setSearchSuggestions([]); } }, 300); return () => clearTimeout(t); }, [searchQuery]); useEffect(() => { function handleClickOutside(e) { if ( searchSuggestionsRef.current && !searchSuggestionsRef.current.contains(e.target) && searchInputRef.current && !searchInputRef.current.contains(e.target) ) { setSearchSuggestionsOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); async function handleSearchAll() { if (searchQuery.trim().length < 3) return; try { const res = await apiFetch( `${BASE_API_URL}/api/tickets/search?q=${encodeURIComponent(searchQuery.trim())}&all_results=true` ); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); setSearchResults(data); setSearchMode("all"); setSearchSuggestionsOpen(false); const allStatuses = Array.from(new Set(data.map((t) => t.status).filter(Boolean))); setStatusFilter(allStatuses); } catch (e) { console.error(e); alert("Search failed."); } } function handleSearchSuggestionClick(ticket) { setSearchResults([ticket]); setSearchMode("single"); setSearchSuggestionsOpen(false); setSearchQuery(""); setSearchSuggestions([]); handleSelectTicket(ticket.key); } function clearSearchMode() { setSearchMode(null); setSearchResults([]); setSearchQuery(""); setSearchSuggestions([]); setSearchSuggestionsOpen(false); fetchTickets(); } useEffect(() => { (async () => { setLoadingStats(true); try { const res = await apiFetch(`${BASE_API_URL}/api/stats?quarter=${quarterOffset}`); if (res.ok) { const data = await res.json(); setStats(data); } } catch (e) { console.warn("Failed to load quarter stats", e); } finally { setLoadingStats(false); } })(); }, [quarterOffset]); async function fetchHighlights() { setLoadingHighlights(true); try { const res = await apiFetch(`${BASE_API_URL}/api/highlights`); if (res.ok) { const data = await res.json(); setHighlights(data); } } catch (e) { console.warn("Failed to load highlights", e); } finally { setLoadingHighlights(false); } } useEffect(() => { fetchHighlights(); const interval = setInterval(fetchHighlights, 60 * 60 * 1000); return () => clearInterval(interval); }, []); function saveGeminiKey(key) { setGeminiApiKey(key); try { if (key) localStorage.setItem("geminiApiKey", key); else localStorage.removeItem("geminiApiKey"); } catch (e) {} } async function generateAiInsights() { if (!highlights?.by_assignee) return; const summaryParts = []; for (const name of highlights.assignee_order || []) { const b = highlights.by_assignee[name]; if (!b) continue; const lines = []; const fmt = (tickets, reason) => (tickets || []).map((t) => `${t.key}: "${(t.title || "").slice(0, 80)}${(t.title || "").length > 80 ? "…" : ""}" (stuck: ${reason})`); lines.push(...fmt(b.unassigned_high_tier, "Unassigned")); lines.push(...fmt(b.high_tier_backlog, "In Backlog")); lines.push(...fmt(b.high_tier_waiting_3d, "Waiting for customer 3+ days")); lines.push(...fmt(b.high_tier_wip_5d, "WIP 5+ days")); if (lines.length) summaryParts.push(`${name}:\n${lines.map((l) => ` - ${l}`).join("\n")}`); } const summary = summaryParts.join("\n\n") || "No highlights."; setLoadingAiInsights(true); setAiInsights(null); try { const res = await apiFetch(`${BASE_API_URL}/api/ai/insights`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ highlights_summary: summary, gemini_api_key: geminiApiKey || undefined, }), }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail || "AI insights failed"); } const data = await res.json(); setAiInsights(data.insights); } catch (e) { setAiInsights(`Error: ${e.message}`); } finally { setLoadingAiInsights(false); } } useEffect(() => { // Public bootstrap, then config/users/labels (skipped until Cognito login when required) (async () => { setAuthConfigError(null); let pub = { cognitoAuthEnabled: false }; try { const pr = await fetch(`${BASE_API_URL}/api/auth/public-config`); if (pr.ok) pub = await pr.json(); } catch (e) { console.warn("public-config", e); } setCognitoAuthEnabled(!!pub.cognitoAuthEnabled); if (pub.cognitoAuthEnabled && (pub.cognitoHostedUiDomain || pub.cognitoAppClientId)) { window.JIRA_INTERFACE_AUTH = { ...(window.JIRA_INTERFACE_AUTH || {}), useCognito: true, ...(pub.cognitoHostedUiDomain ? { cognitoDomain: pub.cognitoHostedUiDomain } : {}), ...(pub.cognitoAppClientId ? { clientId: pub.cognitoAppClientId } : {}), }; } const hasToken = typeof sessionStorage !== "undefined" && !!sessionStorage.getItem("cognito_access_token"); if (pub.cognitoAuthEnabled && !hasToken) { if (!window.JIRA_INTERFACE_AUTH || !window.JIRA_INTERFACE_AUTH.useCognito) { setAuthConfigError( "The API requires Cognito login. Add auth-config.js (see auth-config.example.js) with useCognito and your Hosted UI domain." ); return; } const ac = window.JIRA_INTERFACE_AUTH; const dom = String(ac.cognitoDomain || "") .replace(/^https?:\/\//, "") .trim(); const cid = String(ac.clientId || "").trim(); if (!dom || !cid) { setAuthConfigError( "Cognito needs a Hosted UI domain and app client id. On the API host set COGNITO_HOSTED_UI_DOMAIN (e.g. my-prefix.auth.eu-west-1.amazoncognito.com from App integration → Domain) and COGNITO_APP_CLIENT_ID, or set cognitoDomain and clientId in auth-config.js. The domain is not the User Pool ID — if it is wrong, Cognito shows “Login pages unavailable”." ); return; } setAuthRequiredBlock(true); return; } setAuthRequiredBlock(false); try { const [cfgRes, usersRes, labelsRes] = await Promise.all([ apiFetch(`${BASE_API_URL}/api/config`), apiFetch(`${BASE_API_URL}/api/users`), apiFetch(`${BASE_API_URL}/api/labels`), ]); let cfg = null; if (cfgRes.ok) { cfg = await cfgRes.json(); if (cfg.jiraBaseUrl) setJiraBaseUrl(cfg.jiraBaseUrl); if (cfg.myAccountId) { setCurrentUserId(cfg.myAccountId); setAssigneeFilter((prev) => prev || cfg.myAccountId); } const ids = cfg.teamStatsAssigneeAccountIds; const names = cfg.teamStatsAssigneeDisplayNames; setCoeAssigneeAccountIds(Array.isArray(ids) && ids.length ? ids : null); setCoeAssigneeDisplayNames(Array.isArray(names) && names.length ? names : null); } if (usersRes.ok) { const list = await usersRes.json(); setUsers(list); if (cfg && cfg.myAccountId && !userName) { const me = list.find((u) => u.account_id === cfg.myAccountId); if (me && me.display_name) { setUserName(me.display_name); } } } if (labelsRes.ok) { const labels = await labelsRes.json(); setProjectLabels(labels); } if (pub.cognitoAuthEnabled) { const mr = await apiFetch(`${BASE_API_URL}/api/auth/me`); if (mr.ok) { const m = await mr.json(); if (m.privilege) setUserPrivilege(m.privilege); const looksLikeEmail = typeof m.email === "string" && m.email.includes("@"); if (looksLikeEmail) setUserEmailFromAuth(m.email); // Only fall back to email/sub if Jira didn't already give us a real display name. // Cognito JWTs without an email claim end up returning the raw sub UUID here. setUserName((cur) => { if (cur) return cur; if (looksLikeEmail) return m.email; return cur; }); } try { const sr = await apiFetch(`${BASE_API_URL}/api/auth/jira-credentials/status`); if (sr.ok) setJiraCredsStatus(await sr.json()); } catch (e) { console.warn("jira-credentials status", e); } } } catch (e) { console.warn("Failed to load config/users for filters", e); } })(); }, [userName]); async function fetchTickets() { setLoadingTickets(true); try { const res = await apiFetch(`${BASE_API_URL}/api/tickets?quarter=${quarterOffset}`); if (!res.ok) { const text = await res.text(); throw new Error(text || "Failed to load tickets"); } const data = await res.json(); setTickets(data); // Initialize status filter if empty: include all statuses except Closed / Canceled if (!statusFilter.length && Array.isArray(data)) { const allStatuses = Array.from( new Set(data.map((t) => t.status).filter(Boolean)) ); const defaultStatuses = allStatuses.filter( (s) => s !== "Closed" && s !== "Canceled" && s !== "Cancelled" && s !== "Done" ); setStatusFilter(defaultStatuses); } } catch (e) { console.error(e); alert("Failed to load tickets from Jira. Check your config and server."); } finally { setLoadingTickets(false); } } async function fetchTicketDetail(key) { setLoadingDetail(true); setSelectedDetail(null); setVisibleCommentsCount(10); try { const res = await apiFetch( `${BASE_API_URL}/api/tickets/${encodeURIComponent(key)}` ); if (!res.ok) { const text = await res.text(); throw new Error(text || "Failed to load ticket details"); } const data = await res.json(); setSelectedDetail(data); } catch (e) { console.error(e); alert("Failed to load ticket details."); } finally { setLoadingDetail(false); } } function handleSelectTicket(key) { setSelectedKey(key); fetchTicketDetail(key); } const displayTickets = searchMode ? searchResults : tickets; const assigneeUiUsers = useMemo( () => resolveAssigneeUiUsers(users, coeAssigneeAccountIds, coeAssigneeDisplayNames), [users, coeAssigneeAccountIds, coeAssigneeDisplayNames] ); const mergedTeamStatsRows = useMemo(() => { if (!teamStatsPage) return []; const rows = teamStatsRaw?.rows; if (!rows) return []; if (!users.length) { return [...rows].sort((a, b) => (a.display_name || "").localeCompare(b.display_name || "", undefined, { sensitivity: "base" }) ); } if (assigneeUiUsers.coeSubset) { return mergeTeamStatsForCoeSubset(rows, assigneeUiUsers.list); } return mergeTeamStatsWithUsers(rows, users); }, [teamStatsPage, teamStatsRaw, users, assigneeUiUsers]); function exitTeamStats() { setTeamStatsPage(false); setTeamStatsRaw(null); const u = new URL(window.location.href); u.searchParams.delete("team_stats"); const qs = u.searchParams.toString(); window.history.replaceState({}, "", u.pathname + (qs ? `?${qs}` : "")); } function exitComparePage() { setComparePage(false); setCompareData(null); setCompareError(null); const u = new URL(window.location.href); u.searchParams.delete("compare"); u.searchParams.delete("a"); u.searchParams.delete("b"); const qs = u.searchParams.toString(); window.history.replaceState({}, "", u.pathname + (qs ? `?${qs}` : "")); } function enterComparePage() { setSelectedKey(null); setSelectedDetail(null); setStatsFilterView(null); if (teamStatsPage) { setTeamStatsPage(false); setTeamStatsRaw(null); } setComparePage(true); } function toggleCompareSet(setter, current, q) { const has = current.includes(q); if (has) { if (current.length === 1) return; setter(current.filter((x) => x !== q)); } else { if (current.length >= 4) return; setter([...current, q].sort((x, y) => y - x)); } } function refreshCompare() { if (!comparePage) return; setCompareA((arr) => [...arr]); } const filteredTickets = useMemo(() => { let base = displayTickets; if (searchMode === "single") return base; if (searchMode === "all") { base = base.filter((t) => isTicketInQuarter(t, quarterOffset)); } if (tierFilter !== "All") { base = base.filter((t) => t.tier === tierFilter); } // Status filter: if any statuses are selected, keep only those if (statusFilter.length) { base = base.filter((t) => statusFilter.includes(t.status)); } // Assignee filter: // - empty or unknown => no extra filter // - "ALL" => no extra filter // - explicit accountId => only tickets assigned to that account if (assigneeFilter && assigneeFilter !== "ALL" && assigneeFilter !== "UNASSIGNED") { base = base.filter((t) => t.assignee_id === assigneeFilter); } else if (assigneeFilter === "UNASSIGNED") { base = base.filter((t) => t.is_unassigned); } return base; }, [displayTickets, searchMode, quarterOffset, tierFilter, assigneeFilter, statusFilter]); const unassignedTickets = useMemo(() => { let base = displayTickets.filter((t) => t.is_unassigned); if (searchMode === "all") base = base.filter((t) => isTicketInQuarter(t, quarterOffset)); if (statusFilter.length) base = base.filter((t) => statusFilter.includes(t.status)); return base; }, [displayTickets, searchMode, quarterOffset, statusFilter]); const yourActiveTickets = filteredTickets.filter((t) => { const isAssignedToFilterUser = assigneeFilter === "ALL" ? true : assigneeFilter && assigneeFilter !== "UNASSIGNED" ? t.assignee_id === assigneeFilter : assigneeFilter === "UNASSIGNED" ? t.is_unassigned : currentUserId ? t.assignee_id === currentUserId : t.is_mine; return ( isAssignedToFilterUser && (t.status === STATUS_BACKLOG || t.status === STATUS_WIP || t.status === "Waiting for support") ); }); const escalatedTickets = filteredTickets.filter((t) => { const isAssignedToFilterUser = assigneeFilter === "ALL" ? true : assigneeFilter && assigneeFilter !== "UNASSIGNED" ? t.assignee_id === assigneeFilter : assigneeFilter === "UNASSIGNED" ? t.is_unassigned : currentUserId ? t.assignee_id === currentUserId : t.is_mine; return isAssignedToFilterUser && t.status === STATUS_ESCALATED; }); const waitingForCustomerTickets = filteredTickets.filter((t) => { const isAssignedToFilterUser = assigneeFilter === "ALL" ? true : assigneeFilter && assigneeFilter !== "UNASSIGNED" ? t.assignee_id === assigneeFilter : assigneeFilter === "UNASSIGNED" ? t.is_unassigned : currentUserId ? t.assignee_id === currentUserId : t.is_mine; return isAssignedToFilterUser && t.status === STATUS_WAITING_FOR_CUSTOMER; }); const closedDoneCanceledTickets = useMemo(() => { let base = displayTickets; if (searchMode === "all") base = base.filter((t) => isTicketInQuarter(t, quarterOffset)); if (tierFilter !== "All") base = base.filter((t) => t.tier === tierFilter); if (assigneeFilter && assigneeFilter !== "ALL" && assigneeFilter !== "UNASSIGNED") { base = base.filter((t) => t.assignee_id === assigneeFilter); } else if (assigneeFilter === "UNASSIGNED") { base = base.filter((t) => t.is_unassigned); } base = base.filter((t) => ["Closed", "Done", "Canceled", "Cancelled", "Solution Provided"].includes(t.status) ); if (statusFilter.length) { base = base.filter((t) => statusFilter.includes(t.status)); } return base; }, [displayTickets, searchMode, quarterOffset, tierFilter, assigneeFilter, statusFilter]); const tierCounts = useMemo(() => { let base = displayTickets; if (searchMode === "all") base = base.filter((t) => isTicketInQuarter(t, quarterOffset)); base = base.filter((t) => { if (statusFilter.length && !statusFilter.includes(t.status)) return false; if (assigneeFilter && assigneeFilter !== "ALL") { if (assigneeFilter === "UNASSIGNED") return t.is_unassigned; return t.assignee_id === assigneeFilter; } return true; }); const counts = { All: base.length }; for (const t of base) { const tier = t.tier || "No ARR"; counts[tier] = (counts[tier] || 0) + 1; } return counts; }, [displayTickets, searchMode, quarterOffset, statusFilter, assigneeFilter]); const assigneeCounts = useMemo(() => { let base = displayTickets; if (searchMode === "all") base = base.filter((t) => isTicketInQuarter(t, quarterOffset)); base = base.filter((t) => { if (tierFilter !== "All" && t.tier !== tierFilter) return false; if (statusFilter.length && !statusFilter.includes(t.status)) return false; return true; }); const counts = { ALL: base.length }; if (currentUserId) { counts[currentUserId] = base.filter((t) => t.assignee_id === currentUserId).length; } counts.UNASSIGNED = base.filter((t) => t.is_unassigned).length; for (const u of assigneeUiUsers.list) { counts[u.account_id] = base.filter((t) => t.assignee_id === u.account_id).length; } return counts; }, [displayTickets, searchMode, quarterOffset, tierFilter, statusFilter, assigneeUiUsers, currentUserId]); const statusCounts = useMemo(() => { let base = displayTickets; if (searchMode === "all") base = base.filter((t) => isTicketInQuarter(t, quarterOffset)); base = base.filter((t) => { if (tierFilter !== "All" && t.tier !== tierFilter) return false; if (assigneeFilter && assigneeFilter !== "ALL") { if (assigneeFilter === "UNASSIGNED") return t.is_unassigned; return t.assignee_id === assigneeFilter; } return true; }); const counts = {}; for (const t of base) { const s = t.status || "Unknown"; counts[s] = (counts[s] || 0) + 1; } return counts; }, [displayTickets, searchMode, quarterOffset, tierFilter, assigneeFilter]); const totalArrMine = useMemo( () => tickets .filter((t) => t.is_mine && t.arr != null) .reduce((sum, t) => sum + Number(t.arr || 0), 0), [tickets] ); async function updateTicket(partial, opts = {}) { if (!selectedKey) return; const { preserveCommentDraft = false } = opts; setUpdateBusy(true); try { const res = await apiFetch( `${BASE_API_URL}/api/tickets/${encodeURIComponent(selectedKey)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(partial), } ); if (!res.ok) { const text = await res.text(); throw new Error(text || "Failed to update ticket"); } await fetchTickets(); await fetchTicketDetail(selectedKey); if (!preserveCommentDraft) { setCommentDraft(""); setNewLabelDraft(""); setCommentType("reply"); setCommentEditorKey((k) => k + 1); } } catch (e) { console.error(e); alert("Failed to update ticket in Jira."); } finally { setUpdateBusy(false); } } async function handleStatusChange(e) { const newStatus = e.target.value; if (newStatus !== "Closed") { updateTicket({ status: newStatus }); return; } const comments = selectedDetail?.comments || []; const allCommentText = comments.map((c) => (c.body || "")).join("\n"); const hasKnowledgeGap = /#knowledge_gap/i.test(allCommentText); const hasReporterResponsibility = /#reporter-responsibility/i.test(allCommentText); const hasKnownIssue = /#known-issue/i.test(allCommentText); const hasCoeGap = /#coe_gap/i.test(allCommentText); const currentLabels = selectedDetail?.labels || []; const labelsToAdd = []; if (hasKnowledgeGap && !currentLabels.includes("knowledge_gap")) labelsToAdd.push("knowledge_gap"); if (hasReporterResponsibility && !currentLabels.includes("reporter-responsibility")) labelsToAdd.push("reporter-responsibility"); if (hasKnownIssue && !currentLabels.includes("known-issue")) labelsToAdd.push("known-issue"); const remembered = closedDialogAnswersRef.current[selectedKey] || {}; const wasAnswered = (id) => remembered[id] === "skip" || remembered[id] === "no" || remembered[id] === "yes"; const dialogsToShow = []; if (!hasKnowledgeGap && !wasAnswered("knowledge_gap")) dialogsToShow.push("knowledge_gap"); if (!hasReporterResponsibility && !wasAnswered("escalation")) dialogsToShow.push("escalation"); if (!hasKnownIssue && !wasAnswered("known_issue")) dialogsToShow.push("known_issue"); if (!wasAnswered("solution_confirmed")) dialogsToShow.push("solution_confirmed"); if (!wasAnswered("no_response")) dialogsToShow.push("no_response"); if (selectedDetail?.was_ever_escalated) { if (!wasAnswered("fixed_by_code")) dialogsToShow.push("fixed_by_code"); if (!hasCoeGap && !wasAnswered("coe_gap")) dialogsToShow.push("coe_gap"); if (!wasAnswered("still_open_on_RnD")) dialogsToShow.push("still_open_on_RnD"); } const finalLabels = [...currentLabels, ...labelsToAdd]; if (labelsToAdd.length > 0) { await updateTicket({ labels: finalLabels }); } if (dialogsToShow.length === 0) { updateTicket({ status: "Closed" }); return; } setClosedStatusPendingLabels(finalLabels); setClosedStatusDialogQueue(dialogsToShow); setClosedStatusPrompt(dialogsToShow[0]); } const COMMENT_DIALOGS = ["knowledge_gap", "escalation", "known_issue"]; const LABEL_DIALOG_MAP = { solution_confirmed: "solution_confirmed", no_response: "no_response", fixed_by_code: "fixed_by_code", coe_gap: "coe_gap", still_open_on_RnD: "still_open_on_RnD", }; function recordClosedAnswer(dialogId, answer) { if (!selectedKey) return; closedDialogAnswersRef.current[selectedKey] = closedDialogAnswersRef.current[selectedKey] || {}; closedDialogAnswersRef.current[selectedKey][dialogId] = answer; } function clearClosedAnswersForTicket() { if (selectedKey) delete closedDialogAnswersRef.current[selectedKey]; } function handleClosedPromptYes() { if (COMMENT_DIALOGS.includes(closedStatusPrompt)) { recordClosedAnswer(closedStatusPrompt, "yes"); const templateMap = { knowledge_gap: "knowledgeGap", escalation: "escalationQuality", known_issue: "knownIssue" }; const key = templateMap[closedStatusPrompt]; setCommentType("internal"); setCommentDraft(QUICK_NOTE_TEMPLATES[key] || ""); setCommentEditorKey((k) => k + 1); setClosedStatusPrompt(null); setClosedStatusDialogQueue([]); } else if (closedStatusPrompt === "coe_gap") { recordClosedAnswer("coe_gap", "yes"); updateTicket({ labels: [...closedStatusPendingLabels, "coe_gap"] }, { preserveCommentDraft: true }); setCommentType("internal"); setCommentDraft(QUICK_NOTE_TEMPLATES.coeGap || ""); setCommentEditorKey((k) => k + 1); setClosedStatusPrompt(null); setClosedStatusDialogQueue([]); setClosedStatusPendingLabels([]); } else { recordClosedAnswer(closedStatusPrompt, "yes"); const label = LABEL_DIALOG_MAP[closedStatusPrompt]; if (label) { updateTicket({ status: "Closed", labels: [...closedStatusPendingLabels, label] }); } else { updateTicket({ status: "Closed" }); } setClosedStatusPrompt(null); setClosedStatusDialogQueue([]); setClosedStatusPendingLabels([]); clearClosedAnswersForTicket(); } } function handleClosedPromptSkipOrNo() { const answer = COMMENT_DIALOGS.includes(closedStatusPrompt) ? "skip" : "no"; recordClosedAnswer(closedStatusPrompt, answer); const rest = closedStatusDialogQueue.slice(1); setClosedStatusDialogQueue(rest); setClosedStatusPrompt(rest[0] || null); if (rest.length === 0) { updateTicket({ status: "Closed" }); setClosedStatusPendingLabels([]); clearClosedAnswersForTicket(); } } function handleArrBlur(e) { const raw = e.target.value; if (!raw) { return; } const value = Number(raw.replace(/,/g, "")); if (Number.isNaN(value)) { alert("ARR must be a number."); return; } updateTicket({ arr: value }); } function handleIntercomBlur(e) { const value = e.target.value || null; updateTicket({ intercom_url: value }); } function handleTeamUrlBlur(e) { const value = e.target.value || null; updateTicket({ team_url: value }); } function handleAssigneeChange(e) { const accountId = e.target.value || null; updateTicket({ assignee_id: accountId }); } function handleCustomerChange(e) { const value = e.target.value || ""; updateTicket({ customer: value || null }); } function handleAddLabel(label) { if (!label.trim()) return; const current = selectedDetail?.labels || []; if (current.includes(label.trim())) return; updateTicket({ labels: [...current, label.trim()] }); } function handleRemoveLabel(label) { const current = selectedDetail?.labels || []; updateTicket({ labels: current.filter((l) => l !== label) }); } async function handleDeleteAttachment(attachmentId) { if (!selectedKey || !confirm("Delete this attachment?")) return; setUpdateBusy(true); try { const res = await apiFetch( `${BASE_API_URL}/api/tickets/${encodeURIComponent(selectedKey)}/attachments/${encodeURIComponent(attachmentId)}`, { method: "DELETE" } ); if (!res.ok) throw new Error(await res.text()); await fetchTicketDetail(selectedKey); } catch (e) { console.error(e); alert("Failed to delete attachment."); } finally { setUpdateBusy(false); } } function handleAddComment() { const content = commentDraft.trim(); if (!content) return; const hasHtml = /<[a-z][\s\S]*>/i.test(content); const payload = { comment_internal: commentType === "internal" }; if (hasHtml) { try { payload.comment_adf = htmlToAdf(content, selectedKey); } catch (e) { const div = document.createElement("div"); div.innerHTML = content; payload.comment = (div.innerText || div.textContent || "").trim(); } } else { payload.comment = content; } updateTicket(payload); } function handleDataLossQuickAdd() { const adam = users.find((u) => { const name = (u.display_name || "").toLowerCase(); return name.includes("adam") && name.includes("pikulik"); }); const accountId = adam?.account_id || currentUserId; if (!accountId) { alert("Could not find Adam Pikulik in users. Please add the comment manually."); return; } const adf = { version: 1, type: "doc", content: [{ type: "paragraph", content: [ { type: "mention", attrs: { id: accountId, text: "@Adam Pikulik" } }, { type: "text", text: " Data loss" }, ], }], }; updateTicket({ comment_internal: true, comment_adf: adf }); } const QUICK_NOTE_TEMPLATES = { knowledgeGap: `

#knowledge_gap

Describe why you think it is the knowledge gap issue. Is it covered by a documentation? Is it something very well known?

Example:

#knowledge_gap

The reporter should know that dots are being changed to underscores. It is a part of this training: <link to the training>

`, coeGap: `

#coe_gap

What R&D did to solve the problem? What knowledge/tools you missed to solve it on your own?

`, escalationQuality: `

#reporter-responsibility

Describe why you think it is the reporter should do more before escalating to COE. Is it covered by documentation or the reporter should do more research before escalating? What kind of research? Does TSE or TAM should know the answer? Should he ask first on cs-internal?

Example:

#reporter-responsibility

The reporter should check with kapa.ai first as I did and I found the answer there.

`, knownIssue: `

#known-issue

Ticket is covered by known issue - Slack, BUGV2 is already open. Put an internal note with the link.

Example:

#known-issue

Reported in BUGV2-1234.

`, }; function handleQuickNoteTemplate(templateKey) { setCommentType("internal"); setCommentDraft(QUICK_NOTE_TEMPLATES[templateKey] || ""); setCommentEditorKey((k) => k + 1); } const visibleComments = selectedDetail && selectedDetail.comments ? [...selectedDetail.comments] .slice(Math.max(0, selectedDetail.comments.length - visibleCommentsCount)) .reverse() : []; const canLoadMoreComments = selectedDetail && selectedDetail.comments && selectedDetail.comments.length > visibleCommentsCount; if (authConfigError) { return (

Configuration required

{authConfigError}

); } if (authRequiredBlock) { return (

Sign in

This deployment uses Amazon Cognito. Use your @coralogix.com account and complete MFA (TOTP) when prompted.

); } return (
J

Jira Service Desk Companion

Local dashboard for your COE queue
setSearchQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSearchAll(); }} /> {searchSuggestionsOpen && searchSuggestions.length > 0 && (
{searchSuggestions.map((t) => ( ))}
)}
{userName && (
{(userName || "?").trim().charAt(0).toUpperCase()} {userName} {userPrivilege && ( {userPrivilege} )}
)} {teamStatsPage && ( )} {comparePage && ( )} {statsFilterView && ( )} {searchMode && !statsFilterView && !teamStatsPage && !comparePage && ( )} {jiraCredsStatus?.applicable && ( )} {cognitoAuthEnabled && ( )}
{jiraCredsStatus?.applicable && jiraCredsStatus?.configured === false && (
Jira API token required.{" "} Your account is in the {jiraCredsStatus.privilege} group, which uses your personal Jira PAT. Add it once and the dashboard will load your tickets, comments, and edits.
)} {jiraCredsModalOpen && ( setJiraCredsModalOpen(false)} onSaved={() => { setJiraCredsModalOpen(false); setJiraCredsStatus({ ...(jiraCredsStatus || {}), applicable: true, configured: true }); // Re-fetch the data that previously 503'd because the secret was missing. fetchTickets(); }} /> )} {!statsFilterView && !teamStatsPage && !comparePage && (
Quarter: Customer tier: Assignee: Status: {Array.from(new Set(displayTickets.map((t) => t.status).filter(Boolean))).map( (status) => ( ) )}
)}
{comparePage ? loadingCompare && !compareData ? "Loading comparison…" : "Quarter comparison — pick 1–4 quarters on each side" : teamStatsPage ? loadingTeamStats && !teamStatsRaw ? "Loading teammate statistics…" : "Teammate statistics — same quarter rules as Quarter statistics" : statsFilterView ? statsFilterView.tickets === null ? "Loading tickets…" : `${statsFilterView.label}: ${statsFilterView.tickets.length} ticket${statsFilterView.tickets.length !== 1 ? "s" : ""}` : loadingTickets ? "Loading tickets…" : searchMode ? `Search results: ${filteredTickets.length} ticket${filteredTickets.length !== 1 ? "s" : ""}` : `Displayed ${filteredTickets.length} tickets`}
{comparePage ? ( toggleCompareSet(setCompareA, compareA, q)} toggleB={(q) => toggleCompareSet(setCompareB, compareB, q)} applyPreset={(a, b) => { setCompareA(a); setCompareB(b); }} data={compareData} loading={loadingCompare} error={compareError} onBack={exitComparePage} onRefresh={refreshCompare} /> ) : teamStatsPage ? ( loadingTeamStats && !teamStatsRaw ? (
Loading teammate statistics…
) : !teamStatsRaw ? (
Unable to load teammate statistics. Check the server and use Refresh or go back.
) : ( ) ) : ( <>
{statsFilterView ? ( <>
{statsFilterView.label}
{statsFilterView.tickets === null ? "…" : `${statsFilterView.tickets.length} ticket${statsFilterView.tickets.length !== 1 ? "s" : ""}`}
{statsFilterView.tickets === null ? (
Loading tickets…
) : statsFilterView.tickets.length === 0 ? (
No tickets for this filter.
) : ( renderTierGroups( statsFilterView.tickets, selectedKey, handleSelectTicket, { showUnassignedTimer: false, jiraBaseUrl } ) )}
) : ( <>
Unassigned tickets
{unassignedTickets.length} open
{unassignedTickets.length === 0 && (
No unassigned tickets.
)} {renderTierGroups(unassignedTickets, selectedKey, handleSelectTicket, { showUnassignedTimer: true, jiraBaseUrl })}
Your active tickets
{yourActiveTickets.length} active
{yourActiveTickets.length === 0 && (
No active tickets assigned to you.
)} {renderTierGroups( yourActiveTickets, selectedKey, handleSelectTicket, { jiraBaseUrl } )}
Escalated tickets
{escalatedTickets.length} escalated
{escalatedTickets.length === 0 && (
No escalated tickets assigned to you.
)} {renderTierGroups( escalatedTickets, selectedKey, handleSelectTicket, { jiraBaseUrl } )}
Waiting for customer
{waitingForCustomerTickets.length} waiting
{waitingForCustomerTickets.length === 0 && (
No tickets waiting for customer for this assignee.
)} {renderTierGroups( waitingForCustomerTickets, selectedKey, handleSelectTicket, { jiraBaseUrl } )}
Closed / Done / Canceled / Solution Provided
{closedDoneCanceledTickets.length} resolved
{closedDoneCanceledTickets.length === 0 && (
No closed/done/canceled/solution provided tickets for this filter.
)} {renderTierGroups( closedDoneCanceledTickets, selectedKey, handleSelectTicket, { jiraBaseUrl } )}
)}
{selectedKey ? (
{loadingDetail || !selectedDetail ? (
Loading ticket details from Jira…
) : ( <>
{selectedDetail.key} · {selectedDetail.title}
Description
{selectedDetail.description || "No description."}
Attachments
{(selectedDetail.attachments || []).length === 0 ? (
No attachments
) : ( (selectedDetail.attachments || []).map((att) => { const viewUrl = `${BASE_API_URL}/api/tickets/${encodeURIComponent(selectedKey)}/attachments/${encodeURIComponent(att.id)}/content`; const canViewInBrowser = /\.(png|jpg|jpeg|gif|webp|svg|yaml|yml|txt|json)$/i.test(att.filename || "") || /^(image\/|text\/|application\/json)/.test(att.mimeType || ""); return (
{att.filename}
); }) )}
Reporter {selectedDetail.reporter || "—"}
Assignee
Status
ARR
Customer
Team URL
{selectedDetail.team_url && ( )}
Intercom / Customer Chat Link
{selectedDetail.intercom_url && ( )}
Labels
{(selectedDetail.labels || []).map((label) => ( {label} ))}
setNewLabelDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleAddLabel(newLabelDraft); setNewLabelDraft(""); } }} style={{ flex: 1, minWidth: 80 }} /> {projectLabels .filter((l) => !(selectedDetail?.labels || []).includes(l)) .map((l) => (
Comments
Comment Templates:
/
{canLoadMoreComments && ( )} {visibleComments.length === 0 && (
No comments yet for this ticket.
)} {visibleComments.map((c) => ( fetchTicketDetail(selectedKey)} setUpdateBusy={setUpdateBusy} users={users} attachments={selectedDetail?.attachments || []} onImageClick={setImageLightboxSrc} /> ))}
)}
) : (

Quarter statistics

{stats && ( {stats.quarter_start} — {stats.quarter_end} )}
{loadingStats ? (
Loading statistics…
) : stats ? ( <>
Total tickets
{stats.total_tickets}
Knowledge Gap tickets
Poor escalation tickets
Known issue tickets
Median Resolution Time
{formatResolutionHours(stats.median_resolution_hours)}
Escalated tickets
{stats.median_resolution_by_tier && Object.keys(stats.median_resolution_by_tier).length > 0 && (
Median Resolution Time by Customer Tier Diamond, Platinum, Gold: green < 8 d, orange 8–10 d, red ≥ 10 d
Silver, Bronze: green < 12 d, orange 12–15 d, red ≥ 15 d
Standard, No ARR: always black (default)
Click a value to open resolved tickets for that tier (longest first).
{["Diamond", "Platinum", "Gold", "Silver", "Bronze", "Standard", "No ARR"].map( (tier) => stats.median_resolution_by_tier[tier] != null && (
{tier}
) )}
)} {stats.escalated_count != null && ( )}
Highlights (by assignee) Gold/Platinum/Diamond tickets: unassigned, in Backlog, Waiting for customer 3+ days, or WIP 5+ days.
Rule-based analysis (no AI). Updates every hour.
Optional: add your Gemini API key for AI prioritization suggestions.
{loadingHighlights ? (
Loading highlights…
) : highlights?.by_assignee ? (
{(highlights.assignee_order || Object.keys(highlights.by_assignee)).map((assigneeName) => { const bucket = highlights.by_assignee[assigneeName]; if (!bucket) return null; const total = (bucket.unassigned_high_tier?.length ?? 0) + (bucket.high_tier_backlog?.length ?? 0) + (bucket.high_tier_waiting_3d?.length ?? 0) + (bucket.high_tier_wip_5d?.length ?? 0); if (total === 0) return null; const cats = [ { key: "unassigned_high_tier", label: "Unassigned high-tier", tickets: bucket.unassigned_high_tier || [] }, { key: "high_tier_backlog", label: "In Backlog", tickets: bucket.high_tier_backlog || [] }, { key: "high_tier_waiting_3d", label: "Waiting 3+ days", tickets: bucket.high_tier_waiting_3d || [] }, { key: "high_tier_wip_5d", label: "WIP 5+ days", tickets: bucket.high_tier_wip_5d || [] }, ]; return (
{assigneeName} {total} ticket{total !== 1 ? "s" : ""}
{cats.filter((c) => c.tickets.length > 0).map((cat) => (
{cat.label} {cat.tickets.length}
))}
); })}
) : (
Unable to load highlights.
)}
AI insights (Gemini) saveGeminiKey(e.target.value)} title="Store your own key for AI insights. Get one at aistudio.google.com/app/apikey" />
{aiInsights && (
{aiInsights}
)}
) : (
Unable to load quarter statistics.
)}
)} )}
{imageLightboxSrc && (
setImageLightboxSrc(null)} role="button" tabIndex={0} onKeyDown={(e) => e.key === "Escape" && setImageLightboxSrc(null)} aria-label="Close fullscreen image" >
e.stopPropagation()} className="image-lightbox-img" />
)} {closedStatusPrompt && (

{closedStatusPrompt === "knowledge_gap" && "Do you want to add Knowledge Gap comment?"} {closedStatusPrompt === "escalation" && "Do you want to add Escalation quality comment?"} {closedStatusPrompt === "known_issue" && "Do you want to add Known issue comment?"} {closedStatusPrompt === "solution_confirmed" && "Did the customer confirm the provided solution?"} {closedStatusPrompt === "no_response" && "Do you close it due to no response from the customer?"} {closedStatusPrompt === "fixed_by_code" && "Did you escalate this issue to R&D and it will be fixed by code changes?"} {closedStatusPrompt === "coe_gap" && "Did you escalate this issue to R&D and they were able to provide the answer (no code change was needed)?"} {closedStatusPrompt === "still_open_on_RnD" && "Did you escalate this issue to R&D and the issue is still not concluded by R&D?"}

)}
); } function renderTierGroups(tickets, selectedKey, onSelect, opts = {}) { const { showUnassignedTimer = false, jiraBaseUrl = "" } = opts; const byTier = tickets.reduce((map, t) => { const tier = t.tier || "No ARR"; if (!map[tier]) map[tier] = []; map[tier].push(t); return map; }, {}); const tierOrder = [ "Diamond", "Platinum", "Gold", "Silver", "Bronze", "Standard", "No ARR", ]; return tierOrder .filter((tier) => byTier[tier] && byTier[tier].length > 0) .map((tier) => (
{byTier[tier].map((t) => (
onSelect(t.key)} >
{t.status} {formatCurrency(t.arr)} {t.resolution_hours != null && ( {formatResolutionHours(t.resolution_hours)} )}
{t.last_updated && !["Closed", "Done", "Canceled", "Cancelled", "Solution Provided"].includes(t.status) && Date.now() - new Date(t.last_updated).getTime() > 5 * 24 * 60 * 60 * 1000 && ( No update for 5 days )} {t.assignee_display || "Unassigned"} {showUnassignedTimer && t.is_unassigned && t.created && ( {formatDurationSince(t.created)} )}
))}
)); } ReactDOM.createRoot(document.getElementById("root")).render();