|
function updateReactInput(inp, value) { |
|
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set.call(inp, value); |
|
inp.dispatchEvent(new Event('change', { bubbles: true})); |
|
} |
|
|
|
function updateReactSelect(inp, value) { |
|
Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value').set.call(inp, value); |
|
inp.dispatchEvent(new Event('change', { bubbles: true})); |
|
} |
|
|
|
function updateReactTextArea(inp, value) { |
|
Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set.call(inp, value); |
|
inp.dispatchEvent(new Event('change', { bubbles: true})); |
|
} |
|
|
|
function fillRow(row, value) { |
|
const rowInputs = row.querySelectorAll(".only-desktop input:not(:disabled)"); |
|
// update all elements |
|
rowInputs.forEach((el,index) => { |
|
if (!isHolidayColumn(index)) { |
|
updateReactInput(el, value); |
|
} |
|
}); |
|
// FIXME: we probably need to wait until the row is actually updated |
|
// I guess the best way to do that is to check the value of the rowInput after |
|
// setting them all. |
|
} |
|
|
|
function holidayRow() { |
|
const tds = document.querySelectorAll(".c-timesheet-table-wide tr.c-table__desktop-view td:nth-child(1)"); |
|
let holidayRowIdx = -1; |
|
Array.from(tds).forEach((td, index) => { |
|
const input = td.querySelector("input"); |
|
if (!input) { |
|
return; |
|
} |
|
|
|
if (input.value === "Holiday") { |
|
holidayRowIdx = index; |
|
} |
|
}); |
|
|
|
return holidayRowIdx; |
|
} |
|
|
|
let holidayColumnIndexes; |
|
/** |
|
* To speed this up, the result is cached since it shouldn't change. |
|
* |
|
* @returns the indexes of holiday columns, the indexes do not count disabled weekend cells |
|
* this should be inline with how the rest of the code only selects cells that are not |
|
* disabled. |
|
*/ |
|
function holidayColumns() { |
|
if (Array.isArray(holidayColumnIndexes)) { |
|
return holidayColumnIndexes; |
|
} |
|
|
|
holidayColumnIndexes = []; |
|
const holidayRowIdx = holidayRow(); |
|
if (holidayRowIdx < 0) { |
|
return holidayColumnIndexes; |
|
} |
|
const rows = document.querySelectorAll(".c-timesheet-table-short tr.c-table__desktop-view"); |
|
const row = rows[holidayRowIdx]; |
|
const rowInputs = row.querySelectorAll(".only-desktop input"); |
|
// Keep our own index so we can skip weekend cells |
|
let columnIdx = 0; |
|
// update all elements |
|
rowInputs.forEach(input => { |
|
if (input.disabled) { |
|
if (input.value === "100") { |
|
holidayColumnIndexes.push(columnIdx); |
|
} else { |
|
// skip disabled columns without 100 values these are weekend rows |
|
return; |
|
} |
|
} |
|
columnIdx++; |
|
}); |
|
|
|
return holidayColumnIndexes; |
|
} |
|
|
|
function isHolidayColumn(columnIdx) { |
|
const holidayColumnArray = holidayColumns(); |
|
return holidayColumnArray.indexOf(columnIdx) >= 0; |
|
} |
|
|
|
function applyToRow() { |
|
const value = document.activeElement.value; |
|
const row = document.activeElement.closest("tr"); |
|
fillRow(row, value); |
|
} |
|
|
|
function processProjectsCell(td) { |
|
const select = td.querySelector("select"); |
|
if (!select) { |
|
return null; |
|
} |
|
const cell = Array.from(select.selectedOptions).map(option => option.text)[0]; |
|
if (!cell) { |
|
return null; |
|
} |
|
const numString = cell.match(/\d+/)?.[0]; |
|
if (!numString) { |
|
return null; |
|
} |
|
|
|
return parseInt(numString, 10); |
|
} |
|
|
|
function getSelectedGrantProjects() { |
|
const tds = document.querySelectorAll(".c-timesheet-table-wide tr.c-table__desktop-view td:nth-child(2)"); |
|
return Array.from(tds).map(processProjectsCell); |
|
} |
|
|
|
// This doesn't actually add the rows, it just updates the input field |
|
function updateRowsToAdd(num) { |
|
const numInput = document.querySelector("input[aria-label='Number of Rows To Add']"); |
|
updateReactInput(numInput, num); |
|
} |
|
|
|
// click the add rows button |
|
function addRows() { |
|
document.querySelector(".c-timesheet-add-block button").click(); |
|
} |
|
|
|
function deleteAllRows() { |
|
const deleteButtons = document.querySelectorAll(".only-desktop button:not(:disabled)[aria-label='Delete']"); |
|
deleteButtons.forEach(el => el.click()); |
|
} |
|
|
|
// This brings up the project browse dialog for the last row |
|
function browseLastRowProject() { |
|
const selects = document.querySelectorAll(".c-timesheet-table-wide tr.c-table__desktop-view td:nth-child(2) select"); |
|
const lastProjectField = selects[selects.length-1]; |
|
updateReactSelect(lastProjectField, "browse"); |
|
} |
|
|
|
// Wait for a function to return a truthy value, checking it every 300ms |
|
async function waitFor(func, errorMsg = null, intervalMS=300) { |
|
let intervalId; |
|
return new Promise((resolve, reject) => { |
|
let count = 0; |
|
intervalId = setInterval(() => { |
|
count += 1; |
|
const result = func(); |
|
if(result){ |
|
clearInterval(intervalId); |
|
resolve(result); |
|
} else if(count > 40) { |
|
clearInterval(intervalId); |
|
reject(errorMsg || "waitFor didn't succeed"); |
|
} |
|
}, intervalMS); |
|
}); |
|
} |
|
|
|
// Wait |
|
function wait(millis) { |
|
return new Promise((resolve) => { |
|
setTimeout(() => resolve(), millis); |
|
}); |
|
} |
|
|
|
// Type in a project number and apply the filter, |
|
// wait for the result to match the request |
|
// then select this result |
|
async function selectProjectInDialog(projNum) { |
|
const input = await waitFor( |
|
() => document.querySelector("input[name='CostCenterFullPath']"), |
|
"Could not find input for project number" |
|
); |
|
updateReactInput(input, projNum); |
|
const applyFilterButton = await waitFor( |
|
() => document.querySelector("button[title='Apply Filter']"), |
|
"Could not find the apply filter button" |
|
); |
|
// Try a few times to filter |
|
let foundIt = false; |
|
for (let tryNumber = 0; tryNumber < 3; tryNumber++) { |
|
applyFilterButton.click(); |
|
try { |
|
foundIt = await waitFor( |
|
() => document.querySelector(".item.left.col_2.row_4 span")?.textContent.startsWith(projNum), |
|
`Could not find the project ${projNum}`, |
|
10 |
|
); |
|
} catch (e) { |
|
// the project wasn't found |
|
console.log(`Failed to find project: ${projNum} on try: ${tryNumber}`); |
|
} |
|
if (foundIt) { |
|
break; |
|
} |
|
} |
|
if (!foundIt) { |
|
throw new Error(`Could not find the project ${projNum}`); |
|
} |
|
// then this query will click the first radio button |
|
document.querySelector("input[type='radio']").click(); |
|
} |
|
|
|
// change the project of the last row to be projNum |
|
async function updateLastProject(projNum) { |
|
browseLastRowProject(); |
|
await selectProjectInDialog(projNum); |
|
await waitFor(() => { |
|
const updatedProjects = getSelectedGrantProjects(); |
|
return updatedProjects.lastItem === projNum; |
|
}, |
|
`Project did not update after selection in dialog: ${projNum}`, |
|
50); |
|
} |
|
|
|
// Add new rows for all of the passed in projects |
|
async function addProjects(projArray) { |
|
console.log("adding projects", projArray); |
|
for(const projNum of projArray) { |
|
addRows(); |
|
await waitFor( |
|
() => getSelectedGrantProjects().lastItem === null, |
|
"Blank row wasn't added", |
|
50); |
|
await updateLastProject(projNum); |
|
} |
|
} |
|
|
|
// Delete any rows for projects not in the array |
|
function deleteRowsNotIn(projArray) { |
|
|
|
// This call is fast, it will return null for a holiday row |
|
const initialProjectNumbers = getSelectedGrantProjects(); |
|
|
|
console.log("Existing projects", initialProjectNumbers); |
|
|
|
const deleteButtons = document.querySelectorAll(".only-desktop button[aria-label='Delete']"); |
|
// By working backward the delete buttons and rows don't have to be recomputed |
|
for(let rowIdx = initialProjectNumbers.length - 1; rowIdx >= 0; rowIdx--) { |
|
const rowProject = initialProjectNumbers[rowIdx]; |
|
if (projArray.includes(rowProject)) { |
|
continue; |
|
} |
|
const deleteButton = deleteButtons.item(rowIdx); |
|
if (deleteButton.disabled) { |
|
// holiday rows should have an null rowProject so no error will be shown |
|
if (rowProject) { |
|
console.error(`Unable to delete project ${rowProject} button disabled`); |
|
} |
|
continue; |
|
} |
|
deleteButton.click(); |
|
} |
|
} |
|
|
|
// Add rows for projects in projArray that don't have rows yet |
|
async function addNewRowsFrom(projArray) { |
|
|
|
// This call is fast, it will return null for a holiday row |
|
const initialProjectNumbers = getSelectedGrantProjects(); |
|
|
|
const newProjects = []; |
|
projArray.forEach(proj => { |
|
if (!initialProjectNumbers.includes(proj)) { |
|
newProjects.push(proj); |
|
} |
|
}); |
|
await addProjects(newProjects); |
|
} |
|
|
|
async function syncRowsWithProjects(projArray) { |
|
deleteRowsNotIn(projArray); |
|
await addNewRowsFrom(projArray); |
|
} |
|
|
|
function firstNotesButton(rowIndex) { |
|
const rows = document.querySelectorAll(".c-timesheet-table-short tr.c-table__desktop-view"); |
|
const row = rows[rowIndex]; |
|
// The :has(input:not(:disabled)) makes sure we put the notes |
|
// in a column that isn't a weekend |
|
// TODO: this won't handle holidays correctly |
|
const buttons = row.querySelectorAll(".only-desktop:has(input:not(:disabled)) button[title='Time Entry Notes']"); |
|
|
|
// TODO: make this more efficient, we don't have to do this every time |
|
return Array.from(buttons).find((button, index) => { |
|
if(!isHolidayColumn(index)) { |
|
return button; |
|
} |
|
}); |
|
} |
|
|
|
async function fillFirstNotes(rowIndex, notes) { |
|
const firstButton = firstNotesButton(rowIndex); |
|
firstButton.click(); |
|
const textarea = await waitFor( |
|
() => document.querySelector("textarea[name='comment']"), |
|
"Could not find textarea for notes" |
|
); |
|
updateReactTextArea(textarea, notes); |
|
const saveButton = await waitFor( |
|
() => document.querySelector(".c-popup button.btn-primary:not(:disabled)"), |
|
"Save button isn't enabled" |
|
); |
|
saveButton.click(); |
|
await waitFor( |
|
() => !document.querySelector(".c-popup"), |
|
"Notes dialog didn't close" |
|
); |
|
} |
|
|
|
async function fillPercentagesAndNotes(timesheetData) { |
|
const rows = document.querySelectorAll(".c-timesheet-table-short tr.c-table__desktop-view"); |
|
const projectNumbers = getSelectedGrantProjects(); |
|
for (let index=0; index<projectNumbers.length; index++) { |
|
const project = projectNumbers[index]; |
|
if (!project) { |
|
// This isn't a project row |
|
continue; |
|
} |
|
console.log(`Filling percentages for project row: ${project}`); |
|
const projectData = timesheetData[project]; |
|
if (!projectData) { |
|
console.log("The project of this row can't be found in the timesheetData"); |
|
continue; |
|
} |
|
if (projectData.notes) { |
|
await fillFirstNotes(index, projectData.notes); |
|
} |
|
const row = rows[index]; |
|
fillRow(row, projectData.percentage); |
|
} |
|
} |
|
|
|
async function applyTimesheetData(timesheetData) { |
|
const projs = Object.keys(timesheetData).map(proj => parseInt(proj, 10)); |
|
await syncRowsWithProjects(projs); |
|
console.log("Updated rows to match input projects"); |
|
fillPercentagesAndNotes(timesheetData); |
|
} |
|
|
|
// iterate all the values of an object and transform them |
|
// something like a {}.map |
|
function mapObject(obj, fn) { |
|
return Object.fromEntries( |
|
Object.entries(obj).map( |
|
([k, v], i) => [k, fn(v, k, i)] |
|
) |
|
); |
|
} |
|
|
|
async function syncRowsAndFillPercentages(percentageMap) { |
|
applyTimesheetData(mapObject(percentageMap, value => ({percentage: value}))); |
|
} |