Implemented the Create Internal Service Request functionality
This commit is contained in:
197
src/features/opportunities/internal-sr-form.js
Normal file
197
src/features/opportunities/internal-sr-form.js
Normal file
@@ -0,0 +1,197 @@
|
||||
function createInternalSrForm({ MODAL_ID, searchableSelect, formatDate, getOpportunity, requestPlatform, requestServices, requestSave, notify, environment }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
function node(tag, className, text) {
|
||||
const el = document.createElement(tag);
|
||||
if (className) el.className = className;
|
||||
if (text !== undefined) el.textContent = text;
|
||||
return el;
|
||||
}
|
||||
function close(restoreFocus = true) {
|
||||
if (!active) return false;
|
||||
if (active.saving) return true;
|
||||
const previous = active;
|
||||
active = null;
|
||||
previous.modal.remove();
|
||||
if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus();
|
||||
return true;
|
||||
}
|
||||
function open(parent, trigger) {
|
||||
if (active?.saving) return;
|
||||
const host = document.getElementById(MODAL_ID);
|
||||
if (!host || host.hidden) return;
|
||||
close(false);
|
||||
const modal = node("section", "opportunities-extension-action-form-modal");
|
||||
modal.setAttribute("role", "dialog");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
modal.setAttribute("aria-labelledby", "internal-sr-form-title");
|
||||
const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-internal-sr-dialog");
|
||||
const header = node("header", "opportunities-extension-account-plans-header");
|
||||
const heading = node("div", "opportunities-extension-action-form-heading");
|
||||
const title = node("h2", "", "Create Internal Service Request");
|
||||
title.id = "internal-sr-form-title";
|
||||
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)));
|
||||
}
|
||||
heading.append(title, context);
|
||||
header.append(heading);
|
||||
const form = node("form", "opportunities-extension-action-form");
|
||||
form.noValidate = true;
|
||||
const fields = new Map();
|
||||
const combos = new Map();
|
||||
const ready = new Set();
|
||||
function field(name, label, type, wide = false) {
|
||||
const wrapper = node("label", "opportunities-extension-action-form-field" + (wide ? " opportunities-extension-action-form-wide" : ""));
|
||||
const input = node(type === "textarea" || type === "select" ? type : "input");
|
||||
if (!["textarea", "select"].includes(type)) input.type = type;
|
||||
if (type === "textarea") input.rows = 4;
|
||||
input.id = `internal-sr-${name}`;
|
||||
input.name = name;
|
||||
fields.set(name, input);
|
||||
input.required = name !== "customerDeadline";
|
||||
input.setAttribute("aria-required", String(input.required));
|
||||
wrapper.htmlFor = input.id;
|
||||
wrapper.append(node("span", "", input.required ? `${label} (required)` : label), input);
|
||||
form.append(wrapper);
|
||||
return { input, wrapper };
|
||||
}
|
||||
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";
|
||||
field("internalDeadline", "Internal Deadline", "date");
|
||||
const platform = field("platform", "Platform", "select", true);
|
||||
field("description", "Description", "textarea", true);
|
||||
const services = field("services", "Services", "select", true);
|
||||
const footer = node("footer", "opportunities-extension-action-form-footer");
|
||||
const cancel = node("button", "", "Cancel");
|
||||
const save = node("button", "opportunities-extension-action-form-save", "Save");
|
||||
save.type = "submit";
|
||||
save.disabled = true;
|
||||
const feedback = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide");
|
||||
feedback.setAttribute("role", "status");
|
||||
feedback.hidden = true;
|
||||
cancel.type = "button";
|
||||
cancel.addEventListener("click", () => close());
|
||||
footer.append(save, cancel);
|
||||
form.append(feedback, footer);
|
||||
dialog.append(header, form);
|
||||
modal.append(dialog);
|
||||
host.append(modal);
|
||||
const entry = { modal, trigger, saving: false };
|
||||
active = entry;
|
||||
const validDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`))
|
||||
&& new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (active !== entry || entry.saving || ready.size !== 2) return;
|
||||
let firstInvalid = null;
|
||||
for (const [name, input] of fields) {
|
||||
if (!input.required) continue;
|
||||
const value = String(input.value || "").trim();
|
||||
const valid = !!value && (name !== "internalDeadline" || validDate(value))
|
||||
&& (!combos.has(name) || Array.from(input.children).some(option => option.value === value));
|
||||
input.setAttribute("aria-invalid", String(!valid));
|
||||
const visible = combos.get(name)?.input || input;
|
||||
visible.setAttribute("aria-invalid", String(!valid));
|
||||
if (!valid && !firstInvalid) firstInvalid = visible;
|
||||
}
|
||||
feedback.hidden = false;
|
||||
if (firstInvalid) {
|
||||
feedback.setAttribute("role", "alert");
|
||||
feedback.textContent = "Complete all required fields with valid values.";
|
||||
firstInvalid.focus();
|
||||
return;
|
||||
}
|
||||
const value = name => String(fields.get(name).value || "").trim();
|
||||
const dateParts = value("customerDeadline").match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||
const payload = {
|
||||
Title: value("title"),
|
||||
ServiceNew_Id_c: value("services"),
|
||||
ProblemDescription: value("description"),
|
||||
CustomerDeadline_c: dateParts ? `${dateParts[3]}-${dateParts[2]}-${dateParts[1]}` : "",
|
||||
TechPlatformLookUp_c: value("platform"),
|
||||
ServiceRequest_Id_SRToInternalSR: String(parent.SrId ?? ""),
|
||||
SvcQuestionsCollection_c: [], InternalDeadline_c: value("internalDeadline")
|
||||
};
|
||||
entry.saving = true;
|
||||
form.setAttribute("aria-busy", "true");
|
||||
save.disabled = cancel.disabled = true;
|
||||
save.textContent = "Saving...";
|
||||
for (const [name, input] of fields) { input.disabled = true; combos.get(name)?.sync(); }
|
||||
feedback.setAttribute("role", "status");
|
||||
feedback.textContent = "Creating Internal Service Request...";
|
||||
try {
|
||||
await requestSave(payload);
|
||||
entry.saving = false;
|
||||
close();
|
||||
notify("Internal Service Request created successfully.");
|
||||
} catch (error) {
|
||||
if (active !== entry) return;
|
||||
feedback.setAttribute("role", "alert");
|
||||
feedback.textContent = error.message || "Unable to create Internal Service Request.";
|
||||
notify(feedback.textContent, true);
|
||||
} finally {
|
||||
entry.saving = false;
|
||||
if (active === entry) {
|
||||
form.setAttribute("aria-busy", "false");
|
||||
save.disabled = cancel.disabled = false;
|
||||
save.textContent = "Save";
|
||||
for (const [name, input] of fields) { input.disabled = false; combos.get(name)?.sync(); }
|
||||
}
|
||||
}
|
||||
});
|
||||
function load(target, request, service) {
|
||||
const { input, wrapper } = target;
|
||||
input.disabled = true;
|
||||
const combo = searchableSelect(input, wrapper);
|
||||
combos.set(input.name, combo);
|
||||
const status = node("p", "opportunities-extension-action-form-status", "Loading...");
|
||||
status.setAttribute("role", "status");
|
||||
wrapper.append(status);
|
||||
wrapper.setAttribute("aria-busy", "true");
|
||||
Promise.resolve().then(request).then(items => {
|
||||
if (active !== entry) return;
|
||||
const placeholder = node("option", "", items.length ? "Select an option" : "No options available");
|
||||
placeholder.value = "";
|
||||
input.append(placeholder);
|
||||
const seen = new Set();
|
||||
for (const item of items) {
|
||||
const value = String(service ? item.Id : item.code);
|
||||
if (seen.has(value)) continue;
|
||||
seen.add(value);
|
||||
const option = node("option", "", service ? item.RecordName : item.name);
|
||||
option.value = value;
|
||||
if (service && item.Description_c) option.setAttribute("data-description", item.Description_c);
|
||||
input.append(option);
|
||||
}
|
||||
input.value = "";
|
||||
input.disabled = !items.length;
|
||||
if (items.length) ready.add(input.name);
|
||||
save.disabled = ready.size !== 2;
|
||||
status.textContent = items.length ? "" : "No options available.";
|
||||
status.hidden = !!items.length;
|
||||
combo.sync();
|
||||
}).catch(error => {
|
||||
if (active !== entry) return;
|
||||
status.setAttribute("role", "alert");
|
||||
status.textContent = error.message || "Unable to load options. Close and reopen the form to retry.";
|
||||
}).finally(() => { if (active === entry) wrapper.setAttribute("aria-busy", "false"); });
|
||||
}
|
||||
load(platform, requestPlatform, false);
|
||||
load(services, () => requestServices(parent.SrId), true);
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
||||
if (event.key === "Tab") {
|
||||
const focusable = Array.from(form.querySelectorAll("input, textarea, button")).filter(el => !el.disabled && !el.hidden);
|
||||
const first = focusable[0], last = focusable.at(-1);
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
}
|
||||
});
|
||||
titleInput.focus();
|
||||
}
|
||||
return { open, close };
|
||||
}
|
||||
Reference in New Issue
Block a user