Add global music player, invoicing/payment system, account subpages, mail account admin, and the qualityiron.vegas Jet 2.0 port
Jet interpreter (Tag.cpp, __call.cpp, __imap*.cpp/h, __smtpsend.cpp/h, __imapemptyfolder.cpp/h - new): assorted parser/tag fixes found across this work - a stray literal-tag-in-comment and backslash-quote parser corruption class, a dangling-pointer fix, a __call.cpp fd leak, IMAP folder-management tags, multi-account/per-host IMAP support, and a smtpsend Message-ID fix. barant.com site (jetc/barant-site/): - A site-wide persistent music player (header band, playlist/queue, stereo FFT EQ, session-persisted state) fed by new library/ ianslibrary/music song-list subpages. - /account split into info/phones/invoices subpages; a full invoicing and Square payment system (create/edit/copy/pay/email invoice); admin invoice management and mail-account management subpages. - /services split into overview/hosting/siptrunking/documents subpages, retiring the old standalone pages. - SIPPHONE.tag: a Jet 2.0 WebRTC softphone tag. - main.css/spa.js/email.js: the SPA loading indicator, the music player band styling, and assorted fixes accumulated alongside this work. qualityiron-site/: the qualityiron.vegas job/contractor/customer/ document management app, ported from its original Jet 1.4 codebase to Jet 2.0 as barant.coms APIServers second multi-tenant site. wcwc/wcwc.jet: the WCWC site Jet source (wcwc.jtc build output not included). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
905fbafa5a
commit
b8f7793773
406 files changed
+15803
-587
No files matched your search
@@ -0,0 +1,184 @@
|
||||
// Site-wide file transfer manager for qualityiron.vegas - loaded once
|
||||
// via mainpagetemplate.jet (outside #content) so the floating panel and
|
||||
// any in-progress transfer survive spa.js's own #content-only swaps.
|
||||
// Adapted from barant.com's own documents.js (same proven mechanism),
|
||||
// just pointed at qualityiron's own /jobdocs/* routes instead of
|
||||
// barant.com's /documents/*.
|
||||
(function() {
|
||||
var panel = document.getElementById('filemgr-panel');
|
||||
if (!panel)
|
||||
return;
|
||||
|
||||
var toggle = document.getElementById('filemgr-toggle');
|
||||
var badge = document.getElementById('filemgr-badge');
|
||||
var list = document.getElementById('filemgr-list');
|
||||
var clearBtn = document.getElementById('filemgr-clear');
|
||||
|
||||
var transfers = {};
|
||||
var nextId = 1;
|
||||
|
||||
function escapeHtml(s) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = s == null ? '' : String(s);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function updateBadge() {
|
||||
var activeCount = 0;
|
||||
for (var k in transfers) {
|
||||
if (transfers[k].status === 'active')
|
||||
activeCount++;
|
||||
}
|
||||
badge.textContent = activeCount > 0 ? String(activeCount) : '';
|
||||
}
|
||||
|
||||
function removeEmptyPlaceholder() {
|
||||
var empty = document.getElementById('filemgr-empty');
|
||||
if (empty)
|
||||
empty.remove();
|
||||
}
|
||||
|
||||
function showEmptyPlaceholderIfNeeded() {
|
||||
if (Object.keys(transfers).length === 0 && !document.getElementById('filemgr-empty'))
|
||||
list.innerHTML = '<div id="filemgr-empty">No transfers yet.</div>';
|
||||
}
|
||||
|
||||
var UPLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/></svg>';
|
||||
var DOWNLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v13M5 12l7 7 7-7"/></svg>';
|
||||
|
||||
function addTransfer(name, direction) {
|
||||
var id = 'xfer' + (nextId++);
|
||||
removeEmptyPlaceholder();
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'filemgr-item';
|
||||
el.innerHTML =
|
||||
'<div class="filemgr-item-row">' +
|
||||
'<span class="filemgr-item-icon">' + (direction === 'upload' ? UPLOAD_ICON : DOWNLOAD_ICON) + '</span>' +
|
||||
'<span class="filemgr-item-name">' + escapeHtml(name) + '</span>' +
|
||||
'<span class="filemgr-item-status">0%</span>' +
|
||||
'</div>' +
|
||||
'<div class="filemgr-progress-track"><div class="filemgr-progress-fill"></div></div>';
|
||||
list.insertBefore(el, list.firstChild);
|
||||
|
||||
transfers[id] = { el: el, status: 'active' };
|
||||
panel.classList.add('filemgr-expanded');
|
||||
updateBadge();
|
||||
return id;
|
||||
}
|
||||
|
||||
function updateProgress(id, percent) {
|
||||
var t = transfers[id];
|
||||
if (!t)
|
||||
return;
|
||||
t.el.querySelector('.filemgr-progress-fill').style.width = percent + '%';
|
||||
t.el.querySelector('.filemgr-item-status').textContent = percent + '%';
|
||||
}
|
||||
|
||||
function markDone(id, statusText) {
|
||||
var t = transfers[id];
|
||||
if (!t)
|
||||
return;
|
||||
t.status = 'done';
|
||||
t.el.classList.add('filemgr-done');
|
||||
t.el.querySelector('.filemgr-progress-fill').style.width = '100%';
|
||||
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Done';
|
||||
updateBadge();
|
||||
}
|
||||
|
||||
function markFailed(id, statusText) {
|
||||
var t = transfers[id];
|
||||
if (!t)
|
||||
return;
|
||||
t.status = 'failed';
|
||||
t.el.classList.add('filemgr-failed');
|
||||
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Failed';
|
||||
updateBadge();
|
||||
}
|
||||
|
||||
// Uploads a FileList/array of File objects, each as its own
|
||||
// concurrent XHR request, tracked here as separate panel entries.
|
||||
// job_id/subtype_id come from data attributes on the dropzone element
|
||||
// itself (set by jobdocs.page per the job/category being uploaded
|
||||
// into) rather than being hardcoded here, since this same script is
|
||||
// shared across every job's documents page.
|
||||
window.fileManagerUpload = function(files, jobId, subtypeId) {
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
(function(file) {
|
||||
var id = addTransfer(file.name, 'upload');
|
||||
var xhr = new XMLHttpRequest();
|
||||
transfers[id].xhr = xhr;
|
||||
xhr.open('POST', '/jobdocs/api/upload');
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable)
|
||||
updateProgress(id, Math.round((e.loaded / e.total) * 100));
|
||||
});
|
||||
xhr.addEventListener('load', function() {
|
||||
var res = null;
|
||||
try { res = JSON.parse(xhr.responseText); } catch (e) {}
|
||||
if (xhr.status === 200 && res && res.ok) {
|
||||
markDone(id);
|
||||
document.dispatchEvent(new CustomEvent('jobdocs:changed'));
|
||||
} else {
|
||||
markFailed(id, (res && res.error) || 'Failed');
|
||||
}
|
||||
});
|
||||
xhr.addEventListener('error', function() { markFailed(id); });
|
||||
var fd = new FormData();
|
||||
fd.append('name', file.name);
|
||||
fd.append('job_id', jobId);
|
||||
fd.append('subtype_id', subtypeId);
|
||||
fd.append('mimetype', file.type || 'application/octet-stream');
|
||||
fd.append('file_size', file.size);
|
||||
fd.append('filedata', file);
|
||||
xhr.send(fd);
|
||||
})(files[i]);
|
||||
}
|
||||
};
|
||||
|
||||
// Downloads a document by id via XHR (responseType 'blob') so it gets
|
||||
// real tracked progress, then hands the bytes to the browser's normal
|
||||
// save flow via a Blob URL + synthetic <a download> click.
|
||||
window.fileManagerDownload = function(id, name) {
|
||||
var xferId = addTransfer(name, 'download');
|
||||
var xhr = new XMLHttpRequest();
|
||||
transfers[xferId].xhr = xhr;
|
||||
xhr.open('GET', '/jobdocs/download?id=' + encodeURIComponent(id));
|
||||
xhr.responseType = 'blob';
|
||||
xhr.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable)
|
||||
updateProgress(xferId, Math.round((e.loaded / e.total) * 100));
|
||||
});
|
||||
xhr.addEventListener('load', function() {
|
||||
if (xhr.status !== 200) {
|
||||
markFailed(xferId);
|
||||
return;
|
||||
}
|
||||
var blobUrl = URL.createObjectURL(xhr.response);
|
||||
var a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 30000);
|
||||
markDone(xferId);
|
||||
});
|
||||
xhr.addEventListener('error', function() { markFailed(xferId); });
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
toggle.addEventListener('click', function() {
|
||||
panel.classList.toggle('filemgr-expanded');
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', function() {
|
||||
for (var k in transfers) {
|
||||
if (transfers[k].status !== 'active') {
|
||||
transfers[k].el.remove();
|
||||
delete transfers[k];
|
||||
}
|
||||
}
|
||||
showEmptyPlaceholderIfNeeded();
|
||||
});
|
||||
})();
|
||||
Reference in new issue
Block a user