Table sorting preferences will be saved and reused based on the user’s selections

This commit is contained in:
2026-10-25 00:39:55 -03:00
parent 19edee4e11
commit 0f1f399d1a
40 changed files with 896 additions and 230 deletions

View File

@@ -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);

View File

@@ -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"));

View File

@@ -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);
}

View File

@@ -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
});
}

View File

@@ -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")}`;
}
}

View File

@@ -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);

View File

@@ -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; }

View File

@@ -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();
}

View File

@@ -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.";