|
|
@@ -72,6 +72,7 @@
|
|
|
const jobId = row.dataset.jobId;
|
|
|
const action = btn.dataset.action;
|
|
|
if (action === 'partial_ship') return onPartialShip(btn, row, jobId);
|
|
|
+ if (action === 'delete') return onDelete(btn, row, jobId);
|
|
|
|
|
|
btn.disabled = true;
|
|
|
postForm('bin/jobs_update.php', { job_id: jobId, action })
|
|
|
@@ -85,6 +86,22 @@
|
|
|
.catch(err => alert('Network error: ' + err.message));
|
|
|
}
|
|
|
|
|
|
+ function onDelete(btn, row, jobId) {
|
|
|
+ const jobCell = row.querySelector('[data-column="job"]');
|
|
|
+ const label = jobCell ? (jobCell.textContent.trim() || '#' + jobId) : '#' + jobId;
|
|
|
+ if (!window.confirm('Delete job ' + label + '? This cannot be undone from the UI.')) return;
|
|
|
+ btn.disabled = true;
|
|
|
+ postForm('bin/jobs_delete.php', { job_id: jobId })
|
|
|
+ .then(r => r.text().then(t => ({ ok: r.ok, body: t })))
|
|
|
+ .then(res => {
|
|
|
+ if (!res.ok || res.body.trim() !== 'Success') {
|
|
|
+ alert('Delete failed: ' + res.body);
|
|
|
+ }
|
|
|
+ return reloadTable();
|
|
|
+ })
|
|
|
+ .catch(err => alert('Network error: ' + err.message));
|
|
|
+ }
|
|
|
+
|
|
|
function onPartialShip(btn, row, jobId) {
|
|
|
const currentQty = parseInt(btn.dataset.qty, 10);
|
|
|
const raw = window.prompt(
|
|
|
@@ -272,6 +289,12 @@
|
|
|
const jobId = parseInt(rowEl.dataset.jobId, 10);
|
|
|
composeRowId = jobId;
|
|
|
|
|
|
+ // Pull the row's delete button out of the tab order so Tab from the last
|
|
|
+ // field doesn't land on it. (The table reload after finishCompose
|
|
|
+ // rebuilds the row without this attribute, restoring normal tabbing.)
|
|
|
+ const deleteBtn = rowEl.querySelector('.btn-delete');
|
|
|
+ if (deleteBtn) deleteBtn.setAttribute('tabindex', '-1');
|
|
|
+
|
|
|
const cells = Array.from(rowEl.querySelectorAll('.editable'));
|
|
|
const inputs = cells.map(span => {
|
|
|
const col = span.dataset.column;
|
|
|
@@ -304,6 +327,10 @@
|
|
|
} else {
|
|
|
finishCompose(rowEl, true);
|
|
|
}
|
|
|
+ } else if (e.key === 'Tab' && !e.shiftKey && i === inputs.length - 1) {
|
|
|
+ // Tabbing forward off the last field (Due) commits the new row.
|
|
|
+ e.preventDefault();
|
|
|
+ finishCompose(rowEl, true);
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
@@ -373,67 +400,12 @@
|
|
|
reloadAllThreads();
|
|
|
}
|
|
|
|
|
|
- // -------- long-poll (real-time push) --------
|
|
|
- //
|
|
|
- // The server holds the connection open until job_history or messages get
|
|
|
- // a row beyond our since markers, then immediately tells us what changed.
|
|
|
- // We reload only the affected piece. The setInterval timer below remains
|
|
|
- // as a safety net for transient network/server failures.
|
|
|
-
|
|
|
- let lpHistoryId = (PDQ.initialHistoryId | 0) || 0;
|
|
|
- let lpMessageMaxIds = Object.assign({}, PDQ.initialMessageMaxIds || {});
|
|
|
- let lpAbort = null;
|
|
|
- let lpStopped = false;
|
|
|
-
|
|
|
- function buildSinceM() {
|
|
|
- if (PDQ.audience === 'vendor') {
|
|
|
- return String(lpMessageMaxIds[PDQ.vendorId] || 0);
|
|
|
- }
|
|
|
- return Object.entries(lpMessageMaxIds).map(([k, v]) => k + ':' + v).join(',');
|
|
|
- }
|
|
|
-
|
|
|
- function startLongPoll() {
|
|
|
- if (lpStopped) return;
|
|
|
- lpAbort = new AbortController();
|
|
|
- const url = new URL('bin/events_poll.php', window.location.href);
|
|
|
- Object.entries(authParams()).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
|
- url.searchParams.set('since_h', String(lpHistoryId));
|
|
|
- url.searchParams.set('since_m', buildSinceM());
|
|
|
-
|
|
|
- fetch(url.toString(), { signal: lpAbort.signal })
|
|
|
- .then(r => r.json())
|
|
|
- .then(data => {
|
|
|
- // Always update the since markers so the long-poll moves forward and
|
|
|
- // doesn't re-fire the same event in a tight loop.
|
|
|
- if (typeof data.history === 'number') lpHistoryId = data.history;
|
|
|
- if (data.messages && typeof data.messages === 'object') {
|
|
|
- Object.entries(data.messages).forEach(([vid, maxid]) => {
|
|
|
- lpMessageMaxIds[vid] = maxid;
|
|
|
- });
|
|
|
- }
|
|
|
- // Skip the actual reload if the user is editing — would tear out
|
|
|
- // their input. The 60s tick() and the next change will catch them
|
|
|
- // up once they're done.
|
|
|
- const busy = isUserBusy() || composeRowId !== null;
|
|
|
- if (!busy && data.history_changed) reloadTable();
|
|
|
- if (!busy && data.changed_vendors && data.changed_vendors.length) reloadAllThreads();
|
|
|
- startLongPoll(); // immediately reopen
|
|
|
- })
|
|
|
- .catch(err => {
|
|
|
- if (err.name === 'AbortError') return;
|
|
|
- // Network blip / server hiccup: back off briefly and retry. The 60s
|
|
|
- // tick() will keep things fresh in the meantime.
|
|
|
- setTimeout(startLongPoll, 2000);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
wireAddJob();
|
|
|
wireCompose();
|
|
|
reloadTable();
|
|
|
reloadAllThreads();
|
|
|
- startLongPoll();
|
|
|
- const interval = PDQ.pollMs || 60000;
|
|
|
+ const interval = PDQ.pollMs || 180000;
|
|
|
setInterval(tick, interval);
|
|
|
});
|
|
|
})();
|