Fully implemented the Manage Time functionality
This commit is contained in:
@@ -15,7 +15,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function observeHeaders(rawUrl, headers) {
|
||||
let url;
|
||||
try { url = new URL(rawUrl); } catch { return; }
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management|oalset_timeentrymobile)\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
// Read only the client ID, not cookies, bodies or authorization.
|
||||
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
|
||||
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
|
||||
@@ -28,7 +28,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function start() {
|
||||
if (started || !runtimeApi?.webRequest) return;
|
||||
started = true;
|
||||
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
|
||||
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`, `${origin}/ic/builder/rt/oalset_timeentrymobile/*`], types: ["xmlhttprequest"] };
|
||||
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||
}, filter, ["requestHeaders"]);
|
||||
@@ -105,5 +105,27 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
|
||||
async function timeEntriesHeaders(url) {
|
||||
const target = new URL(url);
|
||||
const path = "/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries";
|
||||
const isDelete = target.pathname.startsWith(`${path}/`) && /^[1-9]\d*$/.test(target.pathname.slice(path.length + 1));
|
||||
if (target.origin !== origin || (target.pathname !== path && !isDelete) || target.search || target.hash) {
|
||||
throw new Error("Unexpected time entries URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
throw new Error("Reload the extension to enable the time entry header permissions.");
|
||||
}
|
||||
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [isDelete ? 7205 : 7204],
|
||||
addRules: [{
|
||||
id: isDelete ? 7205 : 7204, priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: target.origin }] },
|
||||
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [new URL(runtimeApi.runtime.getURL("/")).hostname],
|
||||
requestMethods: isDelete ? ["delete"] : ["post", "put"], resourceTypes: ["xmlhttprequest"] }
|
||||
}]
|
||||
});
|
||||
return sessionHeaders(target.origin, "application/json");
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, timeEntriesHeaders, observeResponseHeaders: observeHeaders };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ function createBackgroundApplication(overrides = {}) {
|
||||
...overrides
|
||||
};
|
||||
const messageTypes = createMessageTypes();
|
||||
const timeEntries = createTimeEntries({ dates: createTimeDates(), fetchJson: (...args) => httpBackgroundJson.fetchJson(...args), getResourceUser: message => fusionSrCreate.getResourceUser(message) });
|
||||
const timeEntries = createTimeEntries({ dates: createTimeDates(), fetchJson: (...args) => httpBackgroundJson.fetchJson(...args), getResourceUser: message => fusionSrCreate.getResourceUser(message), saveHeaders: url => srRequestHeaders.timeEntriesHeaders(url) });
|
||||
const spaCurrentUser = createSpaCurrentUser({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
|
||||
const requestCurrentUser = () => spaCurrentUser.requestCurrentUser();
|
||||
const contracts = createBackgroundContracts();
|
||||
@@ -148,6 +148,9 @@ function createBackgroundApplication(overrides = {}) {
|
||||
startBackgroundApplication({
|
||||
invalidateResourceUser: () => fusionSrCreate.invalidateResourceUser(),
|
||||
requestWeekDetails: message => timeEntries.requestWeekDetails(message),
|
||||
requestTimeEntries: message => timeEntries.requestTimeEntries(message),
|
||||
requestTimeEntriesSave: message => timeEntries.requestTimeEntriesSave(message),
|
||||
requestTimeEntriesDelete: message => timeEntries.requestTimeEntriesDelete(message),
|
||||
requestLookupTaskType: () => timeEntries.requestLookupTaskType(),
|
||||
requestServiceRequestExpand: message => timeEntries.requestServiceRequestExpand(message),
|
||||
requestResourceUser: message => fusionSrCreate.getResourceUser(message),
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
function registerBackgroundMessages({
|
||||
messageTypes,
|
||||
requestWeekDetails,
|
||||
requestTimeEntries,
|
||||
requestTimeEntriesSave,
|
||||
requestTimeEntriesDelete,
|
||||
requestLookupTaskType,
|
||||
requestServiceRequestExpand,
|
||||
requestResourceUser,
|
||||
@@ -45,8 +48,18 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
error: error.message, httpStatus: error.httpStatus, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
}
|
||||
if ([messageTypes.requestServiceRequestExpand, messageTypes.requestWeekDetails, messageTypes.requestResourceUser, messageTypes.requestActiveServiceRequests].includes(message.type)) {
|
||||
const request = message.type === messageTypes.requestServiceRequestExpand ? requestServiceRequestExpand : message.type === messageTypes.requestWeekDetails ? requestWeekDetails : message.type === messageTypes.requestResourceUser ? requestResourceUser : requestActiveServiceRequests;
|
||||
if (message.type === messageTypes.requestTimeEntriesSave) {
|
||||
requestTimeEntriesSave(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, httpStatus: error.httpStatus, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.requestTimeEntriesDelete) {
|
||||
requestTimeEntriesDelete(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, httpStatus: error.httpStatus, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
}
|
||||
if ([messageTypes.requestTimeEntries, messageTypes.requestServiceRequestExpand, messageTypes.requestWeekDetails, messageTypes.requestResourceUser, messageTypes.requestActiveServiceRequests].includes(message.type)) {
|
||||
const request = message.type === messageTypes.requestTimeEntries ? requestTimeEntries : message.type === messageTypes.requestServiceRequestExpand ? requestServiceRequestExpand : message.type === messageTypes.requestWeekDetails ? requestWeekDetails : message.type === messageTypes.requestResourceUser ? requestResourceUser : requestActiveServiceRequests;
|
||||
request(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, httpStatus: error.httpStatus, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
|
||||
@@ -137,6 +137,30 @@ function createContentApplication(overrides = {}) {
|
||||
});
|
||||
const svgPaths = createSvgPaths({ environment });
|
||||
const manageTime = createManageTime({ MODAL_ID: contracts.MODAL_ID, environment, searchableSelect, dates: createTimeDates(),
|
||||
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||
async requestDelete(id) {
|
||||
await srSessionPreparation.ensure();
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestTimeEntriesDelete, id });
|
||||
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to delete time entry.");
|
||||
return result.result;
|
||||
},
|
||||
async requestSave(method, items) {
|
||||
await srSessionPreparation.ensure();
|
||||
await currentUser.ensure();
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestTimeEntriesSave, method, items, emailAddress: currentUser.getUserId() });
|
||||
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to save time entries.");
|
||||
return result.result;
|
||||
},
|
||||
async requestEntries(weekId) {
|
||||
await srSessionPreparation.ensure();
|
||||
await currentUser.ensure();
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestTimeEntries, weekId, emailAddress: currentUser.getUserId() });
|
||||
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to load time entries.");
|
||||
return result.items;
|
||||
},
|
||||
async requestTaskTypes() {
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestLookupTaskType });
|
||||
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
.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; }
|
||||
.oe-time-grid td { padding: 10px 5px; border-bottom: 1px solid var(--time-border); vertical-align: middle; text-align: center; }
|
||||
.oe-time-duplicate-row { outline: 2px solid #b3261e; outline-offset: -2px; animation: oe-time-duplicate-fade 5.5s linear forwards; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .oe-time-duplicate-row { --time-duplicate-color: #ffaaa4; }
|
||||
@keyframes oe-time-duplicate-fade { 0%, 90.909% { outline-color: var(--time-duplicate-color, #b3261e); } 100% { outline-color: transparent; } }
|
||||
@media (prefers-reduced-motion: reduce) { .oe-time-duplicate-row { animation-timing-function: step-end; } }
|
||||
.oe-time-grid select { width: 100%; }
|
||||
.oe-time-grid input[type="number"] { width: min(100%, 60px); padding: 6px 3px; text-align: center; }
|
||||
.oe-time-grid .opportunities-extension-action-combobox input { width: 100%; text-align: left; text-overflow: ellipsis; }
|
||||
@@ -45,6 +49,14 @@
|
||||
.oe-time-footer { flex-wrap: wrap; justify-content: flex-start; margin-top: 18px; }
|
||||
.oe-time-footer button { padding: 7px 14px; }
|
||||
.oe-time-status { padding: 12px 0; }
|
||||
.oe-time-loading-status { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
.oe-time-skeleton { display: inline-block; vertical-align: middle; border-radius: 4px; background: var(--time-border); color: transparent; animation: oe-time-pulse 1.3s ease-in-out infinite alternate; }
|
||||
.oe-time-skeleton-select { display: block; width: 100%; height: 38px; }
|
||||
.oe-time-skeleton-control { width: min(100%, 60px); height: 38px; }
|
||||
.oe-time-skeleton-heading { width: 65%; height: 20px; margin: 17px 0; }
|
||||
.oe-time-skeleton-summary { min-width: 44px; height: 20px; }
|
||||
@keyframes oe-time-pulse { from { opacity: .4; } to { opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .oe-time-skeleton { animation: none; } }
|
||||
.oe-time-status[role="alert"] { color: #a12d27; }
|
||||
.oe-time-dialog [aria-invalid="true"] { border-color: #b3261e; outline: 1px solid #b3261e; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .oe-time-status[role="alert"] { color: #ffd9d5; }
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requestWeek, requestServices = async () => [], requestExpand = async () => [], requestTaskTypes = async () => [] }) {
|
||||
function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requestWeek, requestSave, requestDelete, notify = () => {}, requestEntries = async () => [], requestServices = async () => [], requestExpand = async () => [], requestTaskTypes = async () => [] }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
let ready = false;
|
||||
let launcher = null;
|
||||
const referenceDateKey = "opportunities-extension-manage-time-reference-date";
|
||||
let lastReferenceDate = "";
|
||||
function savedReferenceDate() {
|
||||
try {
|
||||
const saved = environment.localStorage?.getItem(referenceDateKey);
|
||||
if (dates.parse(saved)) lastReferenceDate = saved;
|
||||
} catch { /* Keep the in-memory selection when storage is unavailable. */ }
|
||||
return dates.parse(lastReferenceDate) ? lastReferenceDate : dates.today();
|
||||
}
|
||||
function saveReferenceDate(value) {
|
||||
if (!dates.parse(value)) return;
|
||||
lastReferenceDate = value;
|
||||
try { environment.localStorage?.setItem(referenceDateKey, value); }
|
||||
catch { /* Keep the selection for this page even if persistence is blocked. */ }
|
||||
}
|
||||
function setReady(value) {
|
||||
ready = !!value;
|
||||
if (launcher) { launcher.disabled = !ready; launcher.title = ready ? "Manage Time" : "Waiting for SR session ready"; }
|
||||
@@ -54,12 +69,14 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
}
|
||||
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(trigger) {
|
||||
if (active?.saving) return;
|
||||
if (!ready) return;
|
||||
const host = document.getElementById(MODAL_ID);
|
||||
if (!host || host.hidden) return;
|
||||
@@ -76,17 +93,18 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
const body = node("div", "oe-time-body");
|
||||
const toolbar = node("div", "oe-time-toolbar");
|
||||
const navigation = node("div", "oe-time-navigation");
|
||||
const date = node("input"); date.type = "date"; date.value = dates.today(); date.required = true;
|
||||
const date = node("input"); date.type = "date"; date.value = savedReferenceDate(); date.required = true;
|
||||
date.setAttribute("aria-label", "Reference date");
|
||||
const ending = node("div", "oe-time-ending");
|
||||
const endingValue = node("strong", "", "--");
|
||||
ending.append(node("span", "", "Week Ending"), endingValue);
|
||||
const changeWeek = days => { if (dates.parse(date.value)) { date.value = dates.shift(date.value, days); load(); } };
|
||||
const changeDate = () => { saveReferenceDate(date.value); load(); };
|
||||
const changeWeek = days => { if (dates.parse(date.value)) { date.value = dates.shift(date.value, days); changeDate(); } };
|
||||
navigation.append(button("Previous week", "left", () => changeWeek(-7)), date, button("Next week", "right", () => changeWeek(7)));
|
||||
date.addEventListener("change", load); toolbar.append(navigation, ending);
|
||||
date.addEventListener("change", changeDate); toolbar.append(navigation, ending);
|
||||
const summary = node("div", "oe-time-summary"), sums = {};
|
||||
for (const label of ["Service", "Non Service", "Time Off", "Total Hours"]) {
|
||||
const item = node("div"); sums[label] = node("strong", "", "0 hrs");
|
||||
const item = node("div"); sums[label] = node("strong", "", "0h");
|
||||
item.append(node("span", "", `${label}: `), sums[label]); summary.append(item);
|
||||
}
|
||||
const status = node("div", "oe-time-status"); status.setAttribute("role", "status");
|
||||
@@ -100,13 +118,14 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
const footer = node("footer", "opportunities-extension-action-form-footer oe-time-footer");
|
||||
const add = button("Add", "add", () => addRow("Service"), true);
|
||||
const addNonService = button("Add (Non service)", "add", () => addRow("Non Service"), true);
|
||||
const save = button("Save", "save", null, true); save.disabled = true;
|
||||
save.title = "Saving time entries is not available yet.";
|
||||
const save = button("Save", "save", saveEntries, true); save.disabled = true;
|
||||
const saveStatus = node("div", "oe-time-status"); saveStatus.hidden = true; saveStatus.setAttribute("role", "status");
|
||||
footer.append(add, addNonService, save);
|
||||
body.append(toolbar, summary, status, retry, serviceStatus, serviceRetry, taskStatus, taskRetry, scroll, footer);
|
||||
body.append(toolbar, summary, status, retry, serviceStatus, serviceRetry, taskStatus, taskRetry, scroll, saveStatus, footer);
|
||||
dialog.append(header, body); modal.append(dialog); host.append(modal);
|
||||
const entry = { modal, trigger, version: 0, days: [], rows: [], drafts: new Map(), tbody: null, dayTotals: [], services: [], servicesStarted: false, servicesReady: false, expanded: new Map() };
|
||||
active = entry;
|
||||
entry.deletions = new Map();
|
||||
entry.taskTypes = [];
|
||||
entry.taskTypesReady = false;
|
||||
let combos = [];
|
||||
@@ -121,7 +140,88 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
|
||||
});
|
||||
function blankRow(type) { return { type, hours: {}, notes: "", srNumber: "", activity: "", taskType: "", notesOpen: false }; }
|
||||
function blankRow(type) { return { type, hours: {}, invalidHours: {}, notes: "", srNumber: "", activity: "", taskType: "", notesOpen: false }; }
|
||||
const hourFields = ["sundayTaskHours", "mondayTaskHours", "tuesdayTaskHours", "wednesdayTaskHours", "thursdayTaskHours", "fridayTaskHours", "saturdayTaskHours"];
|
||||
function rowPayload(row) {
|
||||
const payload = { srNumber: row.type === "Non Service" ? "non-service-sr" : row.srNumber,
|
||||
activityCode: row.activity, taskType: row.taskType, deliveryType: row.deliveryType || "",
|
||||
notes: row.notes, weekId: entry.days[0]?.WeekId, tag: row.type === "Non Service" ? "NON_SERVICE" : "SERVICE" };
|
||||
for (const day of entry.days) payload[hourFields[dates.fromApi(day.Date).getUTCDay()]] = Number(row.hours[day.Date]) || 0;
|
||||
return payload;
|
||||
}
|
||||
function signature(row) { return JSON.stringify(rowPayload(row)); }
|
||||
function touched(row) { return row.id || row.persisted || row.srNumber || row.activity || row.taskType || row.notes.trim() || Object.values(row.hours).some(value => value !== "") || Object.values(row.invalidHours).some(Boolean); }
|
||||
function validRow(row) {
|
||||
return !!row.taskType && (row.type === "Non Service" || (!!row.srNumber && !!row.activity))
|
||||
&& !Object.values(row.invalidHours).some(Boolean)
|
||||
&& Object.values(row.hours).every(value => Number.isFinite(Number(value)) && Number(value) >= 0 && Number(value) <= 24)
|
||||
&& Object.values(row.hours).some(value => Number(value) > 0);
|
||||
}
|
||||
function changedRows() { return entry.rows.filter(row => touched(row) && signature(row) !== row.baseline); }
|
||||
function pendingDeletes() {
|
||||
const key = entry.days.map(day => day.Date).join("|");
|
||||
if (!entry.deletions.has(key)) entry.deletions.set(key, new Set());
|
||||
return entry.deletions.get(key);
|
||||
}
|
||||
function updateSave() {
|
||||
const rows = changedRows();
|
||||
const deletions = pendingDeletes();
|
||||
save.disabled = (rows.length > 0 && !requestSave) || (deletions.size > 0 && !requestDelete) || !entry.tbody || entry.saving || (!rows.length && !deletions.size)
|
||||
|| entry.rows.some(row => touched(row) && !validRow(row)) || rows.some(row => row.persisted && !row.id);
|
||||
save.title = save.disabled ? "Complete the required fields and enter hours to save changes." : "Save time entries";
|
||||
}
|
||||
async function saveEntries() {
|
||||
updateSave();
|
||||
if (save.disabled) return;
|
||||
const rows = changedRows(), knownIds = new Set(entry.rows.filter(row => row.id).map(row => String(row.id)));
|
||||
const controls = [];
|
||||
function lock(node) {
|
||||
if (["BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(String(node.tagName || node.tag).toUpperCase())) { controls.push([node, node.disabled]); node.disabled = true; }
|
||||
Array.from(node.children).forEach(lock);
|
||||
}
|
||||
entry.saving = true; lock(modal);
|
||||
saveStatus.hidden = false; saveStatus.setAttribute("role", "status"); saveStatus.textContent = "Saving time entries...";
|
||||
let savedCount = 0, deletedCount = 0;
|
||||
try {
|
||||
const deletions = pendingDeletes();
|
||||
for (const id of [...deletions]) {
|
||||
await requestDelete(id);
|
||||
deletions.delete(id);
|
||||
deletedCount++;
|
||||
}
|
||||
for (const method of ["PUT", "POST"]) {
|
||||
const group = rows.filter(row => method === "PUT" ? !!row.id : !row.id);
|
||||
if (!group.length) continue;
|
||||
const result = await requestSave(method, group.map(row => ({ ...rowPayload(row), ...(row.id ? { id: row.id } : {}) })));
|
||||
group.forEach(row => { row.persisted = true; row.baseline = signature(row); });
|
||||
savedCount += group.length;
|
||||
if (method === "POST") {
|
||||
let records = Array.isArray(result) ? result : Array.isArray(result?.items) ? result.items : [];
|
||||
function assignIds() {
|
||||
const candidates = rowsFromEntries(records, entry.days);
|
||||
for (const row of group.filter(row => !row.id)) {
|
||||
const match = candidates.find(candidate => candidate.id && !knownIds.has(String(candidate.id)) && signature(candidate) === row.baseline);
|
||||
if (match) { row.id = match.id; knownIds.add(String(match.id)); }
|
||||
}
|
||||
}
|
||||
assignIds();
|
||||
if (group.some(row => !row.id)) { records = await requestEntries(entry.days[0].WeekId); assignIds(); }
|
||||
if (group.some(row => !row.id)) throw new Error("Entries saved, but their IDs could not be confirmed. Reopen Manage Time before editing these entries.");
|
||||
}
|
||||
}
|
||||
saveStatus.textContent = [savedCount ? `${savedCount} time ${savedCount === 1 ? "entry" : "entries"} saved.` : "", deletedCount ? `${deletedCount} time ${deletedCount === 1 ? "entry" : "entries"} deleted.` : ""].filter(Boolean).join(" ");
|
||||
notify(saveStatus.textContent);
|
||||
saveStatus.hidden = true;
|
||||
} catch (error) {
|
||||
saveStatus.setAttribute("role", "alert");
|
||||
saveStatus.textContent = `${savedCount ? `${savedCount} saved. ` : ""}${deletedCount ? `${deletedCount} deleted. ` : ""}${error.message || "Unable to save time entries."}`;
|
||||
notify(saveStatus.textContent, true);
|
||||
} finally {
|
||||
entry.saving = false;
|
||||
controls.forEach(([control, disabled]) => { control.disabled = disabled; });
|
||||
if (active === entry) { renderRows(); updateSave(); }
|
||||
}
|
||||
}
|
||||
function total(value) { return Number(Number(value).toFixed(2)).toString(); }
|
||||
function updateTotals() {
|
||||
const groups = { Service: 0, "Non Service": 0, "Time Off": 0, "Total Hours": 0 };
|
||||
@@ -133,18 +233,27 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
});
|
||||
entry.dayTotals[i].textContent = total(sum);
|
||||
});
|
||||
Object.entries(groups).forEach(([label, value]) => { sums[label].textContent = `${total(value)} hrs`; });
|
||||
Object.entries(groups).forEach(([label, value]) => { sums[label].textContent = `${total(value)}h`; });
|
||||
updateSave();
|
||||
}
|
||||
function addRow(type, source) {
|
||||
if (!entry.tbody || add.disabled) return;
|
||||
entry.rows.push(source ? { ...source, hours: { ...source.hours } } : blankRow(type)); renderRows();
|
||||
entry.rows.push(source ? { ...source, id: undefined, persisted: false, baseline: undefined, duplicateHighlightAt: undefined,
|
||||
...(source.type === "Service" ? { srNumber: "", activity: "" } : {}),
|
||||
invalidHours: { ...source.invalidHours }, hours: { ...source.hours } } : blankRow(type)); renderRows();
|
||||
}
|
||||
function renderRows() {
|
||||
if (entry.saving) return;
|
||||
const focusedId = document.activeElement?.id;
|
||||
combos = [];
|
||||
entry.tbody.replaceChildren();
|
||||
entry.rows.forEach((row, index) => {
|
||||
const tr = node("tr");
|
||||
if (row.duplicateHighlightAt && Date.now() - row.duplicateHighlightAt < 5500) {
|
||||
tr.className = "oe-time-duplicate-row";
|
||||
tr.setAttribute("style", `animation-delay: -${Math.max(0, Date.now() - row.duplicateHighlightAt)}ms`);
|
||||
tr.addEventListener("animationend", () => { tr.className = ""; row.duplicateHighlightAt = undefined; });
|
||||
}
|
||||
for (const label of [row.type === "Service" ? "Service Request" : "Non Service", "Activity", "Task Type"]) {
|
||||
const td = node("td"), select = node("select");
|
||||
select.id = `oe-time-${index}-${label.toLowerCase().replaceAll(" ", "-")}`;
|
||||
@@ -156,34 +265,51 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
const option = node("option", "", sr.srTitle ? `${sr.srTitle} (${sr.srNumber})` : sr.srNumber);
|
||||
option.value = sr.srNumber; option.title = option.textContent; select.append(option);
|
||||
}
|
||||
if (row.srNumber && !entry.services.some(sr => sr.srNumber === row.srNumber)) {
|
||||
const option = node("option", "", row.srNumber); option.value = row.srNumber; select.append(option);
|
||||
}
|
||||
select.value = row.srNumber;
|
||||
select.disabled = !entry.servicesReady;
|
||||
select.title = entry.servicesReady ? "Service Request" : "Loading Service Requests";
|
||||
change = () => { row.srNumber = select.value; row.activity = ""; renderRows(); };
|
||||
change = () => {
|
||||
const duplicate = select.value && entry.rows.find(other => other !== row && other.type === "Service" && other.srNumber === select.value);
|
||||
if (duplicate) {
|
||||
duplicate.duplicateHighlightAt = Date.now();
|
||||
select.value = row.srNumber;
|
||||
notify("This Service Request is already selected in another row.", true);
|
||||
renderRows(); return;
|
||||
}
|
||||
row.srNumber = select.value; row.activity = ""; renderRows();
|
||||
};
|
||||
} else if (label === "Activity") {
|
||||
const source = entry.expanded.get(row.type === "Service" ? row.srNumber : "non-service-sr");
|
||||
const raw = String(source?.SRActivities_c || "");
|
||||
const activities = [...new Set(raw.split(raw.includes(";") ? ";" : ",").map(value => value.trim()).filter(Boolean))];
|
||||
if (row.activity && !activities.includes(row.activity)) activities.push(row.activity);
|
||||
for (const activity of activities) { const option = node("option", "", activity); option.value = activity; select.append(option); }
|
||||
select.value = row.activity; select.disabled = !activities.length;
|
||||
select.title = activities.length ? "Activity" : "No activities available for this Service Request";
|
||||
change = () => { row.activity = select.value; };
|
||||
change = () => { row.activity = select.value; updateSave(); };
|
||||
} else if (label === "Task Type") {
|
||||
const tag = row.type === "Service" ? "SERVICE" : "NON_SERVICE";
|
||||
const taskTypes = entry.taskTypes.filter(item => item.tags_c === tag);
|
||||
for (const item of taskTypes) {
|
||||
const option = node("option", "", item.lookUpMeaning_c); option.value = item.lookUpCode_c; select.append(option);
|
||||
}
|
||||
if (entry.taskTypesReady && !taskTypes.some(item => item.lookUpCode_c === row.taskType)) row.taskType = "";
|
||||
if (row.taskType && !taskTypes.some(item => item.lookUpCode_c === row.taskType)) {
|
||||
const option = node("option", "", row.taskType); option.value = row.taskType; select.append(option);
|
||||
}
|
||||
select.value = row.taskType;
|
||||
select.disabled = !entry.taskTypesReady || !taskTypes.length;
|
||||
select.disabled = !entry.taskTypesReady || (!taskTypes.length && !row.taskType);
|
||||
select.title = !entry.taskTypesReady ? "Loading Task Types" : taskTypes.length ? "Task Type" : "No Task Types available for this row type";
|
||||
change = () => { row.taskType = select.value; };
|
||||
change = () => { row.taskType = select.value; updateSave(); };
|
||||
} else { select.disabled = true; select.title = "Options are not available yet."; }
|
||||
td.append(select); tr.append(td);
|
||||
select.addEventListener("change", change);
|
||||
const combo = searchableSelect(select, td, change, {
|
||||
floating: true,
|
||||
listWidth: label === "Service Request" ? 640 : 0,
|
||||
listMaxHeight: label === "Service Request" ? 360 : 180,
|
||||
tooltipContent: label === "Service Request" ? serviceRequestTooltip : undefined
|
||||
});
|
||||
combo.input.setAttribute("aria-label", `${label}, row ${index + 1}`);
|
||||
@@ -193,10 +319,17 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
const td = node("td"), input = node("input");
|
||||
input.type = "number"; input.min = "0"; input.max = "24"; input.step = "1";
|
||||
input.value = row.hours[day.Date] ?? "";
|
||||
input.setAttribute("aria-invalid", String(!!row.invalidHours[day.Date]));
|
||||
input.setAttribute("aria-label", `Hours ${day.Date}, row ${index + 1}`);
|
||||
input.addEventListener("input", () => {
|
||||
const valid = input.value === "" || (Number.isFinite(input.valueAsNumber) && input.valueAsNumber >= 0 && input.valueAsNumber <= 24);
|
||||
input.setAttribute("aria-invalid", String(!valid)); row.hours[day.Date] = valid ? input.value : ""; updateTotals();
|
||||
const valid = !input.validity?.badInput && (input.value === "" || (Number.isFinite(input.valueAsNumber) && input.valueAsNumber >= 0 && input.valueAsNumber <= 24));
|
||||
if (!valid) {
|
||||
input.value = row.hours[day.Date] ?? "";
|
||||
notify("Enter hours between 0 and 24.", true);
|
||||
return;
|
||||
}
|
||||
row.invalidHours[day.Date] = !valid;
|
||||
input.setAttribute("aria-invalid", String(!valid)); row.hours[day.Date] = input.value; updateTotals();
|
||||
}); td.append(input); tr.append(td);
|
||||
}
|
||||
const notesCell = node("td"), notesRow = node("tr", "oe-time-notes"); notesRow.hidden = !row.notesOpen;
|
||||
@@ -205,7 +338,7 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
const remaining = node("span", "oe-time-notes-count", `${2000 - row.notes.length} characters left`);
|
||||
const notes = node("textarea"); notes.rows = 6; notes.maxLength = 2000; notes.value = row.notes;
|
||||
notes.setAttribute("aria-label", `Notes, row ${index + 1}`);
|
||||
notes.addEventListener("input", () => { row.notes = notes.value.slice(0, 2000); remaining.textContent = `${2000 - row.notes.length} characters left`; });
|
||||
notes.addEventListener("input", () => { row.notes = notes.value.slice(0, 2000); remaining.textContent = `${2000 - row.notes.length} characters left`; updateSave(); });
|
||||
const collapse = button(`Collapse notes, row ${index + 1}`, "up", () => {
|
||||
row.notesOpen = false; notesRow.hidden = true; notesButton.setAttribute("aria-expanded", "false"); notesButton.focus();
|
||||
});
|
||||
@@ -216,7 +349,17 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
if (!notesRow.hidden) notes.focus();
|
||||
}); notesButton.setAttribute("aria-expanded", String(row.notesOpen)); notesCell.append(notesButton); tr.append(notesCell);
|
||||
const copy = node("td"); copy.append(button(`Copy row ${index + 1}`, "copy", () => addRow(row.type, row))); tr.append(copy);
|
||||
const remove = node("td"); remove.append(button(`Delete row ${index + 1}`, "trash", () => { entry.rows.splice(index, 1); renderRows(); })); tr.append(remove);
|
||||
const remove = node("td"); remove.append(button(`Delete row ${index + 1}`, "trash", () => {
|
||||
if (entry.saving) return;
|
||||
if (row.persisted && !row.id) {
|
||||
saveStatus.hidden = false; saveStatus.setAttribute("role", "alert");
|
||||
saveStatus.textContent = "Reopen Manage Time to obtain the saved entry ID before deleting it."; return;
|
||||
}
|
||||
if (row.id) pendingDeletes().add(String(row.id));
|
||||
entry.rows.splice(index, 1); renderRows();
|
||||
saveStatus.hidden = false; saveStatus.setAttribute("role", "status");
|
||||
saveStatus.textContent = pendingDeletes().size ? `${pendingDeletes().size} pending ${pendingDeletes().size === 1 ? "deletion" : "deletions"}. Click Save to apply.` : "Unsaved row removed.";
|
||||
})); tr.append(remove);
|
||||
entry.tbody.append(tr, notesRow);
|
||||
}); updateTotals();
|
||||
combos.find(combo => combo.input.id === focusedId)?.input.focus();
|
||||
@@ -240,6 +383,7 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
return details;
|
||||
}
|
||||
async function loadTaskTypes() {
|
||||
taskStatus.className = "oe-time-status oe-time-loading-status";
|
||||
taskStatus.hidden = false; taskStatus.textContent = "Loading Task Types...";
|
||||
taskStatus.setAttribute("role", "status"); taskRetry.hidden = true;
|
||||
try {
|
||||
@@ -248,10 +392,12 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
entry.taskTypes = items.filter(item => typeof item?.lookUpCode_c === "string" && item.lookUpCode_c.trim()
|
||||
&& typeof item.lookUpMeaning_c === "string" && item.lookUpMeaning_c.trim());
|
||||
entry.taskTypesReady = true;
|
||||
taskStatus.className = "oe-time-status";
|
||||
taskStatus.textContent = "No Task Types available."; taskStatus.hidden = entry.taskTypes.length > 0;
|
||||
if (entry.tbody) renderRows();
|
||||
} catch (error) {
|
||||
if (active !== entry) return;
|
||||
taskStatus.className = "oe-time-status";
|
||||
taskStatus.textContent = error.message || "Unable to load Task Types.";
|
||||
taskStatus.setAttribute("role", "alert"); taskRetry.hidden = false;
|
||||
}
|
||||
@@ -260,6 +406,7 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
if (entry.servicesStarted && !retrying) return;
|
||||
entry.servicesStarted = true; entry.initialWeekId = weekId;
|
||||
serviceRetry.hidden = true; serviceStatus.hidden = false;
|
||||
serviceStatus.className = "oe-time-status oe-time-loading-status";
|
||||
serviceStatus.setAttribute("role", "status"); serviceStatus.textContent = "Loading Service Requests...";
|
||||
try {
|
||||
const services = entry.servicesReady ? entry.services : await requestServices(weekId);
|
||||
@@ -276,14 +423,32 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
if (entry.tbody) renderRows();
|
||||
} catch (error) {
|
||||
if (active !== entry) return;
|
||||
serviceStatus.className = "oe-time-status";
|
||||
serviceStatus.textContent = error.message || "Unable to load Service Requests.";
|
||||
serviceStatus.setAttribute("role", "alert"); serviceRetry.hidden = false;
|
||||
}
|
||||
}
|
||||
function renderWeek(items) {
|
||||
function rowsFromEntries(records, days) {
|
||||
return records.map(record => {
|
||||
const row = blankRow(record.tag === "NON_SERVICE" || record.srNumber === "non-service-sr" ? "Non Service" : "Service");
|
||||
row.id = record.id;
|
||||
row.deliveryType = record.deliveryType || "";
|
||||
row.srNumber = record.srNumber || "";
|
||||
row.activity = record.activityCode || "";
|
||||
row.taskType = record.taskType || "";
|
||||
row.notes = typeof record.notes === "string" ? record.notes : "";
|
||||
for (const day of days) {
|
||||
const value = record[hourFields[dates.fromApi(day.Date).getUTCDay()]];
|
||||
row.hours[day.Date] = value == null ? "" : String(value);
|
||||
}
|
||||
row.baseline = signature(row);
|
||||
return row;
|
||||
});
|
||||
}
|
||||
function renderWeek(items, records) {
|
||||
entry.days = items;
|
||||
const key = items.map(day => day.Date).join("|");
|
||||
if (!entry.drafts.has(key)) entry.drafts.set(key, Array.from({ length: 3 }, () => blankRow("Service")));
|
||||
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));
|
||||
@@ -299,20 +464,55 @@ function createManageTime({ MODAL_ID, environment, searchableSelect, dates, requ
|
||||
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();
|
||||
}
|
||||
function renderSkeleton() {
|
||||
const head = node("thead"), heading = node("tr"), skeletonBody = node("tbody");
|
||||
for (let column = 0; column < 13; column++) {
|
||||
const cell = node("th");
|
||||
cell.append(node("span", "oe-time-skeleton oe-time-skeleton-heading")); heading.append(cell);
|
||||
}
|
||||
head.append(heading);
|
||||
for (let row = 0; row < 3; row++) {
|
||||
const tr = node("tr", "oe-time-skeleton-row");
|
||||
for (let column = 0; column < 13; column++) {
|
||||
const cell = node("td");
|
||||
cell.append(node("span", `oe-time-skeleton ${column < 3 ? "oe-time-skeleton-select" : "oe-time-skeleton-control"}`)); tr.append(cell);
|
||||
}
|
||||
skeletonBody.append(tr);
|
||||
}
|
||||
table.setAttribute("aria-hidden", "true"); table.replaceChildren(head, skeletonBody);
|
||||
}
|
||||
function finishSkeleton() {
|
||||
table.setAttribute("aria-hidden", "false");
|
||||
endingValue.className = "";
|
||||
Object.values(sums).forEach(value => { value.className = ""; });
|
||||
status.className = "oe-time-status";
|
||||
}
|
||||
async function load() {
|
||||
if (entry.saving) return;
|
||||
finishSkeleton();
|
||||
const version = ++entry.version; entry.tbody = null; table.replaceChildren(); endingValue.textContent = "--";
|
||||
save.disabled = true; saveStatus.hidden = true;
|
||||
Object.values(sums).forEach(value => { value.textContent = "--"; });
|
||||
add.disabled = addNonService.disabled = true; retry.hidden = true; status.hidden = false;
|
||||
status.setAttribute("role", "status"); scroll.setAttribute("aria-busy", "false");
|
||||
if (!dates.parse(date.value)) { status.textContent = "Select a valid reference date."; return; }
|
||||
status.className = "oe-time-status oe-time-loading-status";
|
||||
endingValue.className = "oe-time-skeleton oe-time-skeleton-summary";
|
||||
Object.values(sums).forEach(value => { value.className = "oe-time-skeleton oe-time-skeleton-summary"; });
|
||||
renderSkeleton();
|
||||
status.textContent = "Loading week details..."; scroll.setAttribute("aria-busy", "true");
|
||||
try {
|
||||
const items = await requestWeek(date.value);
|
||||
if (active !== entry || entry.version !== version) return;
|
||||
renderWeek(items); add.disabled = addNonService.disabled = false; status.hidden = true;
|
||||
status.textContent = "Loading time entries...";
|
||||
const key = items.map(day => day.Date).join("|");
|
||||
const records = entry.drafts.has(key) ? [] : await requestEntries(items[0].WeekId);
|
||||
if (active !== entry || entry.version !== version) return;
|
||||
finishSkeleton(); renderWeek(items, records); add.disabled = addNonService.disabled = false; status.hidden = true;
|
||||
loadServices(items[0].WeekId);
|
||||
} catch (error) {
|
||||
if (active !== entry || entry.version !== version) return;
|
||||
finishSkeleton(); table.replaceChildren();
|
||||
status.textContent = error.message || "Unable to load week details.";
|
||||
status.setAttribute("role", "alert"); retry.hidden = false;
|
||||
} finally {
|
||||
|
||||
@@ -6,7 +6,7 @@ function createSearchableSelect({ environment }) {
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
function searchableSelect(select, field, onChange, { floating = false, tooltipContent } = {}) {
|
||||
function searchableSelect(select, field, onChange, { floating = false, tooltipContent, listWidth = 0, listMaxHeight = 180 } = {}) {
|
||||
const box = element("div", "opportunities-extension-action-combobox");
|
||||
const input = element("input");
|
||||
input.type = "text";
|
||||
@@ -79,11 +79,11 @@ function createSearchableSelect({ environment }) {
|
||||
input.value = options().find(item => item.value === select.value)?.textContent || "";
|
||||
input.title = tooltipContent ? "" : input.value;
|
||||
}
|
||||
function highlight() {
|
||||
function highlight(withTooltip = true) {
|
||||
Array.from(list.children).forEach((node, i) => node.setAttribute("aria-selected", String(i === index)));
|
||||
input.setAttribute("aria-activedescendant", matches[index] ? `${list.id}-${index}` : "");
|
||||
list.children[index]?.scrollIntoView?.({ block: "nearest" });
|
||||
if (matches[index]) showTooltip(matches[index].value, list.children[index]);
|
||||
if (withTooltip && matches[index]) showTooltip(matches[index].value, list.children[index]);
|
||||
}
|
||||
function choose(i) {
|
||||
if (!matches[i]) return;
|
||||
@@ -93,18 +93,22 @@ function createSearchableSelect({ environment }) {
|
||||
if (changed) onChange?.();
|
||||
}
|
||||
function positionList() {
|
||||
const bounds = input.closest?.(".opportunities-extension-action-form-dialog")?.getBoundingClientRect();
|
||||
let bounds = input.closest?.(".opportunities-extension-action-form-dialog")?.getBoundingClientRect();
|
||||
const anchor = input.getBoundingClientRect?.();
|
||||
if (!bounds || !anchor) return;
|
||||
if (floating && listWidth && document.documentElement?.clientWidth) {
|
||||
bounds = { top: 0, left: 0, right: document.documentElement.clientWidth, bottom: document.documentElement.clientHeight };
|
||||
}
|
||||
const below = Math.max(0, bounds.bottom - anchor.bottom - 8);
|
||||
const above = Math.max(0, anchor.top - bounds.top - 8);
|
||||
const opensAbove = below < 180 && above > below;
|
||||
const opensAbove = below < listMaxHeight && above > below;
|
||||
list.setAttribute("data-placement", opensAbove ? "above" : "below");
|
||||
list.style.maxHeight = `${Math.min(180, opensAbove ? above : below)}px`;
|
||||
list.style.maxHeight = `${Math.min(listMaxHeight, opensAbove ? above : below)}px`;
|
||||
if (floating) {
|
||||
const width = listWidth ? Math.min(Math.max(anchor.width, listWidth), bounds.right - bounds.left - 16) : anchor.width;
|
||||
list.style.position = "fixed";
|
||||
list.style.left = `${anchor.left}px`;
|
||||
list.style.width = `${anchor.width}px`;
|
||||
list.style.left = `${listWidth ? Math.max(bounds.left + 8, Math.min(anchor.left, bounds.right - width - 8)) : anchor.left}px`;
|
||||
list.style.width = `${width}px`;
|
||||
list.style.right = "auto";
|
||||
list.style.bottom = "auto";
|
||||
list.style.top = `${opensAbove ? anchor.top - 4 : anchor.bottom + 4}px`;
|
||||
@@ -115,7 +119,7 @@ function createSearchableSelect({ environment }) {
|
||||
if (input.disabled) return;
|
||||
hideTooltip();
|
||||
matches = options().filter(item => normalize(`${item.textContent} ${item.getAttribute?.("data-description") || ""} ${item.value}`).includes(normalize(query)));
|
||||
index = -1;
|
||||
index = matches.findIndex(item => item.value === select.value);
|
||||
list.textContent = "";
|
||||
matches.forEach((item, i) => {
|
||||
const row = element("div", "opportunities-extension-action-option", item.textContent);
|
||||
@@ -140,7 +144,7 @@ function createSearchableSelect({ environment }) {
|
||||
positionList();
|
||||
list.hidden = false;
|
||||
input.setAttribute("aria-expanded", "true");
|
||||
input.setAttribute("aria-activedescendant", "");
|
||||
highlight(false);
|
||||
}
|
||||
input.addEventListener("click", () => { render(); input.select?.(); });
|
||||
input.addEventListener("input", () => render(input.value));
|
||||
|
||||
@@ -4,11 +4,11 @@ function createFormRequestCache({ getHours, getUserId }) {
|
||||
let generation = 0;
|
||||
let owner = "";
|
||||
const queries = new Set([
|
||||
"requestWeekDetails", "requestActiveServiceRequests", "requestServiceRequestExpand", "requestLookupTaskType",
|
||||
"requestWeekDetails", "requestTimeEntries", "requestActiveServiceRequests", "requestServiceRequestExpand", "requestLookupTaskType",
|
||||
"requestActionTeamMembers", "requestActionFindLookup", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage",
|
||||
"requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestLookupTypePlatform", "requestInternalSrServices"
|
||||
]);
|
||||
const writes = new Set(["requestMgmtAction", "requestPostInternalServiceRequest", "requestPostServiceRequest"]);
|
||||
const writes = new Set(["requestTimeEntriesDelete", "requestTimeEntriesSave", "requestMgmtAction", "requestPostInternalServiceRequest", "requestPostServiceRequest"]);
|
||||
function invalidate() { generation++; entries.clear(); pending.clear(); }
|
||||
function maxAge() {
|
||||
const hours = Number(getHours());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createTimeEntries({ fetchJson, dates, getResourceUser }) {
|
||||
function createTimeEntries({ fetchJson, dates, getResourceUser, saveHeaders }) {
|
||||
const endpoint = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries/weekDetails";
|
||||
async function requestWeekDetails(message) {
|
||||
const debugRequests = [];
|
||||
@@ -15,6 +15,61 @@ function createTimeEntries({ fetchJson, dates, getResourceUser }) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function requestTimeEntries(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
if (!/^\d+$/.test(String(message.weekId)) || Number(message.weekId) <= 0) throw new Error("Missing or invalid WeekId.");
|
||||
const resource = await getResourceUser(message);
|
||||
debugRequests.push(...resource.debugRequests);
|
||||
const url = endpoint.replace(/\/weekDetails$/, "") + `?resourceId=${encodeURIComponent(resource.resource.resourcePartyId)}&sortBy=createdDate&sortOrder=ASC&weekId=${encodeURIComponent(message.weekId)}`;
|
||||
const items = await fetchJson(url, { method: "GET", credentials: "include", headers: { Authorization: "Session", Accept: "application/json" } }, "requestTimeEntries", debugRequests);
|
||||
if (!Array.isArray(items)) throw new Error("Invalid time entries response.");
|
||||
return { ok: true, items, debugRequests };
|
||||
} catch (error) { error.debugRequests = [...debugRequests, ...(error.debugRequests || [])]; throw error; }
|
||||
}
|
||||
async function requestTimeEntriesDelete(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
if (!/^[1-9]\d*$/.test(String(message.id))) throw new Error("Missing or invalid time entry ID.");
|
||||
const url = endpoint.replace(/\/weekDetails$/, "") + `/${message.id}`;
|
||||
const headers = await saveHeaders(url);
|
||||
const result = await fetchJson(url, { method: "DELETE", credentials: "include", headers },
|
||||
"requestTimeEntriesDelete", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestTimeEntriesSave(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
if (!["POST", "PUT"].includes(message.method)) throw new Error("Invalid time entry save method.");
|
||||
if (!Array.isArray(message.items) || !message.items.length) throw new Error("No time entries to save.");
|
||||
const hours = ["saturdayTaskHours", "sundayTaskHours", "mondayTaskHours", "tuesdayTaskHours", "wednesdayTaskHours", "thursdayTaskHours", "fridayTaskHours"];
|
||||
for (const item of message.items) {
|
||||
if (!item || !["SERVICE", "NON_SERVICE"].includes(item.tag) || !item.taskType
|
||||
|| (item.tag === "SERVICE" && (!/^SR\d+$/.test(item.srNumber) || !item.activityCode))
|
||||
|| !/^\d+$/.test(String(item.weekId)) || Number(item.weekId) <= 0
|
||||
|| hours.some(key => typeof item[key] !== "number" || !Number.isFinite(item[key]) || item[key] < 0 || item[key] > 24)
|
||||
|| !hours.some(key => item[key] > 0)
|
||||
|| (message.method === "PUT" && (!/^\d+$/.test(String(item.id)) || Number(item.id) <= 0))) throw new Error("Invalid or incomplete time entry.");
|
||||
}
|
||||
const resource = await getResourceUser(message);
|
||||
debugRequests.push(...resource.debugRequests);
|
||||
const items = message.items.map(item => ({
|
||||
...(message.method === "PUT" ? { id: item.id } : {}),
|
||||
srNumber: item.tag === "NON_SERVICE" ? "non-service-sr" : item.srNumber,
|
||||
activityCode: item.activityCode || "", taskType: item.taskType, deliveryType: item.deliveryType || "",
|
||||
...Object.fromEntries(hours.map(key => [key, item[key]])), notes: String(item.notes || ""),
|
||||
weekId: item.weekId, resourceId: String(resource.resource.resourcePartyId), tag: item.tag
|
||||
}));
|
||||
const url = endpoint.replace(/\/weekDetails$/, "");
|
||||
const headers = await saveHeaders(url);
|
||||
const result = await fetchJson(url, {
|
||||
method: message.method, credentials: "include",
|
||||
headers, body: JSON.stringify(items)
|
||||
}, "requestTimeEntriesSave", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) { error.debugRequests = [...debugRequests, ...(error.debugRequests || [])]; throw error; }
|
||||
}
|
||||
async function requestActiveServiceRequests(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
@@ -59,5 +114,5 @@ function createTimeEntries({ fetchJson, dates, getResourceUser }) {
|
||||
return { ok: true, items: result.items, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
return { requestWeekDetails, requestActiveServiceRequests, requestServiceRequestExpand, requestLookupTaskType };
|
||||
return { requestWeekDetails, requestTimeEntries, requestTimeEntriesSave, requestTimeEntriesDelete, requestActiveServiceRequests, requestServiceRequestExpand, requestLookupTaskType };
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
function createMessageTypes() {
|
||||
return Object.freeze({
|
||||
requestWeekDetails: "opportunitiesExtension.requestWeekDetails",
|
||||
requestTimeEntries: "opportunitiesExtension.requestTimeEntries",
|
||||
requestTimeEntriesSave: "opportunitiesExtension.requestTimeEntriesSave",
|
||||
requestTimeEntriesDelete: "opportunitiesExtension.requestTimeEntriesDelete",
|
||||
requestLookupTaskType: "opportunitiesExtension.requestLookupTaskType",
|
||||
requestServiceRequestExpand: "opportunitiesExtension.requestServiceRequestExpand",
|
||||
requestResourceUser: "opportunitiesExtension.requestResourceUser",
|
||||
|
||||
Reference in New Issue
Block a user