function createOpportunityCalendar({ environment, loadLibrary, getState, getItems, getVersion, formatRevenue, formatDate, getOwnerName, createDetailLink, createCopyButton, createCloseIcon, createStageBadge, createStatusBadge, opportunityUrl, modalId, closeSrPopover, createViewIcon }) { const { document } = environment; let elements = null; let calendar = null; let library = null; let selectedView = "overview"; let anchoredVersion = null; let details = null; let lastEvents = ""; let dayCounts = new Map(); const dayBadges = new Map(); let resizeObserver = null; let layoutFrame = null; let eventLimit = 2; function visibleEventLimit(height, weeks, headerHeight) { return Math.max(0, Math.min(2, Math.floor((height / Math.max(1, weeks) - headerHeight - 26) / 48))); } function scheduleLayout() { for (const [name, icon] of [["today", "today"], ["dayGridMonth", "calendar"], ["dayGridWeek", "week"]]) { const button = elements?.calendarHost.querySelector?.(`.fc-${name}-button`); if (button && !button.querySelector("svg")) button.prepend(createViewIcon(icon)); } const win = environment.window; if (!win?.requestAnimationFrame || layoutFrame !== null) return; layoutFrame = win.requestAnimationFrame(() => { layoutFrame = null; if (!calendar || selectedView !== "calendar" || !elements.calendarHost.clientHeight) return; calendar.updateSize(); const frame = elements.calendarHost.querySelector(".fc-daygrid-day-frame"); const events = frame?.querySelector(".fc-daygrid-day-events"); if (!frame || !events) return; const scroller = frame.closest(".fc-scroller"); const weeks = elements.calendarHost.querySelectorAll(".fc-daygrid-body tr").length; if (!scroller || !weeks) return; elements.calendarHost.style.setProperty("--opportunities-calendar-weeks", String(weeks)); const headerHeight = events.getBoundingClientRect().top - frame.getBoundingClientRect().top; const limit = visibleEventLimit(scroller.clientHeight, weeks, headerHeight); if (limit !== eventLimit) { eventLimit = limit; calendar.setOption("dayMaxEvents", limit); } }); } function dateKey(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function fiscalWeekLabel(date) { // FullCalendar's week hook supplies a UTC date marker, not a local instant. const start = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); start.setUTCDate(start.getUTCDate() - start.getUTCDay()); const end = new Date(start.getTime() + 6 * 86400000); const year = end.getUTCMonth() >= 5 ? end.getUTCFullYear() : end.getUTCFullYear() - 1; const fiscalStart = new Date(Date.UTC(year, 5, 1)); const firstSunday = fiscalStart.getTime() - fiscalStart.getUTCDay() * 86400000; const representative = start < fiscalStart ? fiscalStart : start; const quarter = Math.floor(((representative.getUTCMonth() + 7) % 12) / 3) + 1; const week = Math.round((start.getTime() - firstSunday) / (7 * 86400000)) + 1; return `FY${String(year + 1).slice(-2)}-Q${quarter}-W${week}`; } function updateDayBadge(badge, key) { const count = dayCounts.get(key) || 0; badge.hidden = count === 0; badge.textContent = String(count); badge.title = `${count} ${count === 1 ? "opportunity" : "opportunities"}`; badge.setAttribute("aria-label", badge.title); badge.setAttribute("data-empty", String(count === 0)); } function node(tag, className, text) { const el = document.createElement(tag); el.className = className; if (text !== undefined) el.textContent = text; return el; } function closeDate(value) { if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}(?:T|$)/.test(value)) return null; const day = value.slice(0, 10); const date = new Date(`${day}T00:00:00Z`); return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === day ? day : null; } function eventsFor(items) { return items.flatMap((item, index) => { const start = closeDate(item.EffectiveDate); if (!start) return []; return [{ id: String(item.OptyNumber || item.OptyId || index), title: String(item.Name || item.OptyNumber || "-"), start, allDay: true, extendedProps: { item }, classNames: [`opportunities-calendar-stage-${stage(item)}`] }]; }); } function stage(item) { return ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"].includes(item.ForecastGroup_c) ? item.ForecastGroup_c : "OTHER"; } function initialDate(items, now = new Date()) { const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const dates = items.map(item => closeDate(item.EffectiveDate)).filter(Boolean).sort(); return dates.find(date => date >= today) || dates.at(-1) || today; } function closeDetails(restoreFocus = true) { if (!details) return false; const previous = details; details = null; previous.overlay.remove(); if (restoreFocus) (previous.trigger?.isConnected ? previous.trigger : elements?.tabs.calendar)?.focus(); return true; } function showDialog(heading, content, contentControls, trigger) { closeDetails(false); const host = document.getElementById(modalId); if (!host || host.hidden) return; const overlay = node("section", "opportunities-extension-action-form-modal opportunities-calendar-detail"); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-labelledby", "opportunities-calendar-detail-title"); const panel = node("div", "opportunities-extension-action-form-dialog"); const header = node("header", "opportunities-extension-account-plans-header"); const title = node("h2", "", heading); title.id = "opportunities-calendar-detail-title"; const dismiss = node("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.title = "Close"; dismiss.setAttribute("aria-label", "Close opportunity details"); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => closeDetails()); header.append(title, dismiss); panel.append(header, content); overlay.append(panel); host.append(overlay); details = { overlay, trigger }; overlay.addEventListener("click", event => { if (event.target === overlay) closeDetails(); }); overlay.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeDetails(); } if (event.key === "Tab") { const controls = [dismiss, ...contentControls]; const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } } }); dismiss.focus(); } function showDetails(item, trigger) { const content = node("div", "opportunities-calendar-detail-content"); const link = createDetailLink(item.OptyNumber || "-", item.OptyNumber ? opportunityUrl + encodeURIComponent(item.OptyNumber) : ""); const identity = node("div", "opportunities-extension-opty-number-content"); identity.append(link); const copy = item.OptyNumber ? createCopyButton(item.OptyNumber) : null; if (copy) identity.append(copy); const summary = node("dl", "opportunities-extension-action-form-context"); for (const [label, value] of [["Customer", item.CustomerAccount?.PartyUniqueName], ["Owner", getOwnerName(item)], ["Close Date", closeDate(item.EffectiveDate) ? formatDate(item.EffectiveDate) : "N/A"], ["Amount", formatRevenue(item)]]) { summary.append(node("dt", "", label), node("dd", "", String(value || "-"))); } const badges = node("div", "opportunities-calendar-detail-badges"); badges.append(createStatusBadge(item.StatusCode), createStageBadge(item.ForecastGroup_c)); content.append(identity, summary, badges); showDialog(item.Name || item.OptyNumber || "Opportunity", content, item.OptyNumber ? [link, copy] : [], trigger); } function showDayOpportunities({ date, allSegs, jsEvent }) { jsEvent.preventDefault(); const trigger = jsEvent.currentTarget || elements?.tabs.calendar; const content = node("div", "opportunities-calendar-day-list"); const controls = []; const events = new Map(allSegs.map(({ event }) => [event.id, event])); for (const event of events.values()) { const item = event.extendedProps.item; const button = node("button", "opportunities-calendar-day-item"); button.type = "button"; const summary = `${item.OptyNumber || "-"} | ${item.ForecastGroup_c || "N/A"} | ${formatRevenue(item)}`; button.append(node("strong", "", event.title), node("span", "", summary)); button.title = `${event.title}\n${summary}`; button.addEventListener("click", () => showDetails(item, trigger)); controls.push(button); content.append(button); } showDialog(`${formatDate(date.toISOString().slice(0, 10))} | ${events.size} ${events.size === 1 ? "opportunity" : "opportunities"}`, content, controls, trigger); // A truthy non-string result prevents FullCalendar from opening its measured popover. return true; } function mount(body, tableSurface) { resizeObserver?.disconnect(); if (layoutFrame !== null) environment.window?.cancelAnimationFrame(layoutFrame); layoutFrame = null; eventLimit = 2; calendar?.destroy(); calendar = null; lastEvents = ""; anchoredVersion = null; selectedView = "overview"; dayBadges.clear(); dayCounts.clear(); closeDetails(false); const workspace = node("section", "opportunities-extension-views"); const tablist = node("div", "opportunities-extension-view-tabs"); tablist.setAttribute("role", "tablist"); tablist.setAttribute("aria-label", "Opportunity views"); const tabs = {}; const surface = node("section", "opportunities-extension-calendar"); const message = node("p", "opportunities-calendar-message"); message.setAttribute("role", "status"); const calendarHost = node("div", "opportunities-calendar-grid"); const undated = node("details", "opportunities-calendar-undated"); surface.append(message, calendarHost, undated); for (const [key, label, panel] of [["overview", "Overview", tableSurface], ["calendar", "Calendar", surface]]) { const tab = node("button", ""); tab.type = "button"; tab.id = `opportunities-view-${key}-tab`; tab.append(createViewIcon(key), node("span", "", label)); panel.id = `opportunities-view-${key}`; panel.setAttribute("role", "tabpanel"); panel.setAttribute("aria-labelledby", tab.id); tab.setAttribute("role", "tab"); tab.setAttribute("aria-controls", panel.id); tab.addEventListener("click", () => selectView(key)); tab.addEventListener("keydown", event => { if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; event.preventDefault(); const next = event.key === "Home" ? "overview" : event.key === "End" ? "calendar" : key === "overview" ? "calendar" : "overview"; selectView(next); tabs[next].focus(); }); tabs[key] = tab; tablist.append(tab); } workspace.append(tablist, tableSurface, surface); body.append(workspace); elements = { workspace, tableSurface, surface, message, calendarHost, undated, tabs }; if (environment.window?.ResizeObserver) { resizeObserver = new environment.window.ResizeObserver(scheduleLayout); resizeObserver.observe(calendarHost); } selectView("overview"); } function selectView(view) { if (!elements || !["overview", "calendar"].includes(view)) return; closeDetails(false); closeSrPopover(); selectedView = view; elements.tableSurface.hidden = view !== "overview"; elements.surface.hidden = view !== "calendar"; for (const [key, tab] of Object.entries(elements.tabs)) { tab.setAttribute("aria-selected", String(view === key)); tab.tabIndex = view === key ? 0 : -1; } render(getItems(), getState()); if (view === "calendar") calendar?.updateSize(); scheduleLayout(); } function render(items, state) { if (!elements || selectedView !== "calendar") return; const { surface, message, calendarHost, undated } = elements; surface.setAttribute("aria-busy", String(state.status === "loading")); if (["loading", "error", "empty", "idle"].includes(state.status)) { message.textContent = state.message || (state.status === "loading" ? "Loading opportunities..." : "No opportunities found."); message.hidden = false; calendarHost.hidden = true; undated.hidden = true; closeDetails(false); return; } message.textContent = items.length ? "" : "No opportunities found."; message.hidden = !!items.length; calendarHost.hidden = false; const events = eventsFor(items); dayCounts = new Map(); for (const event of events) dayCounts.set(event.start, (dayCounts.get(event.start) || 0) + 1); for (const { badge, key } of dayBadges.values()) updateDayBadge(badge, key); const anchor = initialDate(state.items); if (!calendar) { try { library ||= loadLibrary(); calendar = new library.Calendar(calendarHost, { initialView: "dayGridMonth", initialDate: anchor, height: "100%", expandRows: true, fixedWeekCount: false, datesSet: scheduleLayout, locale: "en", firstDay: 0, editable: false, selectable: false, eventInteractive: true, headerToolbar: { left: "prev,next today", center: "title", right: "dayGridMonth,dayGridWeek" }, buttonText: { today: "Today", month: "Month", week: "Week" }, buttonHints: { prev: "Previous", next: "Next", today: "Today", dayGridMonth: "Month", dayGridWeek: "Week" }, dayMaxEvents: 2, moreLinkText: count => `... +${count}`, displayEventTime: false, moreLinkHint: count => `View ${count} more ${count === 1 ? "opportunity" : "opportunities"}`, moreLinkClick: showDayOpportunities, weekNumbers: true, weekNumberContent: ({ date }) => ({ domNodes: [node("span", "opportunities-calendar-fiscal-week", fiscalWeekLabel(date))] }), dayCellDidMount: ({ el, date }) => { const badge = node("span", "opportunities-calendar-day-count"); const key = dateKey(date); updateDayBadge(badge, key); el.querySelector(".fc-daygrid-day-frame").append(badge); dayBadges.set(el, { badge, key }); }, dayCellWillUnmount: ({ el }) => { dayBadges.get(el)?.badge.remove(); dayBadges.delete(el); }, eventOrder: "title", eventDisplay: "block", events, eventContent: ({ event }) => { const item = event.extendedProps.item; const card = node("div", "opportunities-calendar-event-content"); const summary = node("span", "", `${item.OptyNumber || "-"} | ${item.ForecastGroup_c || "N/A"} | ${formatRevenue(item)}`); summary.title = summary.textContent; card.append(node("strong", "", event.title), summary); return { domNodes: [card] }; }, eventDidMount: ({ el, event }) => { el.title = `${event.title}\n${event.extendedProps.item.CustomerAccount?.PartyUniqueName || ""}\n${formatDate(event.startStr)} | ${event.extendedProps.item.ForecastGroup_c || "-"}`; el.setAttribute("role", "button"); el.setAttribute("aria-label", el.title); }, eventClick: ({ event, el, jsEvent }) => { jsEvent.preventDefault(); showDetails(event.extendedProps.item, el); } }); calendar.render(); lastEvents = JSON.stringify(events); anchoredVersion = getVersion(); } catch { calendar?.destroy(); calendar = null; message.textContent = "Unable to display the calendar. Use the overview or try again."; message.hidden = false; calendarHost.hidden = true; undated.hidden = true; return; } } const signature = JSON.stringify(events); calendar.batchRendering(() => { if (anchoredVersion !== getVersion()) { calendar.gotoDate(anchor); anchoredVersion = getVersion(); closeDetails(false); } if (signature !== lastEvents) { calendar.removeAllEvents(); calendar.addEventSource(events); lastEvents = signature; closeDetails(false); } }); const missing = items.filter(item => !closeDate(item.EffectiveDate)); undated.hidden = !missing.length; undated.textContent = ""; if (missing.length) { undated.append(node("summary", "", `No valid Close Date (${missing.length})`)); for (const item of missing) { const button = node("button", "", `${item.OptyNumber || "-"} | ${item.Name || "-"}`); button.type = "button"; button.addEventListener("click", () => showDetails(item, button)); undated.append(button); } } scheduleLayout(); } return { mount, render, selectView, closeDetails, closeDate, eventsFor, initialDate, fiscalWeekLabel, visibleEventLimit }; }