Table sorting preferences will be saved and reused based on the user’s selections
This commit is contained in:
@@ -29,6 +29,7 @@ function createContentApplication(overrides = {}) {
|
||||
const authSessionState = createAuthSessionState(contracts);
|
||||
const authSalesCommonState = createAuthSalesCommonState();
|
||||
const opportunitiesQueryState = createOpportunitiesQueryState();
|
||||
const tableSorting = createTableSorting({ environment });
|
||||
const opportunitiesTableState = createOpportunitiesTableState();
|
||||
const opportunitiesFilterState = createOpportunitiesFilterState(contracts);
|
||||
const preferencesState = createPreferencesState(contracts);
|
||||
@@ -137,7 +138,7 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const svgPaths = createSvgPaths({ environment });
|
||||
const manageTime = createManageTime({ MODAL_ID: contracts.MODAL_ID, environment, searchableSelect, dates: createTimeDates(),
|
||||
const manageTime = createManageTime({ MODAL_ID: contracts.MODAL_ID, environment, tableSorting, searchableSelect, dates: createTimeDates(),
|
||||
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||
async requestDelete(id) {
|
||||
await srSessionPreparation.ensure();
|
||||
@@ -363,6 +364,7 @@ function createContentApplication(overrides = {}) {
|
||||
closeSrPopover: () => crmSrPopover.close()
|
||||
});
|
||||
const opportunitiesTable = createOpportunitiesTable({
|
||||
tableSorting,
|
||||
renderCalendar: (...args) => opportunityCalendar.render(...args),
|
||||
renderActivityFilters: () => activityFilters.render(),
|
||||
closeCrmSrPopover: (...args) => crmSrPopover.close(...args),
|
||||
@@ -607,6 +609,7 @@ function createContentApplication(overrides = {}) {
|
||||
queueProductsTableRender: () => productsContentJobs.queueProductsTableRender()
|
||||
});
|
||||
const rampProjectionView = createRampProjectionView({
|
||||
tableSorting,
|
||||
saveEstimates: product => workloadForecastUpdate.save(product),
|
||||
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||
MODAL_ID: contracts.MODAL_ID,
|
||||
@@ -630,6 +633,7 @@ function createContentApplication(overrides = {}) {
|
||||
});
|
||||
const customerTableSearch = createCustomerTableSearch({ environment });
|
||||
const installBaseView = createInstallBaseView({
|
||||
tableSorting,
|
||||
createSearch: (...args) => customerTableSearch.createSearch(...args),
|
||||
MODAL_ID: contracts.MODAL_ID,
|
||||
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||
@@ -645,6 +649,7 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const serviceRequestsView = createServiceRequestsView({
|
||||
tableSorting,
|
||||
createSearch: (...args) => customerTableSearch.createSearch(...args),
|
||||
MODAL_ID: contracts.MODAL_ID,
|
||||
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||
@@ -690,6 +695,7 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const ratecardView = createRatecardView({
|
||||
tableSorting,
|
||||
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
|
||||
columns: ratecardModel.columns,
|
||||
formatValue: (...args) => ratecardModel.formatValue(...args),
|
||||
|
||||
@@ -99,6 +99,7 @@ function createContentModal({
|
||||
}
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.setAttribute("lang", "en");
|
||||
debugState.requestLog = [];
|
||||
authSessionState.tokenRelayRequest = null;
|
||||
authSessionState.consumerTokenServiceRequest = null;
|
||||
@@ -154,7 +155,7 @@ function createContentModal({
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.type = "button";
|
||||
closeButton.className = "opportunities-extension-icon-button";
|
||||
closeButton.setAttribute("aria-label", "Fechar");
|
||||
closeButton.setAttribute("aria-label", "Close");
|
||||
closeButton.textContent = "X";
|
||||
closeButton.addEventListener("click", closeOpportunitiesModal);
|
||||
|
||||
@@ -170,7 +171,7 @@ function createContentModal({
|
||||
const preferencesButton = document.createElement("button");
|
||||
preferencesButton.type = "button";
|
||||
preferencesButton.className = "opportunities-extension-icon-button";
|
||||
preferencesButton.setAttribute("aria-label", "Abrir preferências");
|
||||
preferencesButton.setAttribute("aria-label", "Open preferences");
|
||||
preferencesButton.title = "Preferences";
|
||||
preferencesButton.append(createSettingsIcon());
|
||||
preferencesButton.addEventListener("click", openPreferencesModal);
|
||||
|
||||
@@ -218,7 +218,7 @@ function createDebugContentView({
|
||||
const startedTime = document.createElement("time");
|
||||
startedTime.className = "opportunities-extension-debug-time";
|
||||
startedTime.textContent = formatDebugTime(entry.startedAt);
|
||||
startedTime.title = `Started: ${entry.startedAt.toLocaleString()}`;
|
||||
startedTime.title = `Started: ${entry.startedAt.toLocaleString("en-GB")}`;
|
||||
const method = document.createElement("span");
|
||||
method.className = "opportunities-extension-debug-method";
|
||||
method.textContent = entry.method || "GET";
|
||||
@@ -228,7 +228,7 @@ function createDebugContentView({
|
||||
const hasDuration = typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && entry.durationMs >= 0;
|
||||
duration.textContent = hasDuration
|
||||
? entry.durationMs < 1000 ? `${Math.round(entry.durationMs)} ms` : `${(entry.durationMs / 1000).toFixed(2)} s`
|
||||
: entry.status === "pending" ? "In progress" : "N/D";
|
||||
: entry.status === "pending" ? "In progress" : "N/A";
|
||||
duration.title = hasDuration ? `Request duration: ${entry.durationMs} ms` : "Request duration not yet available";
|
||||
summaryMeta.append(startedTime, method, httpStatus, status, duration);
|
||||
|
||||
@@ -349,7 +349,7 @@ function createDebugContentView({
|
||||
}
|
||||
|
||||
function formatDebugTime(date) {
|
||||
return date.toLocaleTimeString(undefined, {
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
|
||||
@@ -78,3 +78,5 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.opportunities-extension-install-base-table .opportunities-extension-account-plans-skeleton-bar { animation: none; }
|
||||
}
|
||||
.opportunities-extension-install-base-table .opportunities-extension-sort-button { min-height: 28px; padding: 4px 16px 4px 0; overflow-wrap: anywhere; }
|
||||
.opportunities-extension-install-base-table .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) {
|
||||
function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, tableSorting, environment }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
const columns = [
|
||||
@@ -33,7 +33,8 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
||||
}
|
||||
return String(value).trim();
|
||||
}
|
||||
function createTable(items, loading = false) {
|
||||
const sortColumns = columns.map(([key, , type]) => ({ key, type: ["amount", "quantity"].includes(type) ? "number" : type }));
|
||||
function createTable(items, loading = false, sorting = tableSorting.create("installed-base", sortColumns), onSort) {
|
||||
const table = element("table", "opportunities-extension-install-base-table");
|
||||
table.setAttribute("aria-label", "Installed base items");
|
||||
const head = element("thead");
|
||||
@@ -43,13 +44,14 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
||||
const col = element("col");
|
||||
col.className = `install-base-col-${key}`;
|
||||
group.append(col);
|
||||
const th = element("th", "", label);
|
||||
const th = element("th");
|
||||
th.scope = "col";
|
||||
sorting.header(th, key, label, onSort, loading);
|
||||
header.append(th);
|
||||
});
|
||||
head.append(header);
|
||||
const body = element("tbody");
|
||||
(loading ? Array.from({ length: 8 }, () => ({})) : items).forEach(item => {
|
||||
(loading ? Array.from({ length: 8 }, () => ({})) : sorting.select(items)).forEach(item => {
|
||||
const row = element("tr");
|
||||
columns.forEach(([key, , type]) => {
|
||||
const cell = element("td");
|
||||
@@ -103,6 +105,7 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
||||
const status = element("p", "opportunities-extension-install-base-message", "Loading installed base...");
|
||||
status.setAttribute("role", "status");
|
||||
let loadedItems = [], loaded = false, failure = "";
|
||||
const sorting = tableSorting.create("installed-base", sortColumns);
|
||||
const search = createSearch({ label: "Search installed base", onSearch: renderResults,
|
||||
getValues: item => columns.flatMap(([key, , type]) => [item[key], formatValue(item[key], type)]) });
|
||||
const toolbar = element("div", "opportunities-extension-customer-table-toolbar");
|
||||
@@ -115,7 +118,7 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
const controls = [close, search.input, content];
|
||||
const controls = [close, search.input, content, ...content.querySelectorAll("button")].filter(control => !control.disabled);
|
||||
const index = controls.indexOf(document.activeElement);
|
||||
controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length].focus();
|
||||
}
|
||||
@@ -130,7 +133,10 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
||||
: "No installed base items found for this customer.");
|
||||
status.setAttribute("role", failure ? "alert" : "status");
|
||||
content.append(status);
|
||||
if (!failure && items.length) content.append(createTable(items));
|
||||
if (!failure && items.length) content.append(createTable(items, false, sorting, key => {
|
||||
renderResults();
|
||||
content.querySelector(`[data-sort-key="${key}"]`)?.focus();
|
||||
}));
|
||||
content.scrollTop = 0;
|
||||
}
|
||||
return { render(items, error = "") {
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
.oe-time-grid { overflow-x: auto; }
|
||||
.oe-time-grid table { width: 100%; min-width: 1060px; border-collapse: collapse; table-layout: fixed; }
|
||||
.oe-time-grid th { background: var(--time-muted); padding: 10px 5px; font-weight: 600; text-align: center; border-bottom: 1px solid var(--time-border); }
|
||||
.oe-time-grid .opportunities-extension-sort-button { min-width: 0; min-height: 28px; padding: 4px 12px 4px 0; border: 0; border-radius: 0; gap: 0; }
|
||||
.oe-time-grid .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }
|
||||
.oe-time-grid .oe-time-day-label { margin-bottom: 0; }
|
||||
.oe-time-grid th:first-child { width: 23%; }
|
||||
.oe-time-grid th:nth-child(2), .oe-time-grid th:nth-child(3) { width: 12%; }
|
||||
.oe-time-grid th:nth-last-child(-n+3) { width: 46px; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requestWeek, requestSave, requestDelete, notify = () => {}, requestEntries = async () => [], requestServices = async () => [], requestExpand = async () => [], requestTaskTypes = async () => [] }) {
|
||||
function createManageTime({ MODAL_ID, environment, tableSorting, searchableSelect, dates, requestWeek, requestSave, requestDelete, notify = () => {}, requestEntries = async () => [], requestServices = async () => [], requestExpand = async () => [], requestTaskTypes = async () => [] }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
let ready = false;
|
||||
@@ -142,6 +142,16 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
});
|
||||
function blankRow(type) { return { type, hours: {}, invalidHours: {}, notes: "", srNumber: "", activity: "", taskType: "", notesOpen: false }; }
|
||||
const hourFields = ["sundayTaskHours", "mondayTaskHours", "tuesdayTaskHours", "wednesdayTaskHours", "thursdayTaskHours", "fridayTaskHours", "saturdayTaskHours"];
|
||||
const sorting = tableSorting.create("manage-time", [
|
||||
{ key: "service", value: row => row.type === "Non Service" ? "Non Service" : `${entry.services.find(sr => sr.srNumber === row.srNumber)?.srTitle || ""} ${row.srNumber}` },
|
||||
{ key: "activity" },
|
||||
{ key: "taskType", value: row => entry.taskTypes.find(item => item.lookUpCode_c === row.taskType)?.lookUpMeaning_c || row.taskType },
|
||||
{ key: "notes" },
|
||||
...hourFields.map((key, weekday) => ({ key, type: "number", value: row => {
|
||||
const day = entry.days.find(item => dates.fromApi(item.Date).getUTCDay() === weekday);
|
||||
return day ? row.hours[day.Date] : null;
|
||||
} }))
|
||||
]);
|
||||
function rowPayload(row) {
|
||||
const payload = { srNumber: row.type === "Non Service" ? "non-service-sr" : row.srNumber,
|
||||
activityCode: row.activity, taskType: row.taskType, deliveryType: row.deliveryType || "",
|
||||
@@ -247,7 +257,8 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
const focusedId = document.activeElement?.id;
|
||||
combos = [];
|
||||
entry.tbody.replaceChildren();
|
||||
entry.rows.forEach((row, index) => {
|
||||
sorting.select(entry.rows).forEach(row => {
|
||||
const index = entry.rows.indexOf(row);
|
||||
const tr = node("tr");
|
||||
if (row.duplicateHighlightAt && Date.now() - row.duplicateHighlightAt < 5500) {
|
||||
tr.className = "oe-time-duplicate-row";
|
||||
@@ -451,15 +462,19 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
if (!entry.drafts.has(key)) entry.drafts.set(key, records.length ? rowsFromEntries(records, items) : Array.from({ length: 3 }, () => blankRow("Service")));
|
||||
entry.rows = entry.drafts.get(key);
|
||||
const head = node("thead"), row = node("tr");
|
||||
for (const label of ["Service Request", "Activity", "Task Type"]) row.append(node("th", "", label));
|
||||
for (const [key, label] of [["service", "Service Request"], ["activity", "Activity"], ["taskType", "Task Type"]]) {
|
||||
const th = node("th"); sorting.header(th, key, label, renderRows); row.append(th);
|
||||
}
|
||||
entry.dayTotals = [];
|
||||
for (const day of items) {
|
||||
const parsed = dates.fromApi(day.Date), th = node("th", "oe-time-day");
|
||||
const dayLabel = node("span", "oe-time-day-label", ["Su", "M", "T", "W", "Th", "F", "Sa"][parsed.getUTCDay()]);
|
||||
const dayButton = sorting.header(th, hourFields[parsed.getUTCDay()], ["Su", "M", "T", "W", "Th", "F", "Sa"][parsed.getUTCDay()], renderRows);
|
||||
const dayLabel = dayButton.children[0]; dayLabel.className += " oe-time-day-label";
|
||||
const count = node("span", "oe-time-day-total", "0"); entry.dayTotals.push(count); dayLabel.append(count);
|
||||
th.append(dayLabel, node("span", "", parsed.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))); row.append(th);
|
||||
th.append(node("span", "", parsed.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))); row.append(th);
|
||||
}
|
||||
for (const label of ["Notes", "Copy", ""]) row.append(node("th", "", label));
|
||||
const notesHeading = node("th"); sorting.header(notesHeading, "notes", "Notes", renderRows); row.append(notesHeading);
|
||||
for (const label of ["Copy", ""]) row.append(node("th", "", label));
|
||||
[...row.children].forEach(th => th.setAttribute("scope", "col"));
|
||||
head.append(row); entry.tbody = node("tbody"); table.replaceChildren(head, entry.tbody);
|
||||
endingValue.textContent = dates.fromApi(items[0].EndDate).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" }); renderRows();
|
||||
|
||||
@@ -60,7 +60,7 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
const count = dayCounts.get(key) || 0;
|
||||
badge.hidden = count === 0;
|
||||
badge.textContent = String(count);
|
||||
badge.title = `${count} oportunidade${count === 1 ? "" : "s"}`;
|
||||
badge.title = `${count} ${count === 1 ? "opportunity" : "opportunities"}`;
|
||||
badge.setAttribute("aria-label", badge.title);
|
||||
badge.setAttribute("data-empty", String(count === 0));
|
||||
}
|
||||
@@ -137,7 +137,7 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
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/D"], ["Amount", formatRevenue(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");
|
||||
@@ -154,13 +154,13 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
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/D"} | ${formatRevenue(item)}`;
|
||||
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} oportunidades`, content, controls, trigger);
|
||||
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;
|
||||
}
|
||||
@@ -173,14 +173,14 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
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", "Visualiza\u00e7\u00e3o das oportunidades");
|
||||
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", "Vis\u00e3o geral", tableSurface], ["calendar", "Calend\u00e1rio", surface]]) {
|
||||
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);
|
||||
@@ -234,12 +234,12 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
calendar = new library.Calendar(calendarHost, {
|
||||
initialView: "dayGridMonth", initialDate: anchor, height: "100%", expandRows: true, fixedWeekCount: false,
|
||||
datesSet: scheduleLayout,
|
||||
locale: "pt-br", firstDay: 0, editable: false, selectable: false, eventInteractive: true,
|
||||
locale: "en", firstDay: 0, editable: false, selectable: false, eventInteractive: true,
|
||||
headerToolbar: { left: "prev,next today", center: "title", right: "dayGridMonth,dayGridWeek" },
|
||||
buttonText: { today: "Hoje", month: "M\u00eas", week: "Semana" },
|
||||
buttonHints: { prev: "Anterior", next: "Pr\u00f3ximo", today: "Hoje", dayGridMonth: "M\u00eas", dayGridWeek: "Semana" },
|
||||
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 => `Ver mais ${count} oportunidades`,
|
||||
moreLinkHint: count => `View ${count} more ${count === 1 ? "opportunity" : "opportunities"}`,
|
||||
moreLinkClick: showDayOpportunities,
|
||||
weekNumbers: true,
|
||||
weekNumberContent: ({ date }) => ({ domNodes: [node("span", "opportunities-calendar-fiscal-week", fiscalWeekLabel(date))] }),
|
||||
@@ -255,7 +255,7 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
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/D"} | ${formatRevenue(item)}`);
|
||||
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] };
|
||||
@@ -280,7 +280,7 @@ function createOpportunityCalendar({ environment, loadLibrary, getState, getItem
|
||||
const missing = items.filter(item => !closeDate(item.EffectiveDate));
|
||||
undated.hidden = !missing.length; undated.textContent = "";
|
||||
if (missing.length) {
|
||||
undated.append(node("summary", "", `Sem Close Date v\u00e1lido (${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);
|
||||
|
||||
@@ -72,15 +72,15 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
||||
const summary = getSummary(optyNumber);
|
||||
const reportedHours = summary.reportedHours !== null
|
||||
? new Intl.NumberFormat("en-US", { useGrouping: false, maximumFractionDigits: 10 }).format(summary.reportedHours)
|
||||
: "N/D";
|
||||
setBadgeContent(badge, "sr", status === "error" ? "?" : items.length ? `${reportedHours}${reportedHours === "N/D" ? "" : "h"}` : "0h");
|
||||
const display = value => value === null || value === undefined || value === "" ? "N/D" : String(value);
|
||||
: "N/A";
|
||||
setBadgeContent(badge, "sr", status === "error" ? "?" : items.length ? `${reportedHours}${reportedHours === "N/A" ? "" : "h"}` : "0h");
|
||||
const display = value => value === null || value === undefined || value === "" ? "N/A" : String(value);
|
||||
const descriptions = items.map(item => [
|
||||
`Title: ${display(item.Title)}`,
|
||||
`Opportunity: ${display(item.OpportunityName_Id_c?.OptyNumber)}`,
|
||||
`Reported Hours: ${display(item.ReportedHours_c)}`,
|
||||
`Customer Deadline: ${display(formatDate(item.CustomerDeadline_c))}`,
|
||||
`Owner: ${[item.Owner?.PersonFirstName, item.Owner?.PersonLastName].filter(Boolean).join(" ") || "N/D"}`,
|
||||
`Owner: ${[item.Owner?.PersonFirstName, item.Owner?.PersonLastName].filter(Boolean).join(" ") || "N/A"}`,
|
||||
`Email: ${display(item.Owner?.PrimaryEmail?.EmailAddress)}`,
|
||||
`Do Not Call: ${display(item.DoNotCallFlag)}`, `Do Not Email: ${display(item.DoNotEmailFlag)}`
|
||||
].join("\n"));
|
||||
|
||||
@@ -81,7 +81,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
|
||||
heading.className = "opportunities-extension-sr-number";
|
||||
const number = item.SrNumber;
|
||||
const link = document.createElement(number ? "a" : "span");
|
||||
link.textContent = number ? String(number) : "N/D";
|
||||
link.textContent = number ? String(number) : "N/A";
|
||||
if (number) {
|
||||
link.href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(String(number))}`;
|
||||
link.target = "_blank";
|
||||
@@ -137,7 +137,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
|
||||
const label = document.createElement("span");
|
||||
label.textContent = "Internal SR Number:";
|
||||
const link = document.createElement(record.SrNumber ? "a" : "span");
|
||||
link.textContent = record.SrNumber ? String(record.SrNumber) : "N/D";
|
||||
link.textContent = record.SrNumber ? String(record.SrNumber) : "N/A";
|
||||
if (record.SrNumber) {
|
||||
link.href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(String(record.SrNumber))}&srId=${encodeURIComponent(String(record.SrId ?? ""))}`;
|
||||
link.target = "_blank";
|
||||
@@ -149,7 +149,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
|
||||
body.className = "opportunities-extension-sr-details";
|
||||
body.textContent = ["Title", "OpportunityName_c", "StatusCd", "ServiceNew_c", "AccountPartyName",
|
||||
"LastUpdateDate", "SvcHealth_c", "AssigneePersonName", "OpenDate"].map(key =>
|
||||
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(key === "LastUpdateDate" || key === "OpenDate" ? formatDate(record[key]) : record[key])}`).join("\n");
|
||||
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/A" : String(key === "LastUpdateDate" || key === "OpenDate" ? formatDate(record[key]) : record[key])}`).join("\n");
|
||||
child.append(row, body);
|
||||
children.append(child);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ function createOpportunitiesDashboard({
|
||||
totalValue,
|
||||
"TOTAL",
|
||||
items.length,
|
||||
(amount) => `${Math.round(amount).toLocaleString()}/${totalCount.toLocaleString()}`
|
||||
(amount) => `${Math.round(amount).toLocaleString("en-US")}/${totalCount.toLocaleString("en-US")}`
|
||||
);
|
||||
totalCard.append(totalLabel, totalValue);
|
||||
dashboard.append(totalCard);
|
||||
@@ -81,7 +81,7 @@ function createOpportunitiesDashboard({
|
||||
}
|
||||
}
|
||||
|
||||
return (amount) => amount.toLocaleString(undefined, {
|
||||
return (amount) => amount.toLocaleString("en-US", {
|
||||
maximumFractionDigits: 0
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ function createOpportunitiesFormatters({
|
||||
}
|
||||
|
||||
if (!currency) {
|
||||
return number.toLocaleString();
|
||||
return number.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -153,7 +153,7 @@ function createOpportunitiesFormatters({
|
||||
currency
|
||||
}).format(number);
|
||||
} catch (error) {
|
||||
return `${currency} ${number.toLocaleString()}`;
|
||||
return `${currency} ${number.toLocaleString("en-US")}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function createInternalSrForm({ MODAL_ID, searchableSelect, formatDate, getOppor
|
||||
const context = node("dl", "opportunities-extension-action-form-context");
|
||||
const opportunity = getOpportunity(parent.OpportunityName_Id_c?.OptyNumber);
|
||||
for (const [label, value] of [["SR Number", parent.SrNumber], ["Opportunity", opportunity?.Name], ["Title", parent.Title]]) {
|
||||
context.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "N/D" : String(value)));
|
||||
context.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "N/A" : String(value)));
|
||||
}
|
||||
heading.append(title, context);
|
||||
header.append(heading);
|
||||
@@ -60,7 +60,7 @@ function createInternalSrForm({ MODAL_ID, searchableSelect, formatDate, getOppor
|
||||
const titleInput = field("title", "Title", "text", true).input;
|
||||
const deadline = field("customerDeadline", "Customer Deadline", "text").input;
|
||||
deadline.readOnly = true;
|
||||
deadline.value = formatDate(parent.CustomerDeadline_c) || "N/D";
|
||||
deadline.value = formatDate(parent.CustomerDeadline_c) || "N/A";
|
||||
field("internalDeadline", "Internal Deadline", "date");
|
||||
const platform = field("platform", "Platform", "select", true);
|
||||
field("description", "Description", "textarea", true);
|
||||
|
||||
@@ -412,6 +412,14 @@
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.opportunities-extension-table-skeleton-row { height: 76px; }
|
||||
.opportunities-extension-table-skeleton-text { width: 85%; max-width: 100%; }
|
||||
.opportunities-extension-table-skeleton-row:nth-of-type(even) .opportunities-extension-table-skeleton-text { width: 70%; }
|
||||
.opportunities-extension-table-skeleton-badge { flex: 0 1 30px; width: 30px; min-width: 0; height: 16px; }
|
||||
.opportunities-extension-table-skeleton-row > td[data-column="customer"] .opportunities-extension-table-skeleton-badge { flex-basis: 18px; width: 18px; }
|
||||
.opportunities-extension-table-skeleton-row > td[data-column="status"] .opportunities-extension-table-skeleton-text,
|
||||
.opportunities-extension-table-skeleton-row > td[data-column="stage"] .opportunities-extension-table-skeleton-text { height: 20px; max-width: 66px; border-radius: 8px; }
|
||||
|
||||
/* Fixed table layout shares leftover width only between the two auto columns. */
|
||||
.opportunities-extension-table > thead > tr > th[data-column="name"] { width: auto; }
|
||||
.opportunities-extension-table > thead > tr > th[data-column="customer"] { width: auto; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
function createOpportunitiesTable({
|
||||
tableSorting,
|
||||
ACCOUNT_DETAIL_URL,
|
||||
OPPORTUNITIES_COLUMNS,
|
||||
OPPORTUNITIES_TABLE_ID,
|
||||
@@ -41,8 +42,8 @@ function createOpportunitiesTable({
|
||||
opportunitiesTableState.opportunitiesTableState = {
|
||||
...opportunitiesTableState.opportunitiesTableState,
|
||||
...nextState,
|
||||
sortKey: nextState.items ? "" : opportunitiesTableState.opportunitiesTableState.sortKey,
|
||||
sortDirection: nextState.items ? "ascending" : opportunitiesTableState.opportunitiesTableState.sortDirection
|
||||
sortKey: tableSorting.read("overview", OPPORTUNITIES_COLUMNS).key,
|
||||
sortDirection: tableSorting.read("overview", OPPORTUNITIES_COLUMNS).direction
|
||||
};
|
||||
renderOpportunitiesTable();
|
||||
renderCustomerFilter();
|
||||
@@ -64,7 +65,7 @@ function createOpportunitiesTable({
|
||||
|
||||
const caption = document.createElement("caption");
|
||||
caption.className = "opportunities-extension-visually-hidden";
|
||||
caption.textContent = "Opportunities";
|
||||
caption.textContent = opportunitiesTableState.opportunitiesTableState.status === "loading" ? "Loading opportunities..." : "Opportunities";
|
||||
|
||||
const tableHead = document.createElement("thead");
|
||||
const headerRow = document.createElement("tr");
|
||||
@@ -85,6 +86,7 @@ function createOpportunitiesTable({
|
||||
|
||||
const sortButton = document.createElement("button");
|
||||
sortButton.type = "button";
|
||||
sortButton.disabled = opportunitiesTableState.opportunitiesTableState.status === "loading";
|
||||
sortButton.className = "opportunities-extension-sort-button";
|
||||
sortButton.setAttribute("data-sort-direction", isSorted ? opportunitiesTableState.opportunitiesTableState.sortDirection : "none");
|
||||
sortButton.setAttribute("aria-label", `Sort by ${column.label}${isSorted ? `, ${opportunitiesTableState.opportunitiesTableState.sortDirection}` : ""}`);
|
||||
@@ -106,7 +108,32 @@ function createOpportunitiesTable({
|
||||
renderStageDashboard(items, opportunitiesTableState.opportunitiesTableState.items.length);
|
||||
renderCalendar?.(items, opportunitiesTableState.opportunitiesTableState);
|
||||
|
||||
if (opportunitiesTableState.opportunitiesTableState.status === "loading" || opportunitiesTableState.opportunitiesTableState.status === "error" || opportunitiesTableState.opportunitiesTableState.status === "empty" || (opportunitiesTableState.opportunitiesTableState.status === "ready" && items.length === 0)) {
|
||||
if (opportunitiesTableState.opportunitiesTableState.status === "loading") {
|
||||
for (let index = 0; index < 8; index++) {
|
||||
const row = document.createElement("tr");
|
||||
row.className = "opportunities-extension-table-skeleton-row";
|
||||
row.setAttribute("aria-hidden", "true");
|
||||
OPPORTUNITIES_COLUMNS.forEach(column => {
|
||||
const cell = document.createElement("td");
|
||||
cell.setAttribute("data-column", column.key);
|
||||
const bar = document.createElement("span");
|
||||
bar.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-table-skeleton-text";
|
||||
cell.append(bar);
|
||||
if (column.key === "name" || column.key === "customer") {
|
||||
const actions = document.createElement("span");
|
||||
actions.className = "opportunities-extension-cell-actions";
|
||||
for (let action = 0; action < (column.key === "name" ? 4 : 3); action++) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-table-skeleton-badge";
|
||||
actions.append(badge);
|
||||
}
|
||||
cell.append(actions);
|
||||
}
|
||||
row.append(cell);
|
||||
});
|
||||
tableBody.append(row);
|
||||
}
|
||||
} else if (opportunitiesTableState.opportunitiesTableState.status === "error" || opportunitiesTableState.opportunitiesTableState.status === "empty" || (opportunitiesTableState.opportunitiesTableState.status === "ready" && items.length === 0)) {
|
||||
const row = document.createElement("tr");
|
||||
const cell = document.createElement("td");
|
||||
cell.className = "opportunities-extension-table-status";
|
||||
@@ -193,11 +220,13 @@ function createOpportunitiesTable({
|
||||
}
|
||||
|
||||
function sortOpportunitiesBy(key) {
|
||||
if (!OPPORTUNITIES_COLUMNS.some(column => column.key === key && column.sortable !== false)) return;
|
||||
const isSameColumn = opportunitiesTableState.opportunitiesTableState.sortKey === key;
|
||||
opportunitiesTableState.opportunitiesTableState.sortKey = key;
|
||||
opportunitiesTableState.opportunitiesTableState.sortDirection = isSameColumn && opportunitiesTableState.opportunitiesTableState.sortDirection === "ascending"
|
||||
? "descending"
|
||||
: "ascending";
|
||||
tableSorting.save("overview", { key, direction: opportunitiesTableState.opportunitiesTableState.sortDirection });
|
||||
renderOpportunitiesTable();
|
||||
}
|
||||
|
||||
|
||||
@@ -155,9 +155,9 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
|
||||
badge.setAttribute("data-state", state);
|
||||
setBadgeContent(badge, "actions", state === "matched" || state === "missing" ? actions.length : state === "loading" ? "..." : "?");
|
||||
badge.title = actions.length ? actions.map(action => [
|
||||
`Action: ${action.name ?? "N/D"}`, `ID: ${action.id ?? "N/D"}`, `Status: ${action.status ?? "N/D"}`,
|
||||
`Owner: ${action.owner || "N/D"}`, `Email: ${action.ownerEmail || "N/D"}`, `Workload: ${action.workloadName || "N/D"}`,
|
||||
`Description: ${action.workloadDescription || "N/D"}`, `Start: ${action.startDate || "N/D"}`, `End: ${action.endDate || "N/D"}`
|
||||
`Action: ${action.name ?? "N/A"}`, `ID: ${action.id ?? "N/A"}`, `Status: ${action.status ?? "N/A"}`,
|
||||
`Owner: ${action.owner || "N/A"}`, `Email: ${action.ownerEmail || "N/A"}`, `Workload: ${action.workloadName || "N/A"}`,
|
||||
`Description: ${action.workloadDescription || "N/A"}`, `Start: ${action.startDate || "N/A"}`, `End: ${action.endDate || "N/A"}`
|
||||
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
||||
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
||||
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
||||
|
||||
75
src/features/preferences/table-sorting.js
Normal file
75
src/features/preferences/table-sorting.js
Normal file
@@ -0,0 +1,75 @@
|
||||
function createTableSorting({ environment }) {
|
||||
const memory = new Map();
|
||||
const storageKey = id => `opportunities-extension-table-sort-v1-${id}`;
|
||||
function read(id, columns) {
|
||||
let saved = memory.get(id);
|
||||
if (!saved) {
|
||||
try { saved = JSON.parse(environment.localStorage?.getItem(storageKey(id)) || "null"); } catch { /* Storage is optional. */ }
|
||||
}
|
||||
return saved && columns.some(column => column.key === saved.key && column.sortable !== false)
|
||||
&& ["ascending", "descending"].includes(saved.direction)
|
||||
? { key: saved.key, direction: saved.direction } : { key: "", direction: "ascending" };
|
||||
}
|
||||
function save(id, state) {
|
||||
memory.set(id, { ...state });
|
||||
try { environment.localStorage?.setItem(storageKey(id), JSON.stringify(state)); } catch { /* Retain the page-session preference. */ }
|
||||
}
|
||||
function value(raw, type) {
|
||||
if (raw == null || String(raw).trim() === "") return null;
|
||||
if (type === "number") return Number.isFinite(Number(raw)) ? Number(raw) : null;
|
||||
if (type === "date") {
|
||||
const timestamp = Date.parse(raw);
|
||||
return Number.isFinite(timestamp) ? timestamp : null;
|
||||
}
|
||||
return String(raw);
|
||||
}
|
||||
function create(id, columns) {
|
||||
let state;
|
||||
const headers = new Map();
|
||||
const get = () => state || (state = read(id, columns));
|
||||
function toggle(key) {
|
||||
if (!columns.some(column => column.key === key && column.sortable !== false)) return;
|
||||
const previous = get();
|
||||
state = { key, direction: previous.key === key && previous.direction === "ascending" ? "descending" : "ascending" };
|
||||
save(id, state);
|
||||
headers.forEach(update);
|
||||
}
|
||||
function select(items) {
|
||||
const selected = get(), column = columns.find(column => column.key === selected.key);
|
||||
if (!column) return [...items];
|
||||
const direction = selected.direction === "descending" ? -1 : 1;
|
||||
return items.map((item, index) => ({ item, index, value: value(column.value ? column.value(item) : item[column.key], column.type) }))
|
||||
.sort((a, b) => {
|
||||
if (a.value === null || b.value === null) return a.value === b.value ? a.index - b.index : a.value === null ? 1 : -1;
|
||||
const result = typeof a.value === "number" ? a.value - b.value : a.value.localeCompare(b.value, undefined, { numeric: true, sensitivity: "base" });
|
||||
return result * direction || a.index - b.index;
|
||||
}).map(record => record.item);
|
||||
}
|
||||
function update({ th, button, key, label }) {
|
||||
const selected = get();
|
||||
const direction = selected.key === key ? selected.direction : "none";
|
||||
th.setAttribute("aria-sort", direction);
|
||||
button.setAttribute("data-sort-direction", direction);
|
||||
button.setAttribute("aria-label", `Sort by ${label}${direction === "none" ? "" : `, ${direction}`}`);
|
||||
}
|
||||
function header(th, key, label, onChange, disabled = false) {
|
||||
const button = environment.document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "opportunities-extension-sort-button";
|
||||
button.disabled = disabled;
|
||||
button.title = `Sort by ${label}`;
|
||||
button.setAttribute("data-sort-key", key);
|
||||
const text = environment.document.createElement("span");
|
||||
text.className = "opportunities-extension-sort-label";
|
||||
text.textContent = label;
|
||||
button.append(text);
|
||||
const record = { th, button, key, label };
|
||||
headers.set(key, record); update(record);
|
||||
th.append(button);
|
||||
button.addEventListener("click", () => { toggle(key); onChange?.(key); });
|
||||
return button;
|
||||
}
|
||||
return { get, toggle, select, header };
|
||||
}
|
||||
return { read, save, create };
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function createPreferencesView({
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.type = "button";
|
||||
closeButton.className = "opportunities-extension-icon-button";
|
||||
closeButton.setAttribute("aria-label", "Fechar preferências");
|
||||
closeButton.setAttribute("aria-label", "Close preferences");
|
||||
closeButton.title = "Close";
|
||||
closeButton.append(createCloseIcon());
|
||||
closeButton.addEventListener("click", closePreferencesModal);
|
||||
|
||||
@@ -189,7 +189,7 @@ function createProductsContentJobs({
|
||||
timeStyle: "short"
|
||||
}).format(new Date(timestamp));
|
||||
} catch (error) {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
return new Date(timestamp).toLocaleString("en-GB");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ function createProductsDetails({
|
||||
function createProductsRampBadge(productsResult) {
|
||||
const values = (Array.isArray(productsResult.items) ? productsResult.items : [])
|
||||
.map(item => item && item.RampMonths_c)
|
||||
.map(value => value === null || value === undefined || String(value).trim() === "" ? "N/D" : String(value).trim())
|
||||
.filter((value, index, items) => value !== "N/D" || items.indexOf(value) === index);
|
||||
.map(value => value === null || value === undefined || String(value).trim() === "" ? "N/A" : String(value).trim())
|
||||
.filter((value, index, items) => value !== "N/A" || items.indexOf(value) === index);
|
||||
if (!values.length) return null;
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "opportunities-extension-products-ramp";
|
||||
badge.setAttribute("data-missing", String(values.includes("N/D")));
|
||||
badge.setAttribute("data-missing", String(values.includes("N/A")));
|
||||
setBadgeContent(badge, "ramp", values.join(" | "));
|
||||
badge.title = `Ramp Months: ${values.join(", ")}`;
|
||||
badge.setAttribute("aria-label", badge.title);
|
||||
@@ -114,7 +114,7 @@ function createProductsDetails({
|
||||
{ label: "Close date", value: (item) => formatOracleResponseDate(item.EffectiveDate) || "-" },
|
||||
{ label: "Consumption Start", value: (item) => formatOracleResponseDate(item.ConsumptionStartDate_c) || "-" },
|
||||
{ label: "Ramp Months", value: (item) => item.RampMonths_c === null || item.RampMonths_c === undefined || String(item.RampMonths_c).trim() === ""
|
||||
? "N/D" : formatProductNumber(item.RampMonths_c) },
|
||||
? "N/A" : formatProductNumber(item.RampMonths_c) },
|
||||
{ label: "Projection", render: (item) => createProjectionIndicator(item, opportunity) }
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCloseIcon, expandProducts, saveEstimates, notify, formatAmount, formatDate, environment }) {
|
||||
function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCloseIcon, expandProducts, saveEstimates, notify, formatAmount, formatDate, environment, tableSorting }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
function node(tag, className, text) {
|
||||
@@ -96,8 +96,15 @@ function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCl
|
||||
const table = node("table", "opportunities-extension-projection-table");
|
||||
const caption = node("caption", "opportunities-extension-visually-hidden", `Monthly consumption comparison (${currency}). Difference is forecast minus calculated.`);
|
||||
const head = node("thead"), headRow = node("tr"), tbody = node("tbody");
|
||||
for (const label of ["Month", "Forecast", "Calculated", "Difference"]) {
|
||||
const th = node("th", "", label); th.scope = "col"; headRow.append(th);
|
||||
const sorting = tableSorting.create("consumption-projection", [
|
||||
{ key: "month", type: "number", value: item => item.year * 12 + item.month },
|
||||
...["actual", "calculated", "difference"].map(key => ({ key, type: "number" }))
|
||||
]);
|
||||
const rowNodes = new Map();
|
||||
const renderRows = () => tbody.replaceChildren(...sorting.select(result.rows).map(item => rowNodes.get(item)));
|
||||
for (const [key, label] of [["month", "Month"], ["actual", "Forecast"], ["calculated", "Calculated"], ["difference", "Difference"]]) {
|
||||
const th = node("th"); th.scope = "col";
|
||||
sorting.header(th, key, label, renderRows); headRow.append(th);
|
||||
}
|
||||
head.append(headRow);
|
||||
for (const item of result.rows) {
|
||||
@@ -108,8 +115,9 @@ function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCl
|
||||
if (item.prorated) month.append(node("span", "opportunities-extension-projection-prorated", " (Prorated)"));
|
||||
tr.append(month);
|
||||
for (const value of [item.actual, item.calculated, item.difference]) tr.append(node("td", "", value === null ? "-" : formatAmount(value, currency)));
|
||||
tbody.append(tr);
|
||||
rowNodes.set(item, tr);
|
||||
}
|
||||
renderRows();
|
||||
table.append(caption, head, tbody);
|
||||
viewport.append(table);
|
||||
body.append(viewport);
|
||||
@@ -203,7 +211,7 @@ function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCl
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
||||
if (event.key === "Tab") {
|
||||
const controls = [dismiss, viewport, updateButton].filter(control => !control.disabled);
|
||||
const controls = [dismiss, viewport, ...table.querySelectorAll("button"), updateButton].filter(control => !control.disabled);
|
||||
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();
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
.opportunities-extension-projection-table th, .opportunities-extension-projection-table td { padding: 9px 12px; border-bottom: 1px solid #dedbd7; text-align: right; white-space: nowrap; }
|
||||
.opportunities-extension-projection-table th:first-child { text-align: left; }
|
||||
.opportunities-extension-projection-table thead th { background: #f5f4f2; font-weight: 600; }
|
||||
.opportunities-extension-projection-table .opportunities-extension-sort-button { min-height: 28px; padding: 4px 18px 4px 0; justify-content: flex-end; }
|
||||
.opportunities-extension-projection-table th:first-child .opportunities-extension-sort-button { justify-content: flex-start; }
|
||||
.opportunities-extension-projection-table .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }
|
||||
.opportunities-extension-projection-table tbody th { font-weight: 400; }
|
||||
.opportunities-extension-projection-table tr[data-divergent="true"] { background: #fff7e5; }
|
||||
.opportunities-extension-projection-table tr[data-divergent="true"] td:last-child { color: #815400; font-weight: 700; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, selectItems, createConsumptionHeader, createPlanDetails, environment }) {
|
||||
function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, selectItems, createConsumptionHeader, createPlanDetails, tableSorting, environment }) {
|
||||
const { document, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame } = environment;
|
||||
let active = null;
|
||||
let closing = null;
|
||||
@@ -78,6 +78,9 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
const headRow = element("tr");
|
||||
const sortHeaders = [];
|
||||
const current = { root, background, wasInert: background.inert, trigger, items: [], status: "loading", sortKey: "", direction: "asc", openFrame: null };
|
||||
const savedSort = tableSorting.read("rate-card", columns);
|
||||
current.sortKey = savedSort.key;
|
||||
current.direction = savedSort.direction === "descending" ? "desc" : "asc";
|
||||
active = current;
|
||||
background.inert = true;
|
||||
const alive = () => active === current && root.isConnected && !parent.hidden;
|
||||
@@ -98,6 +101,7 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
sort.addEventListener("click", () => {
|
||||
current.direction = current.sortKey === column.key && current.direction === "asc" ? "desc" : "asc";
|
||||
current.sortKey = column.key;
|
||||
tableSorting.save("rate-card", { key: current.sortKey, direction: current.direction === "desc" ? "descending" : "ascending" });
|
||||
updateSortHeaders();
|
||||
if (current.status === "ready") renderRows();
|
||||
});
|
||||
@@ -106,6 +110,7 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
headRow.append(th);
|
||||
});
|
||||
head.append(headRow);
|
||||
updateSortHeaders();
|
||||
const body = element("tbody");
|
||||
table.append(caption, colgroup, head, body);
|
||||
viewport.append(table);
|
||||
|
||||
@@ -38,3 +38,5 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.opportunities-extension-service-requests-table .opportunities-extension-account-plans-skeleton-bar { animation: none; }
|
||||
}
|
||||
.opportunities-extension-service-requests-table .opportunities-extension-sort-button { min-height: 28px; padding: 4px 16px 4px 0; overflow-wrap: anywhere; }
|
||||
.opportunities-extension-service-requests-table .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) {
|
||||
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, tableSorting, environment }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
const columns = [
|
||||
@@ -31,7 +31,8 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
||||
const text = value === null || value === undefined || value === "" ? "-" : String(value);
|
||||
return text !== "-" && (key === "created" || key === "lastUpdated") ? formatOracleResponseDate(value) : text;
|
||||
}
|
||||
function createTable(items, loading = false) {
|
||||
const sortColumns = columns.map(([key, , value]) => ({ key, value, type: ["created", "lastUpdated"].includes(key) ? "date" : "text" }));
|
||||
function createTable(items, loading = false, sorting = tableSorting.create("service-requests", sortColumns), onSort) {
|
||||
const table = element("table", "opportunities-extension-service-requests-table");
|
||||
table.setAttribute("aria-label", "Customer service requests");
|
||||
const group = element("colgroup");
|
||||
@@ -39,13 +40,14 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
||||
const header = element("tr");
|
||||
columns.forEach(([key, label]) => {
|
||||
group.append(element("col", `service-requests-col-${key}`));
|
||||
const th = element("th", "", label);
|
||||
const th = element("th");
|
||||
th.scope = "col";
|
||||
sorting.header(th, key, label, onSort, loading);
|
||||
header.append(th);
|
||||
});
|
||||
head.append(header);
|
||||
const body = element("tbody");
|
||||
(loading ? Array.from({ length: 8 }, () => ({})) : items).forEach(item => {
|
||||
(loading ? Array.from({ length: 8 }, () => ({})) : sorting.select(items)).forEach(item => {
|
||||
const row = element("tr");
|
||||
columns.forEach(([key, , getValue]) => {
|
||||
const cell = element("td");
|
||||
@@ -99,6 +101,7 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
||||
const status = element("p", "opportunities-extension-service-requests-message", "Loading service requests...");
|
||||
status.setAttribute("role", "status");
|
||||
let loadedItems = [], loaded = false, failure = "";
|
||||
const sorting = tableSorting.create("service-requests", sortColumns);
|
||||
const search = createSearch({ label: "Search service requests", onSearch: renderResults,
|
||||
getValues: item => columns.flatMap(([key, , getValue]) => [getValue(item), formatValue(getValue(item), key)]) });
|
||||
const toolbar = element("div", "opportunities-extension-customer-table-toolbar");
|
||||
@@ -111,7 +114,7 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
const controls = [close, search.input, content];
|
||||
const controls = [close, search.input, content, ...content.querySelectorAll("button")].filter(control => !control.disabled);
|
||||
const index = controls.indexOf(document.activeElement);
|
||||
controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length].focus();
|
||||
}
|
||||
@@ -126,7 +129,10 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
||||
: "No open service requests found for this customer.");
|
||||
status.setAttribute("role", failure ? "alert" : "status");
|
||||
content.append(status);
|
||||
if (!failure && items.length) content.append(createTable(items));
|
||||
if (!failure && items.length) content.append(createTable(items, false, sorting, key => {
|
||||
renderResults();
|
||||
content.querySelector(`[data-sort-key="${key}"]`)?.focus();
|
||||
}));
|
||||
content.scrollTop = 0;
|
||||
}
|
||||
return { render(items, error = "") {
|
||||
|
||||
Reference in New Issue
Block a user