|
@@ -208,6 +208,10 @@
|
|
|
|
|
|
|
|
// -------- add job (ICG only) --------
|
|
// -------- add job (ICG only) --------
|
|
|
|
|
|
|
|
|
|
+ // While this is non-null, polling skips auto-reload so the in-progress
|
|
|
|
|
+ // compose form isn't ripped out from under the user.
|
|
|
|
|
+ let composeRowId = null;
|
|
|
|
|
+
|
|
|
function wireAddJob() {
|
|
function wireAddJob() {
|
|
|
const btn = $('#add-job');
|
|
const btn = $('#add-job');
|
|
|
if (!btn) return;
|
|
if (!btn) return;
|
|
@@ -217,16 +221,114 @@
|
|
|
postForm('bin/jobs_add.php', { vendor, ajax: '1' })
|
|
postForm('bin/jobs_add.php', { vendor, ajax: '1' })
|
|
|
.then(r => r.text().then(t => ({ ok: r.ok, body: t })))
|
|
.then(r => r.text().then(t => ({ ok: r.ok, body: t })))
|
|
|
.then(res => {
|
|
.then(res => {
|
|
|
- if (!res.ok || res.body.trim() !== 'Success') {
|
|
|
|
|
|
|
+ const newId = parseInt((res.body || '').trim(), 10);
|
|
|
|
|
+ if (!res.ok || !Number.isInteger(newId) || newId <= 0) {
|
|
|
alert('Add failed: ' + res.body);
|
|
alert('Add failed: ' + res.body);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- return reloadTable();
|
|
|
|
|
|
|
+ return reloadTable().then(() => {
|
|
|
|
|
+ const row = $('#jobs-table tr[data-job-id="' + newId + '"]');
|
|
|
|
|
+ if (row) composeRow(row);
|
|
|
|
|
+ });
|
|
|
})
|
|
})
|
|
|
.catch(err => alert('Network error: ' + err.message));
|
|
.catch(err => alert('Network error: ' + err.message));
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Open every editable cell of `rowEl` as an <input> simultaneously so the
|
|
|
|
|
+ // user can Tab between them. Each input's initial value is captured; on
|
|
|
|
|
+ // exit (Escape, Enter past the last cell, or focus leaving the row) only
|
|
|
|
|
+ // the changed fields are saved in parallel, then the table reloads once.
|
|
|
|
|
+ function composeRow(rowEl) {
|
|
|
|
|
+ const jobId = parseInt(rowEl.dataset.jobId, 10);
|
|
|
|
|
+ composeRowId = jobId;
|
|
|
|
|
+
|
|
|
|
|
+ const cells = Array.from(rowEl.querySelectorAll('.editable'));
|
|
|
|
|
+ const inputs = cells.map(span => {
|
|
|
|
|
+ const col = span.dataset.column;
|
|
|
|
|
+ const raw = span.dataset.raw !== undefined ? span.dataset.raw : span.textContent.trim();
|
|
|
|
|
+ const initial = raw === '—' ? '' : raw;
|
|
|
|
|
+ const input = document.createElement('input');
|
|
|
|
|
+ input.type = 'text';
|
|
|
|
|
+ input.className = 'inline-edit';
|
|
|
|
|
+ input.value = initial;
|
|
|
|
|
+ input.dataset.column = col;
|
|
|
|
|
+ input.dataset.initial = initial;
|
|
|
|
|
+ // The span owns tabindex=0 so it can be entered from the keyboard, but
|
|
|
|
|
+ // while it hosts a live input we want Tab to jump straight to the input.
|
|
|
|
|
+ span.setAttribute('tabindex', '-1');
|
|
|
|
|
+ span.innerHTML = '';
|
|
|
|
|
+ span.appendChild(input);
|
|
|
|
|
+ return input;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ inputs.forEach((input, i) => {
|
|
|
|
|
+ input.addEventListener('keydown', e => {
|
|
|
|
|
+ if (e.key === 'Escape') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ finishCompose(rowEl, /*save=*/false);
|
|
|
|
|
+ } else if (e.key === 'Enter') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ if (i + 1 < inputs.length) {
|
|
|
|
|
+ inputs[i + 1].focus();
|
|
|
|
|
+ inputs[i + 1].select();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ finishCompose(rowEl, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Detect focus genuinely leaving the row (not just hopping between its inputs).
|
|
|
|
|
+ rowEl.addEventListener('focusout', () => {
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (composeRowId === jobId && !rowEl.contains(document.activeElement)) {
|
|
|
|
|
+ finishCompose(rowEl, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 50);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (inputs[0]) { inputs[0].focus(); inputs[0].select(); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function finishCompose(rowEl, save) {
|
|
|
|
|
+ if (composeRowId === null) return;
|
|
|
|
|
+ const jobId = composeRowId;
|
|
|
|
|
+ composeRowId = null;
|
|
|
|
|
+
|
|
|
|
|
+ const inputs = Array.from(rowEl.querySelectorAll('input.inline-edit'));
|
|
|
|
|
+ const dirty = save ? inputs.filter(i => i.value !== i.dataset.initial) : [];
|
|
|
|
|
+
|
|
|
|
|
+ // Serialize the saves so they queue against the SQLite writer lock
|
|
|
|
|
+ // politely instead of all racing at once.
|
|
|
|
|
+ const failed = [];
|
|
|
|
|
+ const runNext = (i) => {
|
|
|
|
|
+ if (i >= dirty.length) return Promise.resolve();
|
|
|
|
|
+ const input = dirty[i];
|
|
|
|
|
+ return postForm('bin/jobs_update.php', {
|
|
|
|
|
+ job_id: jobId,
|
|
|
|
|
+ column: input.dataset.column,
|
|
|
|
|
+ value: input.value,
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(r => r.text().then(t => {
|
|
|
|
|
+ if (!r.ok || t.trim() !== 'Success') {
|
|
|
|
|
+ failed.push(input.dataset.column + ': ' + t);
|
|
|
|
|
+ }
|
|
|
|
|
+ }))
|
|
|
|
|
+ .then(() => runNext(i + 1));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ runNext(0)
|
|
|
|
|
+ .then(() => {
|
|
|
|
|
+ if (failed.length) alert('Some saves failed:\n' + failed.join('\n'));
|
|
|
|
|
+ return reloadTable();
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(err => {
|
|
|
|
|
+ alert('Network error: ' + err.message);
|
|
|
|
|
+ reloadTable();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// -------- polling --------
|
|
// -------- polling --------
|
|
|
|
|
|
|
|
function isUserBusy() {
|
|
function isUserBusy() {
|