Δ = 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.
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.
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 && (
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).
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.
{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?"}