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
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 510 KiB |
@@ -43,6 +43,13 @@
|
||||
pageNext:
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<path d="M9 18l6-6-6-6"/>' +
|
||||
'</svg>',
|
||||
emptyFolder:
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<path d="M4 7h16"/>' +
|
||||
'<path d="M9 7V4h6v3"/>' +
|
||||
'<path d="M6 7l1 13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-13"/>' +
|
||||
'<path d="M9.5 12.5l5 5M14.5 12.5l-5 5"/>' +
|
||||
'</svg>'
|
||||
};
|
||||
|
||||
@@ -109,6 +116,15 @@
|
||||
var links = document.querySelectorAll('.email-folder-link');
|
||||
for (var i = 0; i < links.length; i++)
|
||||
links[i].classList.toggle('email-folder-selected', links[i].getAttribute('data-folder') === currentFolder);
|
||||
// Only Spam/Trash ever offer this - emptying Inbox (or any future
|
||||
// folder) as a single destructive click isn't something any real
|
||||
// mail client exposes, and the server enforces the exact same
|
||||
// Spam/Trash-only allowlist independently (see email.jet) - this is
|
||||
// just keeping the button from ever being visible somewhere it
|
||||
// would 400 anyway, not the actual safety boundary.
|
||||
var emptyBtn = document.getElementById('email-empty-folder-btn');
|
||||
if (emptyBtn)
|
||||
emptyBtn.hidden = (currentFolder !== 'Spam' && currentFolder !== 'Trash');
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
@@ -158,6 +174,7 @@
|
||||
'<button type="button" id="email-search-btn" class="icon-btn" title="Search">' + ICONS.search + '</button>' +
|
||||
'</span>' +
|
||||
'<button type="button" id="email-compose-btn" class="icon-btn" title="New Mail">' + ICONS.compose + '</button>' +
|
||||
'<button type="button" id="email-empty-folder-btn" class="icon-btn" title="Permanently empty this folder" hidden>' + ICONS.emptyFolder + '</button>' +
|
||||
'<button type="button" id="email-refresh-btn" class="icon-btn" title="Refresh">' + ICONS.refresh + '</button>' +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
@@ -218,6 +235,7 @@
|
||||
document.getElementById('email-refresh-btn').addEventListener('click', function() {
|
||||
loadMessageList();
|
||||
});
|
||||
document.getElementById('email-empty-folder-btn').addEventListener('click', emptyCurrentFolder);
|
||||
|
||||
// Matches sender, subject, or body content in whatever folder is
|
||||
// currently open (see __imap.cpp's searchMessages()) - an empty
|
||||
@@ -444,6 +462,38 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Permanently deletes every message in the CURRENT folder (only ever
|
||||
// reachable while viewing Spam or Trash - see syncActiveFolder()'s own
|
||||
// button-visibility gate, and email.jet's independent server-side
|
||||
// allowlist, which is the real enforcement). Unlike trashMessage()
|
||||
// there's no undo, no "move somewhere" - a real, human-readable confirm()
|
||||
// is the whole safety net here, matching how Gmail/Outlook both gate
|
||||
// "Empty Trash now" behind exactly one extra click and nothing fancier.
|
||||
function emptyCurrentFolder() {
|
||||
if (currentFolder !== 'Spam' && currentFolder !== 'Trash')
|
||||
return;
|
||||
if (!window.confirm('Permanently delete every message in ' + currentFolder + '? This cannot be undone.'))
|
||||
return;
|
||||
var btn = document.getElementById('email-empty-folder-btn');
|
||||
if (btn) btn.disabled = true;
|
||||
post('/email/emptyfolder', { folder: currentFolder })
|
||||
.then(function(res) {
|
||||
if (btn) btn.disabled = false;
|
||||
if (!res.ok) {
|
||||
window.alert(res.error || 'Could not empty the folder.');
|
||||
return;
|
||||
}
|
||||
currentUid = null;
|
||||
document.getElementById('email-reading-pane').innerHTML =
|
||||
'<div id="email-empty-state">Select a message to read it.</div>';
|
||||
loadMessageList();
|
||||
})
|
||||
.catch(function() {
|
||||
if (btn) btn.disabled = false;
|
||||
window.alert('Empty-folder request failed.');
|
||||
});
|
||||
}
|
||||
|
||||
// Same fade-out/fetch-concurrently/fade-in shape as openMessage() and
|
||||
// spa.js's own navigate() - every list refresh (folder switch,
|
||||
// refresh button) gets the same smooth transition instead of a flash
|
||||
@@ -488,8 +538,10 @@
|
||||
if (!skipFade)
|
||||
list.classList.remove('email-fading');
|
||||
})
|
||||
.catch(function() {
|
||||
list.innerHTML = '<div id="email-empty-state">Could not load messages.</div>';
|
||||
.catch(function(err) {
|
||||
console.error('loadMessageList failed:', err);
|
||||
list.innerHTML = '<div id="email-empty-state">Could not load messages' +
|
||||
(err && err.message ? ': ' + escapeHtml(err.message) : '.') + '</div>';
|
||||
if (!skipFade)
|
||||
list.classList.remove('email-fading');
|
||||
});
|
||||
@@ -506,8 +558,10 @@
|
||||
// cosmetic problem, not worth surfacing as a user-visible failure.
|
||||
function loadFolderCounts() {
|
||||
post('/email/folders', {}).then(function(res) {
|
||||
if (res.error)
|
||||
if (res.error) {
|
||||
console.error('loadFolderCounts server error:', res.error);
|
||||
return;
|
||||
}
|
||||
var folders = ['Inbox', 'Spam', 'Trash'];
|
||||
for (var i = 0; i < folders.length; i++) {
|
||||
var f = folders[i];
|
||||
@@ -515,7 +569,9 @@
|
||||
if (span && res[f])
|
||||
span.textContent = res[f].messages > 0 ? (res[f].unseen + '/' + res[f].messages) : '';
|
||||
}
|
||||
}).catch(function() {});
|
||||
}).catch(function(err) {
|
||||
console.error('loadFolderCounts failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function loadAccounts() {
|
||||
@@ -756,8 +812,10 @@
|
||||
}
|
||||
pane.classList.remove('email-fading');
|
||||
})
|
||||
.catch(function() {
|
||||
pane.innerHTML = '<div id="email-empty-state">Could not load message.</div>';
|
||||
.catch(function(err) {
|
||||
console.error('openMessage failed:', err);
|
||||
pane.innerHTML = '<div id="email-empty-state">Could not load message' +
|
||||
(err && err.message ? ': ' + escapeHtml(err.message) : '.') + '</div>';
|
||||
pane.classList.remove('email-fading');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
|
||||
@font-face {
|
||||
font-family: avenir;
|
||||
src: url('https://barant.com/fonts/AvenirLTStd-Light.ttf');
|
||||
src: url('https://barant.com/assets/AvenirLTStd-Light.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: source;
|
||||
src: url('https://barant.com/fonts/jackinput.TTF');
|
||||
src: url('https://barant.com/assets/jackinput.TTF');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: exo2;
|
||||
src: url('https://barant.com/fonts/Exo2-RegularCondensed.otf');
|
||||
src: url('https://barant.com/assets/Exo2-RegularCondensed.otf');
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -283,7 +283,7 @@ textarea.fieldinputerror {
|
||||
background: linear-gradient(180deg, #131318, #0d0d10);
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
border-bottom: 1px solid var(--bond-line);
|
||||
max-height: 80px;
|
||||
max-height: 125px;
|
||||
}
|
||||
.entryform {
|
||||
background: #232330;
|
||||
@@ -365,24 +365,43 @@ textarea.fieldinputerror {
|
||||
transition: color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* The currently-active entry in a page's own sidebar submenu (e.g.
|
||||
opensource.menu's list of subpages) - a background highlight rather
|
||||
than baritemselected's "raised tab" treatment, since these are plain
|
||||
inline links stacked in a list, not tabs sitting on a bar edge.
|
||||
Applied server-side (see opensource.menu) by comparing each
|
||||
linkbutton's own subpage against $[SUBPAGE] - spa.js already
|
||||
re-renders and swaps this menu's markup on every client-side
|
||||
navigation (see its #_spa_panelmenu handling), so this stays correct
|
||||
without any script of its own. */
|
||||
/* Every entry in a page's own sidebar submenu (e.g. opensource.menu's
|
||||
list of subpages) shares this same box model - selected and
|
||||
unselected alike - so the only thing that changes when an item
|
||||
becomes active is its background/border, never its box size or the
|
||||
position of its own text. Giving only the active item padding/margin
|
||||
(the original approach) made its text visibly shift the moment it
|
||||
was selected, since the unselected items sat flush with none. */
|
||||
#panelmenu a {
|
||||
display: block;
|
||||
padding: 2px 6px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* The currently-active entry - a background highlight rather than
|
||||
baritemselected's "raised tab" treatment, since these are plain
|
||||
links stacked in a list, not tabs sitting on a bar edge. Applied
|
||||
server-side (see opensource.menu) by comparing each linkbutton's own
|
||||
subpage against $[SUBPAGE] - spa.js also applies it instantly client-
|
||||
side on click (see its #panelmenu click listener) and then
|
||||
re-confirms it from the server's own #_spa_panelmenu markup once the
|
||||
fetch resolves, so this stays correct either way. */
|
||||
.submenuitem-active {
|
||||
background: var(--bond-panel-2);
|
||||
color: var(--bond-gold-bright) !important;
|
||||
padding: 2px 6px;
|
||||
margin: -2px -6px;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 0 1px var(--bond-gold);
|
||||
}
|
||||
|
||||
/* Every menu entry's own markup is <linkbutton...></linkbutton><br> - that
|
||||
<br> was needed to force a line break after an inline link, but every
|
||||
entry is display:block now (its own implicit line break), so the
|
||||
trailing <br> doubles up into a blank line for every item, not just
|
||||
the active one. */
|
||||
#panelmenu a + br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--bond-panel);
|
||||
margin: 10px;
|
||||
@@ -861,3 +880,499 @@ textarea.fieldinputerror {
|
||||
.filemgr-progress-fill { height: 100%; width: 0%; background: var(--bond-gold-bright); transition: width 0.15s ease; }
|
||||
.filemgr-item.filemgr-done .filemgr-progress-fill { background: #6c6; }
|
||||
.filemgr-item.filemgr-failed .filemgr-progress-fill { background: #c66; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Fades in during an SPA navigation fetch - lives outside #content (a
|
||||
content.innerHTML swap would just wipe it out otherwise), positioned
|
||||
relative to the viewport rather than the resizable content column so
|
||||
dragging #panelmenu-resizer narrower/wider never throws its centering
|
||||
off. The transition duration alone (no separate timer) is what keeps
|
||||
a fast load from ever showing it at a noticeable opacity before the
|
||||
class comes back off. */
|
||||
.spa-loading-indicator {
|
||||
position: fixed;
|
||||
top: 90px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--bond-text-dim);
|
||||
font-size: 10pt;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
pointer-events: none;
|
||||
z-index: 500;
|
||||
}
|
||||
.spa-loading-indicator.spa-loading-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Fades in during an SPA navigation fetch - lives outside #content (a
|
||||
content.innerHTML swap would just wipe it out otherwise), positioned
|
||||
relative to the viewport rather than the resizable content column so
|
||||
dragging #panelmenu-resizer narrower/wider never throws its centering
|
||||
off. The transition duration alone (no separate timer) is what keeps
|
||||
a fast load from ever showing it at a noticeable opacity before the
|
||||
class comes back off. */
|
||||
.spa-loading-indicator {
|
||||
position: fixed;
|
||||
top: 90px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--bond-text-dim);
|
||||
font-size: 10pt;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
pointer-events: none;
|
||||
z-index: 500;
|
||||
}
|
||||
.spa-loading-indicator.spa-loading-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Keeps the music/library/ianslibrary subpages pinned to the browser
|
||||
window's height (footer always visible, no whole-page scroll) while
|
||||
the song list itself scrolls internally - the exact same pattern
|
||||
already proven for the email app's own message list
|
||||
(.pageclass:has(#email-app)), just scoped to this marker instead and
|
||||
adapted for <sectionframe>'s own heading-then-contents structure
|
||||
(the heading needs to stay a fixed size above the scrolling list,
|
||||
not get swept into the same scroll region). height:100% has to be
|
||||
set explicitly at every level down to .music-song-list itself -
|
||||
percentage heights only resolve against an ancestor with its own
|
||||
definite height, and a plain block element's default is auto, which
|
||||
does not count, so skipping any one level here would silently break
|
||||
the whole chain. #main-subpage (main.page's generic per-subpage
|
||||
wrapper, between #content and .sectionframe) is exactly that kind of
|
||||
skip - missing it let .sectionframe's height:100% resolve to auto,
|
||||
so .music-song-list sized itself to its full content height instead
|
||||
of a scrollable box, and the overflow got clipped by .pageclass's
|
||||
own overflow:hidden with no scrollbar to show for it. */
|
||||
.pageclass:has(.music-song-list) {
|
||||
height: calc(100vh - 150px);
|
||||
overflow: hidden;
|
||||
}
|
||||
#content:has(.music-song-list) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#main-subpage:has(.music-song-list) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sectionframe:has(.music-song-list) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.sectionframe:has(.music-song-list) .sectionframecontents {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.music-song-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Fades in during an SPA navigation fetch - lives outside #content (a
|
||||
content.innerHTML swap would just wipe it out otherwise), positioned
|
||||
relative to the viewport rather than the resizable content column so
|
||||
dragging #panelmenu-resizer narrower/wider never throws its centering
|
||||
off. The transition duration alone (no separate timer) is what keeps
|
||||
a fast load from ever showing it at a noticeable opacity before the
|
||||
class comes back off. */
|
||||
.spa-loading-indicator {
|
||||
position: fixed;
|
||||
top: 90px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--bond-text-dim);
|
||||
font-size: 10pt;
|
||||
letter-spacing: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease;
|
||||
pointer-events: none;
|
||||
z-index: 500;
|
||||
}
|
||||
.spa-loading-indicator.spa-loading-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Global persistent music player band - slides open only while playing */
|
||||
.globalplayer-band {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--bond-panel);
|
||||
border-bottom: 1pt solid var(--bond-line);
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
}
|
||||
.globalplayer-band.globalplayer-open {
|
||||
max-height: 54px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.globalplayer-title {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
/* A flex item's default min-width is its content's own min-content
|
||||
size, not 0 - without this, a long song name would force the whole
|
||||
LCD panel wider (squeezing the volume slider/buttons) rather than
|
||||
actually truncating against the space it is given. */
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0 0 8px rgba(26,143,196,0.12), inset 0 2px 4px rgba(0,0,0,0.8);
|
||||
}
|
||||
.globalplayer-duration {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 10pt;
|
||||
color: #1a8fc4;
|
||||
text-shadow: 0 0 4px rgba(26,143,196,0.7);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.globalplayer-random-indicator {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 8px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 7pt;
|
||||
letter-spacing: 1px;
|
||||
color: #1a8fc4;
|
||||
text-shadow: 0 0 4px rgba(26,143,196,0.7);
|
||||
opacity: 0.85;
|
||||
display: none;
|
||||
}
|
||||
.globalplayer-random-indicator.globalplayer-random-indicator-visible {
|
||||
display: block;
|
||||
}
|
||||
.globalplayer-title-text {
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 20pt;
|
||||
line-height: 1.1;
|
||||
color: #1a8fc4;
|
||||
text-shadow: 0 0 5px rgba(26,143,196,0.8), 0 0 1px rgba(26,143,196,0.9);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.globalplayer-progress {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
background: rgba(26,143,196,0.15);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
.globalplayer-progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: #1a8fc4;
|
||||
border-radius: 4px 0 0 4px;
|
||||
box-shadow: 0 0 4px rgba(26,143,196,0.8);
|
||||
transition: width 0.2s linear;
|
||||
pointer-events: none;
|
||||
}
|
||||
.globalplayer-progress-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 30%, #fff6d0 0%, #d4af37 55%, #6b5313 100%);
|
||||
border: 1px solid #5a4410;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.7);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: left 0.2s linear;
|
||||
pointer-events: none;
|
||||
}
|
||||
.globalplayer-progress.globalplayer-progress-dragging .globalplayer-progress-fill,
|
||||
.globalplayer-progress.globalplayer-progress-dragging .globalplayer-progress-handle {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Square, touch-tone-style buttons for the player band - same dished
|
||||
gold-on-black look as the sipphone tag's dialpad keys. */
|
||||
.globalplayer-btn {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: radial-gradient(circle at 50% 50%, #8a6a22 0%, #b8903a 32%, #d4af37 55%, #ecce79 80%, #fff6d0 100%);
|
||||
border: 1px solid #5a4410;
|
||||
box-shadow:
|
||||
0 2px 0 #4a3a0f,
|
||||
0 3px 5px rgba(0,0,0,0.5),
|
||||
inset 0 3px 5px rgba(0,0,0,0.45),
|
||||
inset 0 -1px 1px rgba(255,255,255,0.5);
|
||||
color: #3a2d05;
|
||||
cursor: pointer;
|
||||
transition: transform 0.06s ease, box-shadow 0.06s ease;
|
||||
}
|
||||
.globalplayer-btn svg { width: 16px; height: 16px; }
|
||||
.globalplayer-btn:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow:
|
||||
inset 0 4px 8px rgba(0,0,0,0.6),
|
||||
inset 0 -1px 1px rgba(255,255,255,0.3);
|
||||
}
|
||||
.globalplayer-btn-text {
|
||||
width: auto;
|
||||
padding: 0 12px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Stereo FFT-style bar graph - left channel above the midline, right
|
||||
channel below it, drawn directly onto a small canvas. */
|
||||
.globalplayer-eq {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
height: 32px;
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Playlist dropdown - its own independent sliding strip directly below
|
||||
the player band, NOT nested inside it: the band's own overflow:hidden
|
||||
(needed for its slide animation) would otherwise clip anything
|
||||
positioned to pop out below it, which is why this was invisible
|
||||
before despite the open/close class toggling correctly in JS. */
|
||||
.globalplayer-queue {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bond-panel-2);
|
||||
border-bottom: 1pt solid var(--bond-line);
|
||||
transition: max-height 0.25s ease;
|
||||
}
|
||||
.globalplayer-queue.globalplayer-queue-open {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.globalplayer-queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 6px 10px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--bond-line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
color: var(--bond-text-dim);
|
||||
}
|
||||
.globalplayer-queue-name {
|
||||
font-size: 18pt;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.globalplayer-queue-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.globalplayer-queue-handle {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--bond-text-dim);
|
||||
cursor: grab;
|
||||
}
|
||||
.globalplayer-queue-handle svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.globalplayer-queue-item.globalplayer-queue-dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.globalplayer-queue-item.globalplayer-queue-dragover {
|
||||
border-top: 2px solid var(--bond-gold-bright);
|
||||
}
|
||||
.globalplayer-queue-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.globalplayer-queue-duration {
|
||||
flex-shrink: 0;
|
||||
font-size: 9pt;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.globalplayer-queue-remove {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--bond-line);
|
||||
background: linear-gradient(180deg, #1c1c24, #131318);
|
||||
color: var(--bond-text-dim);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
.globalplayer-queue-remove svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: block;
|
||||
}
|
||||
.globalplayer-queue-remove:hover {
|
||||
border-color: var(--bond-red);
|
||||
color: var(--bond-red);
|
||||
box-shadow: 0 0 0.5em rgba(161,58,58,0.35);
|
||||
background: linear-gradient(180deg, #24242e, #1a1a20);
|
||||
}
|
||||
.globalplayer-queue-item.globalplayer-queue-current {
|
||||
color: var(--bond-gold-bright);
|
||||
font-weight: 600;
|
||||
border-color: var(--bond-gold);
|
||||
background: rgba(212,175,55,0.08);
|
||||
}
|
||||
|
||||
/* Tactile feedback for clicking a song to queue it: an immediate flash
|
||||
on the row, plus a lasting checkmark so it is clear at a glance which
|
||||
songs are already in the queue. */
|
||||
/* Scoped to the name span, not the row itself: the row is a flex
|
||||
container with justify-content:space-between, and a ::before on it
|
||||
directly becomes its own flex item - with 3 items instead of 2, the
|
||||
name ends up looking centered between the checkmark and the duration/
|
||||
remove group instead of staying left-aligned. Putting it on the name
|
||||
span keeps it as ordinary inline content inside that one flex item. */
|
||||
.song-queued .music-song-name::before {
|
||||
content: "\2713 ";
|
||||
color: var(--bond-gold-bright);
|
||||
}
|
||||
.song-queued-flash {
|
||||
animation: songQueuedFlash 0.6s ease;
|
||||
}
|
||||
@keyframes songQueuedFlash {
|
||||
0% { background: var(--bond-gold-bright); color: var(--bond-bg); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
|
||||
/* Song-picker rows (music/jukebox subpages) - same boxed, rounded-corner
|
||||
look as the playlist dropdown's own entries, for visual consistency
|
||||
between "songs you can pick" and "what's in the queue". */
|
||||
.music-song-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 4px 0;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--bond-line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
color: var(--bond-text-dim);
|
||||
cursor: pointer;
|
||||
}
|
||||
.music-song-row:hover {
|
||||
border-color: var(--bond-gold);
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
.music-song-name {
|
||||
font-size: 12pt;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.music-song-duration {
|
||||
flex-shrink: 0;
|
||||
font-size: 8pt;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* Volume slider - a small gold knob riding a thin gold-tinted track */
|
||||
.globalplayer-volume {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
flex-shrink: 0;
|
||||
width: 70px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--bond-line);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.globalplayer-volume::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 30%, #fff6d0 0%, #d4af37 55%, #6b5313 100%);
|
||||
border: 1px solid #5a4410;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.globalplayer-volume::-moz-range-track {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--bond-line);
|
||||
}
|
||||
.globalplayer-volume::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 30%, #fff6d0 0%, #d4af37 55%, #6b5313 100%);
|
||||
border: 1px solid #5a4410;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.6);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
(function() {
|
||||
var content = document.getElementById('content');
|
||||
var form = document.getElementById('mainform');
|
||||
var loadingIndicator = document.getElementById('spa-loading-indicator');
|
||||
// Lives outside #content (a sibling in the persistent shell, next to
|
||||
// the menu bar/form) - a fragment swap never touches it on its own,
|
||||
// so a per-page side menu would otherwise stay frozen on whichever
|
||||
@@ -220,6 +221,12 @@
|
||||
function navigate(url, push) {
|
||||
var sep = url.indexOf('?') === -1 ? '?' : '&';
|
||||
content.classList.add('spa-fading');
|
||||
// Opacity transition alone (see the CSS) means a fetch that resolves
|
||||
// quickly never gets the indicator past a barely-visible opacity
|
||||
// before this removes the class again - exactly the "do not flash
|
||||
// this for a fast load" behavior wanted, with no separate delay
|
||||
// timer needed to achieve it.
|
||||
if (loadingIndicator) loadingIndicator.classList.add('spa-loading-visible');
|
||||
Promise.all([
|
||||
fetch(url + sep + '_fragment=1', { credentials: 'same-origin' }).then(function(r) { return r.text(); }),
|
||||
delay(180)
|
||||
@@ -233,6 +240,7 @@
|
||||
syncActiveTab(finalUrl.split('?')[0]);
|
||||
}
|
||||
content.classList.remove('spa-fading');
|
||||
if (loadingIndicator) loadingIndicator.classList.remove('spa-loading-visible');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,6 +262,7 @@
|
||||
var subpage = fd.get('SUBPAGE') || '';
|
||||
var url = formPrefix + '/' + page + (subpage ? '/' + subpage : '');
|
||||
content.classList.add('spa-fading');
|
||||
if (loadingIndicator) loadingIndicator.classList.add('spa-loading-visible');
|
||||
Promise.all([
|
||||
fetch(url + '?_fragment=1', {
|
||||
method: 'POST',
|
||||
@@ -271,6 +280,7 @@
|
||||
syncActiveTab(finalUrl.split('?')[0]);
|
||||
}
|
||||
content.classList.remove('spa-fading');
|
||||
if (loadingIndicator) loadingIndicator.classList.remove('spa-loading-visible');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -279,6 +289,24 @@
|
||||
navigate(window.location.pathname + window.location.search, false);
|
||||
});
|
||||
|
||||
// Instant visual feedback on a side-menu click - without this, the
|
||||
// active-item highlight only updated once applyResponse() swapped in
|
||||
// the new #_spa_panelmenu markup after the fetch finished, which read
|
||||
// as unresponsive (the click did nothing visible until the whole
|
||||
// subpage had already loaded). That server-rendered swap is still the
|
||||
// authoritative source of truth and will correct this on arrival (e.g.
|
||||
// if a click ends up redirected somewhere else entirely), so this is
|
||||
// purely an optimistic bridge, not a second source of truth.
|
||||
if (panelMenu) {
|
||||
panelMenu.addEventListener('click', function(e) {
|
||||
var a = e.target.closest('a');
|
||||
if (!a || !panelMenu.contains(a)) return;
|
||||
var current = panelMenu.querySelector('.submenuitem-active');
|
||||
if (current) current.classList.remove('submenuitem-active');
|
||||
a.classList.add('submenuitem-active');
|
||||
});
|
||||
}
|
||||
|
||||
// Drag-to-resize for the side menu (#panelmenu) against the main
|
||||
// content column - the resizer bar (#panelmenu-resizer) lives in the
|
||||
// persistent shell right alongside panelMenu itself (see its own
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
<tag name="sipphone">
|
||||
|
||||
<style>
|
||||
.sipphone-bond {
|
||||
width: 360px;
|
||||
margin: 10px 0 24px 0;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(160deg, #1a1a1a 0%, #060606 60%, #000 100%);
|
||||
border: 1px solid #000;
|
||||
box-shadow:
|
||||
0 0 0 2px #2a2a2a,
|
||||
0 0 0 3px #b8860b,
|
||||
0 10px 30px rgba(0,0,0,0.6),
|
||||
inset 0 1px 0 rgba(255,255,255,0.05);
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
color: #d4af37;
|
||||
}
|
||||
.sipphone-bond .sipphone-brand {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
letter-spacing: 4px;
|
||||
color: #d4af37;
|
||||
text-shadow: 0 0 6px rgba(212,175,55,0.6);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.sipphone-bond .sipphone-lcd {
|
||||
background: #000;
|
||||
border: 2px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 10px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
color: #39ff6a;
|
||||
text-shadow: 0 0 5px rgba(57,255,106,0.8), 0 0 1px rgba(57,255,106,0.9);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
min-height: 56px;
|
||||
box-shadow: inset 0 0 12px rgba(57,255,106,0.15), inset 0 2px 6px rgba(0,0,0,0.8);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sipphone-bond .sipphone-lcd::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0.15) 0px,
|
||||
rgba(0,0,0,0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
.sipphone-bond .sipphone-lcd-line1 { font-size: 30px; font-weight: bold; line-height: 1.2; }
|
||||
.sipphone-bond .sipphone-lcd-line2 { opacity: 0.85; }
|
||||
.sipphone-bond .sipphone-video-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
.sipphone-bond .sipphone-video-wrap.active { display: block; }
|
||||
.sipphone-bond .sipphone-remote-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: #000;
|
||||
}
|
||||
.sipphone-bond .sipphone-local-video {
|
||||
position: absolute;
|
||||
width: 72px;
|
||||
height: 54px;
|
||||
right: 6px;
|
||||
bottom: 6px;
|
||||
border: 1px solid #d4af37;
|
||||
border-radius: 3px;
|
||||
object-fit: cover;
|
||||
background: #111;
|
||||
}
|
||||
.sipphone-bond .sipphone-dialpad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 44px);
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sipphone-bond .sipphone-key {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #3a2d05;
|
||||
background: radial-gradient(circle at 50% 50%, #8a6a22 0%, #b8903a 32%, #d4af37 55%, #ecce79 80%, #fff6d0 100%);
|
||||
border: 1px solid #5a4410;
|
||||
box-shadow:
|
||||
0 2px 0 #4a3a0f,
|
||||
0 4px 6px rgba(0,0,0,0.6),
|
||||
inset 0 3px 5px rgba(0,0,0,0.45),
|
||||
inset 0 -1px 1px rgba(255,255,255,0.5);
|
||||
transition: transform 0.06s ease, box-shadow 0.06s ease;
|
||||
}
|
||||
.sipphone-bond .sipphone-key:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow:
|
||||
inset 0 4px 8px rgba(0,0,0,0.6),
|
||||
inset 0 -1px 1px rgba(255,255,255,0.3);
|
||||
}
|
||||
.sipphone-bond .sipphone-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.sipphone-bond .sipphone-btn {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
padding: 9px 0;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
color: #3a2d05;
|
||||
background: linear-gradient(180deg, #f3d77a 0%, #d4af37 45%, #9c7a1f 100%);
|
||||
border: 1px solid #5a4410;
|
||||
box-shadow: 0 2px 0 #4a3a0f, 0 4px 8px rgba(0,0,0,0.6), inset 0 1px 1px rgba(255,255,255,0.7);
|
||||
}
|
||||
.sipphone-bond .sipphone-btn.sipphone-hangup {
|
||||
background: linear-gradient(180deg, #e06666 0%, #a30000 60%, #6b0000 100%);
|
||||
color: #2a0000;
|
||||
border-color: #3a0000;
|
||||
}
|
||||
.sipphone-bond .sipphone-btn:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
||||
}
|
||||
.sipphone-bond .sipphone-btn.sipphone-btn-disabled {
|
||||
cursor: default;
|
||||
opacity: 0.35;
|
||||
filter: grayscale(70%);
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sipphone-bond .sipphone-lcd-status-offline {
|
||||
color: #ff3b3b;
|
||||
text-shadow: 0 0 6px rgba(255,59,59,0.8);
|
||||
}
|
||||
.sipphone-bond .sipphone-status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
background: #666;
|
||||
box-shadow: 0 0 4px #666;
|
||||
}
|
||||
.sipphone-bond .sipphone-status-dot.reg {
|
||||
background: #39ff6a;
|
||||
box-shadow: 0 0 6px #39ff6a;
|
||||
}
|
||||
.sipphone-bond .sipphone-status-dot.incall {
|
||||
background: #ffcc00;
|
||||
box-shadow: 0 0 6px #ffcc00;
|
||||
}
|
||||
</style>
|
||||
|
||||
<sql>select id, exten, secret from phones.extensions where user_id=$[_login_id] and secret is not null</sql>
|
||||
<ifrow>
|
||||
<whilerow>
|
||||
<div class="sipphone-bond" id="sipphone-$[.id]"
|
||||
data-sipphone-id="$[.id]"
|
||||
data-sipphone-exten="$[.exten]"
|
||||
data-sipphone-secret="$[.secret]"
|
||||
data-sipphone-domain="barant.com"
|
||||
data-sipphone-wss="wss://barant.com:8089/ws">
|
||||
|
||||
<div class="sipphone-brand">◆ B · A · R · A · N · T ◆</div>
|
||||
|
||||
<div class="sipphone-lcd">
|
||||
<div class="sipphone-lcd-line1" data-role="lcd-line1">$[.exten]</div>
|
||||
<div class="sipphone-lcd-line2" data-role="lcd-line2"><span class="sipphone-status-dot" data-role="status-dot"></span><span data-role="lcd-status">OFFLINE</span></div>
|
||||
</div>
|
||||
|
||||
<div class="sipphone-video-wrap" data-role="video-wrap">
|
||||
<video class="sipphone-remote-video" data-role="remote-video" autoplay playsinline></video>
|
||||
<video class="sipphone-local-video" data-role="local-video" autoplay playsinline muted></video>
|
||||
</div>
|
||||
|
||||
<div class="sipphone-dialpad" data-role="dialpad">
|
||||
<div class="sipphone-key" data-digit="1">1</div>
|
||||
<div class="sipphone-key" data-digit="2">2</div>
|
||||
<div class="sipphone-key" data-digit="3">3</div>
|
||||
<div class="sipphone-key" data-digit="4">4</div>
|
||||
<div class="sipphone-key" data-digit="5">5</div>
|
||||
<div class="sipphone-key" data-digit="6">6</div>
|
||||
<div class="sipphone-key" data-digit="7">7</div>
|
||||
<div class="sipphone-key" data-digit="8">8</div>
|
||||
<div class="sipphone-key" data-digit="9">9</div>
|
||||
<div class="sipphone-key" data-digit="*">*</div>
|
||||
<div class="sipphone-key" data-digit="0">0</div>
|
||||
<div class="sipphone-key" data-digit="#">#</div>
|
||||
</div>
|
||||
|
||||
<div class="sipphone-controls">
|
||||
<div class="sipphone-btn" data-role="call-hangup-btn">CALL</div>
|
||||
<div class="sipphone-btn" data-role="video-toggle-btn">VIDEO: OFF</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</whilerow>
|
||||
|
||||
<script type="module">
|
||||
import * as SIP from "https://cdn.jsdelivr.net/npm/sip.js@0.21.2/lib/index.min.js";
|
||||
(function() {
|
||||
function initSipPhone(root) {
|
||||
var exten = root.dataset.sipphoneExten;
|
||||
var secret = root.dataset.sipphoneSecret;
|
||||
var domain = root.dataset.sipphoneDomain;
|
||||
var wss = root.dataset.sipphoneWss;
|
||||
|
||||
var dialedDigits = "";
|
||||
var currentSession = null;
|
||||
var videoEnabled = false;
|
||||
var isRegistered = false;
|
||||
var previewStream = null;
|
||||
|
||||
var lcdLine1 = root.querySelector('[data-role="lcd-line1"]');
|
||||
var lcdStatus = root.querySelector('[data-role="lcd-status"]');
|
||||
var statusDot = root.querySelector('[data-role="status-dot"]');
|
||||
var videoWrap = root.querySelector('[data-role="video-wrap"]');
|
||||
var remoteVideo = root.querySelector('[data-role="remote-video"]');
|
||||
var localVideo = root.querySelector('[data-role="local-video"]');
|
||||
var callHangupBtn = root.querySelector('[data-role="call-hangup-btn"]');
|
||||
var videoToggleBtn = root.querySelector('[data-role="video-toggle-btn"]');
|
||||
|
||||
function setLcd(line1, status, dotClass) {
|
||||
if (line1 !== undefined) lcdLine1.textContent = line1;
|
||||
if (status !== undefined) lcdStatus.textContent = status;
|
||||
statusDot.className = "sipphone-status-dot" + (dotClass ? (" " + dotClass) : "");
|
||||
}
|
||||
|
||||
function updateCallButton() {
|
||||
if (currentSession) {
|
||||
callHangupBtn.textContent = "END";
|
||||
callHangupBtn.classList.add("sipphone-hangup");
|
||||
callHangupBtn.classList.remove("sipphone-btn-disabled");
|
||||
} else {
|
||||
callHangupBtn.textContent = "CALL";
|
||||
callHangupBtn.classList.remove("sipphone-hangup");
|
||||
callHangupBtn.classList.toggle("sipphone-btn-disabled", !isRegistered || !dialedDigits);
|
||||
}
|
||||
lcdStatus.classList.toggle("sipphone-lcd-status-offline", !isRegistered);
|
||||
}
|
||||
|
||||
var ua = new SIP.UserAgent({
|
||||
uri: SIP.UserAgent.makeURI("sip:" + exten + "@" + domain),
|
||||
transportOptions: { server: wss },
|
||||
authorizationUsername: exten,
|
||||
authorizationPassword: secret,
|
||||
logLevel: "error",
|
||||
delegate: {
|
||||
onInvite: function(invitation) {
|
||||
currentSession = invitation;
|
||||
updateCallButton();
|
||||
setLcd("INCOMING CALL", "RINGING", "incall");
|
||||
attachSessionHandlers(invitation);
|
||||
invitation.accept({
|
||||
sessionDescriptionHandlerOptions: {
|
||||
constraints: { audio: true, video: videoEnabled }
|
||||
}
|
||||
}).then(function() {
|
||||
setLcd(exten, "IN CALL", "incall");
|
||||
videoWrap.classList.add("active");
|
||||
attachMedia(invitation);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var registerer = new SIP.Registerer(ua);
|
||||
|
||||
updateCallButton();
|
||||
|
||||
ua.start().then(function() {
|
||||
return registerer.register();
|
||||
}).then(function() {
|
||||
isRegistered = true;
|
||||
setLcd(exten, "READY", "reg");
|
||||
updateCallButton();
|
||||
}).catch(function() {
|
||||
isRegistered = false;
|
||||
setLcd(exten, "REG FAILED", "");
|
||||
updateCallButton();
|
||||
});
|
||||
|
||||
function stopPreview() {
|
||||
if (previewStream) {
|
||||
previewStream.getTracks().forEach(function(t) { t.stop(); });
|
||||
previewStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startPreview() {
|
||||
if (previewStream || currentSession) return;
|
||||
navigator.mediaDevices.getUserMedia({ video: true }).then(function(stream) {
|
||||
if (!videoEnabled || currentSession) {
|
||||
stream.getTracks().forEach(function(t) { t.stop(); });
|
||||
return;
|
||||
}
|
||||
previewStream = stream;
|
||||
localVideo.srcObject = stream;
|
||||
videoWrap.classList.add("active");
|
||||
}).catch(function() {
|
||||
videoEnabled = false;
|
||||
videoToggleBtn.textContent = "VIDEO: OFF";
|
||||
});
|
||||
}
|
||||
|
||||
function attachMedia(session) {
|
||||
stopPreview();
|
||||
var pc = session.sessionDescriptionHandler.peerConnection;
|
||||
var remoteStream = new MediaStream();
|
||||
pc.getReceivers().forEach(function(receiver) {
|
||||
if (receiver.track) remoteStream.addTrack(receiver.track);
|
||||
});
|
||||
remoteVideo.srcObject = remoteStream;
|
||||
|
||||
var localStream = new MediaStream();
|
||||
pc.getSenders().forEach(function(sender) {
|
||||
if (sender.track) localStream.addTrack(sender.track);
|
||||
});
|
||||
localVideo.srcObject = localStream;
|
||||
}
|
||||
|
||||
function attachSessionHandlers(session) {
|
||||
session.stateChange.addListener(function(state) {
|
||||
if (state === SIP.SessionState.Established) {
|
||||
setLcd(undefined, "IN CALL", "incall");
|
||||
videoWrap.classList.add("active");
|
||||
attachMedia(session);
|
||||
} else if (state === SIP.SessionState.Terminated) {
|
||||
setLcd(exten, "READY", "reg");
|
||||
videoWrap.classList.remove("active");
|
||||
remoteVideo.srcObject = null;
|
||||
localVideo.srcObject = null;
|
||||
currentSession = null;
|
||||
dialedDigits = "";
|
||||
updateCallButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
root.querySelectorAll('[data-digit]').forEach(function(key) {
|
||||
key.addEventListener('click', function() {
|
||||
var d = key.dataset.digit;
|
||||
if (currentSession) {
|
||||
currentSession.sessionDescriptionHandler.sendDtmf(d);
|
||||
} else {
|
||||
dialedDigits += d;
|
||||
setLcd(dialedDigits || exten, dialedDigits ? "DIALING" : "READY", dialedDigits ? "" : "reg");
|
||||
updateCallButton();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
callHangupBtn.addEventListener('click', function() {
|
||||
if (!currentSession) {
|
||||
if (!dialedDigits || !isRegistered) return;
|
||||
var target = SIP.UserAgent.makeURI("sip:" + dialedDigits + "@" + domain);
|
||||
var inviter = new SIP.Inviter(ua, target, {
|
||||
sessionDescriptionHandlerOptions: {
|
||||
constraints: { audio: true, video: videoEnabled }
|
||||
}
|
||||
});
|
||||
currentSession = inviter;
|
||||
updateCallButton();
|
||||
attachSessionHandlers(inviter);
|
||||
setLcd(dialedDigits, "CALLING...", "incall");
|
||||
inviter.invite();
|
||||
} else {
|
||||
if (currentSession.state === SIP.SessionState.Established) {
|
||||
currentSession.bye();
|
||||
} else if (currentSession instanceof SIP.Inviter) {
|
||||
currentSession.cancel();
|
||||
} else {
|
||||
currentSession.reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
videoToggleBtn.addEventListener('click', function() {
|
||||
videoEnabled = !videoEnabled;
|
||||
videoToggleBtn.textContent = "VIDEO: " + (videoEnabled ? "ON" : "OFF");
|
||||
if (currentSession) return;
|
||||
if (videoEnabled) {
|
||||
startPreview();
|
||||
} else {
|
||||
stopPreview();
|
||||
localVideo.srcObject = null;
|
||||
videoWrap.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.sipphone-bond').forEach(initSipPhone);
|
||||
})();
|
||||
</script>
|
||||
</ifrow>
|
||||
|
||||
</tag>
|
||||
@@ -0,0 +1,83 @@
|
||||
<sectionframe title="Account Information">
|
||||
|
||||
<sql>select * from users where id=$[_login_id]</sql>
|
||||
<table>
|
||||
<tr><th class="left">Name:</th><td class="right">$[.name]</td></tr>
|
||||
<if value1="$[company_name]" value2="" type="ne">
|
||||
<tr><th class="left">Company Name:</th><td class="right">$[.company_name]</td></tr>
|
||||
</if>
|
||||
<tr><th class="left">E-Mail:</th><td class="right">$[.email]</td></tr>
|
||||
</table>
|
||||
|
||||
</sectionframe>
|
||||
|
||||
<sql>select * from user_messages where user_id=$[_login_id]</sql>$[sqlerror]
|
||||
<ifrow>
|
||||
<sectionframe title="Messages">
|
||||
<table>
|
||||
<whilerow>
|
||||
<tr>
|
||||
<td>$[.message]</td>
|
||||
<td>$[.link]</td>
|
||||
</tr>
|
||||
</whilerow>
|
||||
</table>
|
||||
</sectionframe>
|
||||
</ifrow>
|
||||
|
||||
<sql>select *
|
||||
from virtual_machines
|
||||
join products on virtual_machines.sku=products.sku
|
||||
where user_id=$[_login_id]</sql>
|
||||
<ifrow>
|
||||
<sectionframe title="Virtual Machines">
|
||||
|
||||
<whilerow>
|
||||
|
||||
<div style="margin-bottom: 3px; border: 1pt solid #000; border-radius: 5px;">
|
||||
<div style="float: left; padding: 2px 5px 0px 5px; vertical-align: middle; display: inline-block;"><img src="$[.image]" width="40"></div>
|
||||
<div style="float: left; background: #ccf; width: 300px; padding: 0px 0px 0px 0px;"><span style="font-size: 7pt;">Ubuntu Server 22.04 LTE</span>
|
||||
<br><span style="font-size: 9pt;"><b>barant.com</b></span>
|
||||
<br><span style="font-size: 7pt;">IP:</span> <span style="font-size: 9pt; font-family: monospace;">74.80.233.78</span></div>
|
||||
|
||||
<div style="float: right; padding: 5px;"><img src="/assets/power_on.png" width="32"></div>
|
||||
<div style="float: right; width: 90px; height: 48px; background: #444;"></div>
|
||||
<div style="float: right; margin-right: 25px;">3 days 2:10:33<br><br>34% of 40.2GB</div>
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
|
||||
</whilerow>
|
||||
|
||||
</sectionframe>
|
||||
</ifrow>
|
||||
|
||||
<sql>select *
|
||||
from files
|
||||
join products on files.sku=products.sku
|
||||
where user_id=$[_login_id]</sql>
|
||||
<ifrow>
|
||||
<sectionframe title="Downloadable Content">
|
||||
<p>Content that has been purchased and can be downloaded is displayed
|
||||
below. Click on the Download button to begin downloading your files.</p>
|
||||
|
||||
<whilerow>
|
||||
<table style="margin: 3px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="$[.image]" width="50px"">
|
||||
</td>
|
||||
<td style="width: 300px; border: 1pt inset #888">
|
||||
$[.description]
|
||||
</td>
|
||||
<td style="width: 300px;">
|
||||
<div>$[license_name]</div><div>$[license_key]</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/downloadfile.cgi?sku=$[.sku]"><div class="button">Download</div></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</whilerow>
|
||||
|
||||
</sectionframe>
|
||||
</ifrow>
|
||||
@@ -0,0 +1,18 @@
|
||||
<sectionframe title="Invoices">
|
||||
<if value1="$[@_invoice_id]" value2="" type="ne">
|
||||
<sql>select user_id, status from invoices where id=$[@_invoice_id]</sql>
|
||||
<ifrow>
|
||||
<if expr="('$[.user_id]'='$[_login_id]') and ('$[.status]'='OPEN')">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<include file="invoice.method">
|
||||
<include file="payment.method">
|
||||
</div>
|
||||
</if>
|
||||
</ifrow>
|
||||
</if>
|
||||
<div>
|
||||
<showtable process="selectinvoice" name="invoice_id" value="X">
|
||||
select id as X,id as 'Invoice ID',date_format(created, '%b %D %Y') as 'Date',amount as 'Amount',status as 'Status' from invoices where user_id=$[_login_id] order by created desc
|
||||
</showtable>
|
||||
</div>
|
||||
</sectionframe>
|
||||
@@ -0,0 +1,4 @@
|
||||
<include file="phones.section">
|
||||
|
||||
<include file="SIPPHONE.tag">
|
||||
<sipphone>
|
||||
@@ -0,0 +1,17 @@
|
||||
<sectionframe title="ACCOUNT">
|
||||
<if value1="$[SUBPAGE]" value2="" type="eq">
|
||||
<linkbutton page="account" class="submenuitem-active">ACCOUNT INFORMATION</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="account">ACCOUNT INFORMATION</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="phones" type="eq">
|
||||
<linkbutton page="account" subpage="phones" class="submenuitem-active">PHONES</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="account" subpage="phones">PHONES</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="invoices" type="eq">
|
||||
<linkbutton page="account" subpage="invoices" class="submenuitem-active">INVOICES</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="account" subpage="invoices">INVOICES</linkbutton><br>
|
||||
</if>
|
||||
</sectionframe>
|
||||
@@ -4,108 +4,16 @@
|
||||
<retain name="download_file">
|
||||
<retain name="invoice_id">
|
||||
|
||||
<sectionframe title="Account Information">
|
||||
|
||||
<sql>select * from users where id=$[_login_id]</sql>
|
||||
<table>
|
||||
<tr><th class="left">Name:</th><td class="right">$[.name]</td></tr>
|
||||
<if value1="$[company_name]" value2="" type="ne">
|
||||
<tr><th class="left">Company Name:</th><td class="right">$[.company_name]</td></tr>
|
||||
</if>
|
||||
<tr><th class="left">E-Mail:</th><td class="right">$[.email]</td></tr>
|
||||
</table>
|
||||
|
||||
</sectionframe>
|
||||
|
||||
<sql>select * from user_messages where user_id=$[_login_id]</sql>$[sqlerror]
|
||||
<ifrow>
|
||||
<sectionframe title="Messages">
|
||||
<table>
|
||||
<whilerow>
|
||||
<tr>
|
||||
<td>$[.message]</td>
|
||||
<td>$[.link]</td>
|
||||
</tr>
|
||||
</whilerow>
|
||||
</table>
|
||||
</sectionframe>
|
||||
</ifrow>
|
||||
|
||||
<sql>select *
|
||||
from virtual_machines
|
||||
join products on virtual_machines.sku=products.sku
|
||||
where user_id=$[_login_id]</sql>
|
||||
<ifrow>
|
||||
<sectionframe title="Virtual Machines">
|
||||
|
||||
<whilerow>
|
||||
|
||||
<div style="margin-bottom: 3px; border: 1pt solid #000; border-radius: 5px;">
|
||||
<div style="float: left; padding: 2px 5px 0px 5px; vertical-align: middle; display: inline-block;"><img src="$[.image]" width="40"></div>
|
||||
<div style="float: left; background: #ccf; width: 300px; padding: 0px 0px 0px 0px;"><span style="font-size: 7pt;">Ubuntu Server 22.04 LTE</span>
|
||||
<br><span style="font-size: 9pt;"><b>barant.com</b></span>
|
||||
<br><span style="font-size: 7pt;">IP:</span> <span style="font-size: 9pt; font-family: monospace;">74.80.233.78</span></div>
|
||||
|
||||
<div style="float: right; padding: 5px;"><img src="/assets/power_on.png" width="32"></div>
|
||||
<div style="float: right; width: 90px; height: 48px; background: #444;"></div>
|
||||
<div style="float: right; margin-right: 25px;">3 days 2:10:33<br><br>34% of 40.2GB</div>
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
|
||||
</whilerow>
|
||||
|
||||
</sectionframe>
|
||||
</ifrow>
|
||||
|
||||
<include file="phones.section">
|
||||
|
||||
<sql>select *
|
||||
from files
|
||||
join products on files.sku=products.sku
|
||||
where user_id=$[_login_id]</sql>
|
||||
<ifrow>
|
||||
<sectionframe title="Downloadable Content">
|
||||
<p>Content that has been purchased and can be downloaded is displayed
|
||||
below. Click on the Download button to begin downloading your files.</p>
|
||||
|
||||
<whilerow>
|
||||
<table style="margin: 3px;">
|
||||
<tr>
|
||||
<td>
|
||||
<img src="$[.image]" width="50px"">
|
||||
</td>
|
||||
<td style="width: 300px; border: 1pt inset #888">
|
||||
$[.description]
|
||||
</td>
|
||||
<td style="width: 300px;">
|
||||
<div>$[license_name]</div><div>$[license_key]</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/downloadfile.cgi?sku=$[.sku]"><div class="button">Download</div></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</whilerow>
|
||||
|
||||
</sectionframe>
|
||||
</ifrow>
|
||||
|
||||
<sectionframe title="Invoices">
|
||||
<div style="display: flex;">
|
||||
<div style="display: inline-block; padding: 0px 10px 0px 0px;">
|
||||
<showtable process="selectinvoice" name="invoice_id" value="X">
|
||||
select id as X,id as 'Invoice ID',date_format(created, '%b %D %Y') as 'Date',amount as 'Amount',status as 'Status' from invoices where user_id=$[_login_id]
|
||||
</showtable>
|
||||
</div>
|
||||
<if value1="$[_invoice_id]" value2="" type="ne">
|
||||
<div>
|
||||
<include file="invoice.method">
|
||||
<include file="payment.method">
|
||||
</div>
|
||||
</if>
|
||||
</div>
|
||||
</sectionframe>
|
||||
|
||||
|
||||
<div id="account-subpage">
|
||||
<if value1="$[SUBPAGE]" value2="" type="eq">
|
||||
<include file="account-info.subpage">
|
||||
<else>
|
||||
<try>
|
||||
<include file="account-$[SUBPAGE].subpage">
|
||||
<catch>
|
||||
<p>That account section doesn't exist.</p>
|
||||
</try>
|
||||
</if>
|
||||
</div>
|
||||
</contents>
|
||||
</page>
|
||||
@@ -93,6 +93,16 @@
|
||||
box-shadow: 0 0 0.4em rgba(197, 164, 90, 0.25);
|
||||
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.1s ease;
|
||||
}
|
||||
/* .icon-btn's own display:inline-flex is a plain class selector -
|
||||
same specificity as the browser's own [hidden]{display:none} UA
|
||||
default, and an author rule always wins that tie regardless of
|
||||
source order - confirmed as a real, previously-shipped bug on a
|
||||
different site this same session (a hidden modal that stayed
|
||||
visible for the exact same reason). Without this, the empty-folder
|
||||
button's `hidden` attribute would be set correctly but silently
|
||||
ignored, leaving it visible on every folder instead of only
|
||||
Spam/Trash. */
|
||||
.icon-btn[hidden] { display: none; }
|
||||
.icon-btn svg { width: 16px; height: 16px; display: block; }
|
||||
.icon-btn:hover {
|
||||
box-shadow: 0 0 0.8em rgba(197, 164, 90, 0.5), 0 0 0.25em rgba(227, 196, 126, 0.55);
|
||||
@@ -118,12 +128,14 @@
|
||||
<div id="email-client">
|
||||
<div id="email-toolbar">
|
||||
<span id="email-connected-as">$[@_imap_user]</span><span id="email-list-count"></span>
|
||||
<span id="email-pagination"></span>
|
||||
<span id="email-toolbar-actions">
|
||||
<span id="email-search-box">
|
||||
<input type="text" id="email-search-input" placeholder="Search mail…">
|
||||
<button type="button" id="email-search-btn" class="icon-btn" title="Search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg></button>
|
||||
</span>
|
||||
<button type="button" id="email-compose-btn" class="icon-btn" title="New Mail"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h13a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M2.5 7.5 12 14l9.5-6.5"/><path d="M18.5 3.5v5M16 6h5"/></svg></button>
|
||||
<button type="button" id="email-empty-folder-btn" class="icon-btn" title="Permanently empty this folder" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"/><path d="M9 7V4h6v3"/><path d="M6 7l1 13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-13"/><path d="M9.5 12.5l5 5M14.5 12.5l-5 5"/></svg></button>
|
||||
<button type="button" id="email-refresh-btn" class="icon-btn" title="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.6-4.4M4 4v5h5"/><path d="M4 13a8 8 0 0 0 14.6 4.4M20 20v-5h-5"/></svg></button>
|
||||
<button type="button" id="email-disconnect-btn" class="icon-btn" title="Disconnect"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg></button>
|
||||
</span>
|
||||
@@ -135,4 +147,4 @@
|
||||
</div>
|
||||
</if>
|
||||
</div>
|
||||
<script src="/assets/email.js?v=20260907b"></script>
|
||||
<script src="/assets/email.js?v=20260926a"></script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<include file="invoice.method">
|
||||
<br>
|
||||
<linkbutton page="editinvoice"><div class="button">Edit Invoice</div></linkbutton>
|
||||
<div style="margin-top: 10px; display: flex; align-items: center; gap: 6px;">
|
||||
<input type="email" id="mail_invoice_email" placeholder="recipient@example.com" style="width: 220px;">
|
||||
<button type="button" id="mail_invoice_btn" class="button">Mail Invoice</button>
|
||||
<span id="mail_invoice_status" style="font-size: 12px; color: var(--bond-text-dim);"></span>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var btn = document.getElementById('mail_invoice_btn');
|
||||
var input = document.getElementById('mail_invoice_email');
|
||||
var status = document.getElementById('mail_invoice_status');
|
||||
if (!btn || !input || !status) return;
|
||||
btn.addEventListener('click', function() {
|
||||
var email = input.value.trim();
|
||||
if (!email) {
|
||||
status.textContent = 'Enter an e-mail address first.';
|
||||
status.style.color = '#a13a3a';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
status.textContent = 'Sending…';
|
||||
status.style.color = 'var(--bond-text-dim)';
|
||||
fetch('/admin/mail-invoice', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ email: email }).toString()
|
||||
}).then(function(r) { return r.json(); }).then(function(res) {
|
||||
btn.disabled = false;
|
||||
if (res.ok) {
|
||||
status.textContent = 'Invoice emailed to ' + email + '.';
|
||||
status.style.color = '#6fb8c9';
|
||||
} else {
|
||||
status.textContent = res.error || 'Could not send the invoice.';
|
||||
status.style.color = '#a13a3a';
|
||||
}
|
||||
}).catch(function() {
|
||||
btn.disabled = false;
|
||||
status.textContent = 'Request failed. Please try again.';
|
||||
status.style.color = '#a13a3a';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -1,5 +1,120 @@
|
||||
<retain name="invoice_id">
|
||||
<h3>Invoices</h3>
|
||||
<showtable page="admin_invoice" process="selectinvoice" name="invoice_id" value="X">
|
||||
select invoices.id as X,invoices.id,user_id,name,invoices.created,amount,invoices.status from invoices join users on invoices.user_id=users.id
|
||||
</showtable>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<button type="button" id="new_invoice_toggle" class="button">+ Create New Invoice</button>
|
||||
<div id="new_invoice_form" style="display: none; margin-top: 8px; align-items: center; gap: 6px;">
|
||||
<select id="new_invoice_user">
|
||||
<option value="">Select a customer…</option>
|
||||
<sql>select id,name,email from users order by name</sql>
|
||||
<whilerow>
|
||||
<option value="$[.id]">$[.name] ($[.email])</option>
|
||||
</whilerow>
|
||||
</select>
|
||||
<button type="button" id="new_invoice_create_btn" class="button">Create</button>
|
||||
<span id="new_invoice_status" style="font-size: 12px; color: var(--bond-text-dim);"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span id="copy_invoice_status" style="font-size: 12px; color: var(--bond-text-dim);"></span>
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>id</th>
|
||||
<th>user_id</th>
|
||||
<th>name</th>
|
||||
<th>created</th>
|
||||
<th>amount</th>
|
||||
<th>status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<sql>select invoices.id,user_id,name,invoices.created,amount,invoices.status from invoices join users on invoices.user_id=users.id</sql>
|
||||
<whilerow>
|
||||
<tr>
|
||||
<td><linkbutton page="admin" subpage="invoice" process="selectinvoice" name="invoice_id" value="$[.id]" class="button">Open</linkbutton></td>
|
||||
<td>$[.id]</td>
|
||||
<td>$[.user_id]</td>
|
||||
<td>$[.name]</td>
|
||||
<td>$[.created]</td>
|
||||
<td>$[.amount]</td>
|
||||
<td>$[.status]</td>
|
||||
<td><button type="button" class="button copy_invoice_btn" data-invoice-id="$[.id]">Copy</button></td>
|
||||
</tr>
|
||||
</whilerow>
|
||||
</table>
|
||||
<script>
|
||||
(function() {
|
||||
var toggle = document.getElementById('new_invoice_toggle');
|
||||
var form = document.getElementById('new_invoice_form');
|
||||
var userSelect = document.getElementById('new_invoice_user');
|
||||
var createBtn = document.getElementById('new_invoice_create_btn');
|
||||
var status = document.getElementById('new_invoice_status');
|
||||
if (!toggle || !form || !userSelect || !createBtn || !status) return;
|
||||
|
||||
toggle.addEventListener('click', function() {
|
||||
form.style.display = form.style.display === 'none' ? 'flex' : 'none';
|
||||
});
|
||||
|
||||
createBtn.addEventListener('click', function() {
|
||||
var userId = userSelect.value;
|
||||
if (!userId) {
|
||||
status.textContent = 'Please select a customer.';
|
||||
status.style.color = '#a13a3a';
|
||||
return;
|
||||
}
|
||||
createBtn.disabled = true;
|
||||
status.textContent = 'Creating…';
|
||||
status.style.color = 'var(--bond-text-dim)';
|
||||
fetch('/admin/create-invoice', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ user_id: userId }).toString()
|
||||
}).then(function(r) { return r.json(); }).then(function(res) {
|
||||
if (res.ok) {
|
||||
window.location.href = '/editinvoice';
|
||||
} else {
|
||||
createBtn.disabled = false;
|
||||
status.textContent = res.error || 'Could not create the invoice.';
|
||||
status.style.color = '#a13a3a';
|
||||
}
|
||||
}).catch(function() {
|
||||
createBtn.disabled = false;
|
||||
status.textContent = 'Request failed. Please try again.';
|
||||
status.style.color = '#a13a3a';
|
||||
});
|
||||
});
|
||||
|
||||
var copyStatus = document.getElementById('copy_invoice_status');
|
||||
document.querySelectorAll('.copy_invoice_btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var sourceId = btn.getAttribute('data-invoice-id');
|
||||
document.querySelectorAll('.copy_invoice_btn').forEach(function(b) { b.disabled = true; });
|
||||
if (copyStatus) {
|
||||
copyStatus.textContent = 'Copying…';
|
||||
copyStatus.style.color = 'var(--bond-text-dim)';
|
||||
}
|
||||
fetch('/admin/copy-invoice', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ source_invoice_id: sourceId }).toString()
|
||||
}).then(function(r) { return r.json(); }).then(function(res) {
|
||||
if (res.ok) {
|
||||
window.location.href = '/editinvoice';
|
||||
} else {
|
||||
document.querySelectorAll('.copy_invoice_btn').forEach(function(b) { b.disabled = false; });
|
||||
if (copyStatus) {
|
||||
copyStatus.textContent = res.error || 'Could not copy the invoice.';
|
||||
copyStatus.style.color = '#a13a3a';
|
||||
}
|
||||
}
|
||||
}).catch(function() {
|
||||
document.querySelectorAll('.copy_invoice_btn').forEach(function(b) { b.disabled = false; });
|
||||
if (copyStatus) {
|
||||
copyStatus.textContent = 'Request failed. Please try again.';
|
||||
copyStatus.style.color = '#a13a3a';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<h3>Mail Accounts</h3>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<button type="button" id="new_mailaccount_toggle" class="button">+ Add Mail Account</button>
|
||||
<div id="new_mailaccount_form" style="display: none; margin-top: 8px; align-items: center; gap: 6px;">
|
||||
<input type="text" id="new_mailaccount_host" placeholder="Mail Server" value="mail.barant.com">
|
||||
<input type="text" id="new_mailaccount_email" placeholder="E-Mail Address">
|
||||
<input type="password" id="new_mailaccount_password" placeholder="Password">
|
||||
<button type="button" id="new_mailaccount_create_btn" class="button">Add</button>
|
||||
<span id="new_mailaccount_status" style="font-size: 12px; color: var(--bond-text-dim);"></span>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>E-Mail</th>
|
||||
<th>Server</th>
|
||||
<th>Added</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<sql>select id,email,host,created from email_accounts where user_id=$[@_login_id] order by email</sql>
|
||||
<whilerow>
|
||||
<tr>
|
||||
<td>$[.email]</td>
|
||||
<td>$[.host]</td>
|
||||
<td>$[.created]</td>
|
||||
<td><button type="button" class="button remove_mailaccount_btn" data-account-id="$[.id]">Remove</button></td>
|
||||
</tr>
|
||||
</whilerow>
|
||||
</table>
|
||||
<p style="font-size: 12px; color: var(--bond-text-dim);">Saved accounts automatically reconnect the next time you log in.</p>
|
||||
<script>
|
||||
(function() {
|
||||
var toggle = document.getElementById('new_mailaccount_toggle');
|
||||
var form = document.getElementById('new_mailaccount_form');
|
||||
var hostInput = document.getElementById('new_mailaccount_host');
|
||||
var emailInput = document.getElementById('new_mailaccount_email');
|
||||
var passwordInput = document.getElementById('new_mailaccount_password');
|
||||
var createBtn = document.getElementById('new_mailaccount_create_btn');
|
||||
var status = document.getElementById('new_mailaccount_status');
|
||||
if (!toggle || !form || !hostInput || !emailInput || !passwordInput || !createBtn || !status) return;
|
||||
|
||||
toggle.addEventListener('click', function() {
|
||||
form.style.display = form.style.display === 'none' ? 'flex' : 'none';
|
||||
});
|
||||
|
||||
createBtn.addEventListener('click', function() {
|
||||
var host = hostInput.value.trim();
|
||||
var email = emailInput.value.trim();
|
||||
var password = passwordInput.value;
|
||||
if (!host || !email || !password) {
|
||||
status.textContent = 'Please enter a mail server, e-mail address, and password.';
|
||||
status.style.color = '#a13a3a';
|
||||
return;
|
||||
}
|
||||
createBtn.disabled = true;
|
||||
status.textContent = 'Connecting…';
|
||||
status.style.color = 'var(--bond-text-dim)';
|
||||
fetch('/admin/add-mail-account', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ mail_host: host, mail_email: email, mail_password: password }).toString()
|
||||
}).then(function(r) { return r.json(); }).then(function(res) {
|
||||
if (res.ok) {
|
||||
window.location.href = '/admin/mailaccounts';
|
||||
} else {
|
||||
createBtn.disabled = false;
|
||||
status.textContent = res.error || 'Could not add the account.';
|
||||
status.style.color = '#a13a3a';
|
||||
}
|
||||
}).catch(function() {
|
||||
createBtn.disabled = false;
|
||||
status.textContent = 'Request failed. Please try again.';
|
||||
status.style.color = '#a13a3a';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.remove_mailaccount_btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var accountId = btn.getAttribute('data-account-id');
|
||||
document.querySelectorAll('.remove_mailaccount_btn').forEach(function(b) { b.disabled = true; });
|
||||
fetch('/admin/remove-mail-account', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ account_id: accountId }).toString()
|
||||
}).then(function(r) { return r.json(); }).then(function(res) {
|
||||
if (res.ok) {
|
||||
window.location.href = '/admin/mailaccounts';
|
||||
} else {
|
||||
document.querySelectorAll('.remove_mailaccount_btn').forEach(function(b) { b.disabled = false; });
|
||||
}
|
||||
}).catch(function() {
|
||||
document.querySelectorAll('.remove_mailaccount_btn').forEach(function(b) { b.disabled = false; });
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -1,8 +1,29 @@
|
||||
<sectionframe title="ADMINISTRATIVE SUPPORT">
|
||||
<linkbutton page="admin" subpage="email">EMAIL</linkbutton><br>
|
||||
<linkbutton page="admin" subpage="invoices">INVOICES</linkbutton><br>
|
||||
<linkbutton page="admin" subpage="products">PRODUCTS</linkbutton><br>
|
||||
<linkbutton page="admin" subpage="users">USERS</linkbutton><br>
|
||||
<if value1="$[SUBPAGE]" value2="email" type="eq">
|
||||
<linkbutton page="admin" subpage="email" class="submenuitem-active">EMAIL</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="admin" subpage="email">EMAIL</linkbutton><br>
|
||||
</if>
|
||||
<if expr="('$[SUBPAGE]'='invoices') or ('$[SUBPAGE]'='invoice')">
|
||||
<linkbutton page="admin" subpage="invoices" class="submenuitem-active">INVOICES</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="admin" subpage="invoices">INVOICES</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="products" type="eq">
|
||||
<linkbutton page="admin" subpage="products" class="submenuitem-active">PRODUCTS</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="admin" subpage="products">PRODUCTS</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="users" type="eq">
|
||||
<linkbutton page="admin" subpage="users" class="submenuitem-active">USERS</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="admin" subpage="users">USERS</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="mailaccounts" type="eq">
|
||||
<linkbutton page="admin" subpage="mailaccounts" class="submenuitem-active">MAIL ACCOUNTS</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="admin" subpage="mailaccounts">MAIL ACCOUNTS</linkbutton><br>
|
||||
</if>
|
||||
</sectionframe>
|
||||
<if value1="$[SUBPAGE]" value2="email" type="eq">
|
||||
<include file="email.menu">
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
<title>ADMINISTRATION</title>
|
||||
<contents>
|
||||
<if value1="$[SUBPAGE]" value2="email" type="eq">
|
||||
<include file="admin-email.subpage">
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
<p>Requested page is not found.</p>
|
||||
<else>
|
||||
<include file="admin-email.subpage">
|
||||
</if>
|
||||
<else>
|
||||
<if value1="$[SUBPAGE]" value2="newemail" type="eq">
|
||||
<include file="admin-newemail.subpage">
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
<p>Requested page is not found.</p>
|
||||
<else>
|
||||
<include file="admin-newemail.subpage">
|
||||
</if>
|
||||
<else>
|
||||
<if value1="$[SUBPAGE]" value2="invoices" type="eq">
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
@@ -14,6 +22,13 @@
|
||||
<include file="admin-invoices.subpage">
|
||||
</if>
|
||||
<else>
|
||||
<if value1="$[SUBPAGE]" value2="invoice" type="eq">
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
<p>Requested page is not found.</p>
|
||||
<else>
|
||||
<include file="admin-invoice.subpage">
|
||||
</if>
|
||||
<else>
|
||||
<if value1="$[SUBPAGE]" value2="users" type="eq">
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
<p>Requested page is not found.</p>
|
||||
@@ -28,18 +43,27 @@
|
||||
<include file="admin-products.subpage">
|
||||
</if>
|
||||
<else>
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
<if value1="$[SUBPAGE]" value2="mailaccounts" type="eq">
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
<p>Requested page is not found.</p>
|
||||
<else>
|
||||
<include file="admin-mailaccounts.subpage">
|
||||
</if>
|
||||
<else>
|
||||
<if expr="('$[_login_type]'='user')">
|
||||
<p>Requested page is not found.</p>
|
||||
<else>
|
||||
<eval name="_menu">
|
||||
<include file="adminmenu.section">
|
||||
</eval>
|
||||
<p>Select an item from the menu.</p>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</contents>
|
||||
</page>
|
||||
@@ -1,11 +0,0 @@
|
||||
<page secure="yes" admin="true" loggedin="yes">
|
||||
<title>INVOICE</title>
|
||||
<contents>
|
||||
<eval name="_menu">
|
||||
<include file="adminmenu.section">
|
||||
</eval>
|
||||
<include file="invoice.method">
|
||||
<br>
|
||||
<linkbutton page="editinvoice"><div class="button">Edit Invoice</div></linkbutton>
|
||||
</contents>
|
||||
</page>
|
||||
@@ -1,4 +1,4 @@
|
||||
<sectionframe title="Admin Functions">
|
||||
<div><linkbutton page="modular1_1_preset_admin">Modular 1 Presets</linkbutton></div>
|
||||
<div><linkbutton page="admin_invoices">Invoices</linkbutton></div>
|
||||
<div><linkbutton page="admin" subpage="invoices">Invoices</linkbutton></div>
|
||||
</sectionframe>
|
||||
@@ -95,7 +95,7 @@
|
||||
</if>
|
||||
</tag>
|
||||
|
||||
<tag name="showtable" keywords="sessionid page process name value">
|
||||
<tag name="showtable" keywords="sessionid page subpage process name value">
|
||||
<sql sessionid="#[sessionid]"><container></sql>$[sqlerror]
|
||||
<ifrow sessionid="#[sessionid]">
|
||||
<table>
|
||||
@@ -117,7 +117,7 @@
|
||||
<set name="field" value="$[?$[index]]">
|
||||
<if value1="$[field]" value2="X" type="eq">
|
||||
<td>
|
||||
<linkbutton page="#[page]" process="#[process]" name="#[name]" value="$[#[value]]" class="button">Open</linkbutton>
|
||||
<linkbutton page="#[page]" subpage="#[subpage]" process="#[process]" name="#[name]" value="$[#[value]]" class="button">Open</linkbutton>
|
||||
</td>
|
||||
<else>
|
||||
<if expr="substring('$[field]',1,1)!='_'">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#api POST /admin/copy-invoice
|
||||
<set name="host" value="localhost">
|
||||
<set name="database" value="barant">
|
||||
<set name="user" value="barant">
|
||||
<set name="password" value="uversa1960">
|
||||
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
|
||||
<header name="Content-Type" value="application/json">
|
||||
<if value1="$[@_login_type]" value2="admin" type="ne">
|
||||
{"ok":false,"error":"Not authorized."}
|
||||
<else>
|
||||
<if value1="$[source_invoice_id]" value2="" type="eq">
|
||||
{"ok":false,"error":"Please select an invoice to copy."}
|
||||
<else>
|
||||
<sql>select * from invoices where id=$[source_invoice_id]</sql>
|
||||
<ifrow>
|
||||
<sql>insert into invoices set user_id=$[.user_id], created=now(), amount=$[.amount], status="OPEN"</sql>
|
||||
<sql>select last_insert_id() as new_invoice_id</sql>
|
||||
<set name="new_invoice_id" value="$[.new_invoice_id]">
|
||||
<sql>insert into invoices_detail (invoice_id,sku,sku_suffix,quantity,description,text,price,total,tax_amount,type)
|
||||
select $[new_invoice_id],sku,sku_suffix,quantity,description,text,price,total,tax_amount,type
|
||||
from invoices_detail where invoice_id=$[source_invoice_id]</sql>
|
||||
<set name="@_invoice_id" value="$[new_invoice_id]">
|
||||
{"ok":true,"invoice_id":$[new_invoice_id]}
|
||||
<else>
|
||||
{"ok":false,"error":"Invoice not found."}
|
||||
</ifrow>
|
||||
</if>
|
||||
</if>
|
||||
</database>
|
||||
@@ -0,0 +1,21 @@
|
||||
#api POST /admin/create-invoice
|
||||
<set name="host" value="localhost">
|
||||
<set name="database" value="barant">
|
||||
<set name="user" value="barant">
|
||||
<set name="password" value="uversa1960">
|
||||
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
|
||||
<header name="Content-Type" value="application/json">
|
||||
<if value1="$[@_login_type]" value2="admin" type="ne">
|
||||
{"ok":false,"error":"Not authorized."}
|
||||
<else>
|
||||
<if value1="$[user_id]" value2="" type="eq">
|
||||
{"ok":false,"error":"Please select a user."}
|
||||
<else>
|
||||
<sql>insert into invoices set user_id=$[user_id], created=now(), amount=0.00, status="OPEN"</sql>
|
||||
<sql>select last_insert_id() as new_invoice_id</sql>
|
||||
<set name="new_invoice_id" value="$[.new_invoice_id]">
|
||||
<set name="@_invoice_id" value="$[new_invoice_id]">
|
||||
{"ok":true,"invoice_id":$[new_invoice_id]}
|
||||
</if>
|
||||
</if>
|
||||
</database>
|
||||
Executable → Regular
+113
-59
@@ -3,9 +3,6 @@
|
||||
<contents>
|
||||
<database host="$[host]" name="$[database]" user="$[user]" password="$[password]" sessionid="1">
|
||||
<script>
|
||||
function checkDetail() {
|
||||
document.getElementById('detail').style.display=(document.getElementById('sku').value=='***')?'block':'none';
|
||||
}
|
||||
function totalInvoice() {
|
||||
const detail_section = document.getElementById('detail_section');
|
||||
const totalsum = document.getElementById('totalsum');
|
||||
@@ -13,10 +10,11 @@ function totalInvoice() {
|
||||
var sum = 0;
|
||||
var index = 0;
|
||||
detail_section.childNodes.forEach((child) => {
|
||||
if (!child.quantity_z) return;
|
||||
child.quantity_z.setAttribute('name','quantity' + index);
|
||||
child.sku_z.setAttribute('name','sku' + index);
|
||||
child.description_z.setAttribute('name','description' + index);
|
||||
child.textarea_z.setAttribute('name','textarea' + index);
|
||||
child.textarea_z.setAttribute('name','textarea' + index);
|
||||
child.detail_z.setAttribute('name','detail' + index);
|
||||
child.price_z.setAttribute('name','price' + index);
|
||||
child.taxAmount_z.setAttribute('name','tax_amount' + index);
|
||||
@@ -31,16 +29,18 @@ function totalInvoice() {
|
||||
}
|
||||
function addLine(qty,skuid,descr,text,prc,tax,tot,typ) {
|
||||
|
||||
const lineItem = document.createElement('div');
|
||||
lineItem.style.border = "padding: 3px;";
|
||||
const lineItem = document.createElement('tr');
|
||||
|
||||
column1 = document.createElement('div');
|
||||
column1.setAttribute('style','display: inline-block; margin-right: 5px;');
|
||||
column1 = document.createElement('td');
|
||||
column1.setAttribute('style','text-align: center; vertical-align: top; padding: 6px 4px; border-bottom: 1px solid #000;');
|
||||
removeButton = document.createElement('button');
|
||||
removeButton.setAttribute('type','button');
|
||||
removeButton.setAttribute('class','button');
|
||||
removeButton.setAttribute('style','padding: 1px 7px 2px 7px; font-size: 11pt; line-height: 1;');
|
||||
removeButton.innerText = '-';
|
||||
removeButton.addEventListener('click', function(e) {
|
||||
document.getElementById('detail_section').removeChild(lineItem);
|
||||
if (lineItem.detailRow) document.getElementById('detail_section').removeChild(lineItem.detailRow);
|
||||
totalInvoice();
|
||||
});
|
||||
column1.appendChild(removeButton)
|
||||
@@ -66,16 +66,15 @@ function addLine(qty,skuid,descr,text,prc,tax,tot,typ) {
|
||||
lineItem.total_z = total;
|
||||
lineItem.type_z = type;
|
||||
|
||||
column2 = document.createElement('div');
|
||||
column2.setAttribute('style','display: inline-block; margin-right: 5px;');
|
||||
column2 = document.createElement('td');
|
||||
column2.setAttribute('style','width: 54px; vertical-align: top; padding: 6px 4px; border-bottom: 1px solid #000;');
|
||||
quantity.setAttribute('type','text');
|
||||
quantity.setAttribute('size','5');
|
||||
quantity.setAttribute('maxlength','5');
|
||||
quantity.setAttribute('style','width: 44px; text-align: center;');
|
||||
quantity.setAttribute('name','quantity');
|
||||
quantity.setAttribute('value',qty);
|
||||
quantity.addEventListener('change',(e) => {
|
||||
taxAmount.value = '0.00';
|
||||
total.value = (quantity.value * sku.options[sku.selectedIndex].price).toFixed(2);
|
||||
total.value = (quantity.value * sku.options[sku.selectedIndex].price).toFixed(2);
|
||||
description.value = sku.options[sku.selectedIndex].description;
|
||||
lineItem.total = total.value;
|
||||
totalInvoice();
|
||||
@@ -83,13 +82,14 @@ function addLine(qty,skuid,descr,text,prc,tax,tot,typ) {
|
||||
column2.appendChild(quantity);
|
||||
lineItem.appendChild(column2);
|
||||
|
||||
column3 = document.createElement('div');
|
||||
column3.setAttribute('style','display: inline-block; margin-right: 5px;');
|
||||
column3 = document.createElement('td');
|
||||
column3.setAttribute('style','vertical-align: top; padding: 6px 4px; border-bottom: 1px solid #000;');
|
||||
sku.setAttribute('name','sku');
|
||||
sku.setAttribute('style','width: 100%;');
|
||||
sku.detail = detail;
|
||||
sku.addEventListener('change', (e) => {
|
||||
sku.detail.style.display = sku.value == '***' ? 'block':'none';
|
||||
price.value = sku.options[sku.selectedIndex].price;
|
||||
price.value = sku.options[sku.selectedIndex].price;
|
||||
taxAmount.value = '0.00';
|
||||
total.value = (quantity.value * sku.options[sku.selectedIndex].price).toFixed(2);
|
||||
lineItem.total = total.value;
|
||||
@@ -99,13 +99,16 @@ function addLine(qty,skuid,descr,text,prc,tax,tot,typ) {
|
||||
});
|
||||
<sql sessionid="1">select * from product_list</sql>
|
||||
<whilerow sessionid="1">
|
||||
<call pgm="/bin/sh" arg1="-c" arg2="jq -R -s . | sed 's#/#\\/#g'" input="$[1.description]" name="product_description_js">
|
||||
<call pgm="/bin/sh" arg1="-c" arg2="jq -R -s . | sed 's#/#\\/#g'" input="$[1.sku]" name="product_sku_js">
|
||||
<call pgm="/bin/sh" arg1="-c" arg2="jq -R -s . | sed 's#/#\\/#g'" input="$[1.type]" name="product_type_js">
|
||||
option = document.createElement('option');
|
||||
option.setAttribute('value','$[1.sku]');
|
||||
option.description = '$[1.description]';
|
||||
option.typex = '$[1.type]';
|
||||
option.setAttribute('value',$[product_sku_js]);
|
||||
option.description = $[product_description_js];
|
||||
option.typex = $[product_type_js];
|
||||
option.price = '$[1.price]';
|
||||
option.innerText = '$[1.description]';
|
||||
if(skuid == '$[1.sku]') {
|
||||
option.innerText = $[product_description_js];
|
||||
if(skuid == $[product_sku_js]) {
|
||||
option.selected = 'selected';
|
||||
}
|
||||
sku.appendChild(option);
|
||||
@@ -121,47 +124,52 @@ function addLine(qty,skuid,descr,text,prc,tax,tot,typ) {
|
||||
type.setAttribute('value', typ);
|
||||
lineItem.appendChild(type);
|
||||
|
||||
column4 = document.createElement('div');
|
||||
column4.setAttribute('style','display: inline-block; margin-right: 5px;');
|
||||
column4 = document.createElement('td');
|
||||
column4.setAttribute('style','width: 90px; vertical-align: top; padding: 6px 4px; border-bottom: 1px solid #000;');
|
||||
price.setAttribute('type','text');
|
||||
price.setAttribute('size','15');
|
||||
price.setAttribute('maxlength','15');
|
||||
price.setAttribute('style','width: 80px; text-align: right;');
|
||||
price.setAttribute('name','price');
|
||||
price.setAttribute('value',prc);
|
||||
column4.appendChild(price);
|
||||
lineItem.appendChild(column4);
|
||||
|
||||
column5 = document.createElement('div');
|
||||
column5.setAttribute('style','display: inline-block; margin-right: 5px;');
|
||||
column5 = document.createElement('td');
|
||||
column5.setAttribute('style','width: 70px; vertical-align: top; padding: 6px 4px; border-bottom: 1px solid #000;');
|
||||
taxAmount.setAttribute('type','text');
|
||||
taxAmount.setAttribute('size','15');
|
||||
taxAmount.setAttribute('maxlength','15');
|
||||
taxAmount.setAttribute('style','width: 60px; text-align: right;');
|
||||
taxAmount.setAttribute('name','tax_amount');
|
||||
taxAmount.setAttribute('value',tax);
|
||||
column5.appendChild(taxAmount);
|
||||
lineItem.appendChild(column5);
|
||||
|
||||
column6 = document.createElement('div');
|
||||
column6.setAttribute('style','display: inline-block;');
|
||||
column6 = document.createElement('td');
|
||||
column6.setAttribute('style','width: 90px; vertical-align: top; padding: 6px 4px; border-bottom: 1px solid #000;');
|
||||
total.setAttribute('type','text');
|
||||
total.setAttribute('size','15');
|
||||
total.setAttribute('maxlength','15');
|
||||
total.setAttribute('style','width: 80px; text-align: right;');
|
||||
total.setAttribute('name','total');
|
||||
total.setAttribute('value',tot);
|
||||
column6.appendChild(total);
|
||||
lineItem.appendChild(column6);
|
||||
|
||||
const detailRow = document.createElement('tr');
|
||||
const detailCell = document.createElement('td');
|
||||
detailCell.setAttribute('colspan','6');
|
||||
detailCell.setAttribute('style','padding: 0 4px 6px 4px; border-bottom: 1px solid #000;');
|
||||
textarea.setAttribute('name','textarea');
|
||||
textarea.setAttribute('rows','5');
|
||||
textarea.setAttribute('style','width: 100%; background: #101014; color: #e7e4dc; border: 1px solid rgba(255, 255, 255, 0.07); border-radius: 2px; padding: 6px 8px; font-family: inherit; font-size: 14px;');
|
||||
textarea.innerHTML = text;
|
||||
CKEDITOR.replace(textarea);
|
||||
detail.appendChild(textarea);
|
||||
lineItem.appendChild(detail);
|
||||
detailCell.appendChild(detail);
|
||||
detailRow.appendChild(detailCell);
|
||||
lineItem.detailRow = detailRow;
|
||||
|
||||
detail.style.display = sku.value == '***' ? 'block':'none';
|
||||
|
||||
lineItem.total = total.value;
|
||||
|
||||
document.getElementById('detail_section').appendChild(lineItem);
|
||||
document.getElementById('detail_section').appendChild(lineItem);
|
||||
document.getElementById('detail_section').appendChild(detailRow);
|
||||
}
|
||||
</script>
|
||||
<input type="hidden" id="linecount" name="linecount">
|
||||
@@ -169,37 +177,83 @@ function addLine(qty,skuid,descr,text,prc,tax,tot,typ) {
|
||||
<eval name="_menu">
|
||||
<include file="adminmenu.section">
|
||||
</eval>
|
||||
<h3>Invoice $[_invoice_id]</h3>
|
||||
<sql>select * from invoices where id=$[_invoice_id]</sql>
|
||||
<entryform>
|
||||
<td>Account</td>
|
||||
<td>
|
||||
<select>
|
||||
<sql sessionid="1">select * from users</sql>
|
||||
<whilerow sessionid="1">
|
||||
<option value="$[1.id]"
|
||||
<if value1="$[.user_id]" value2="$[1.id]" type="eq">selected</if>
|
||||
>$[1.name]</option>
|
||||
</whilerow>
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr><td colspan="3">
|
||||
<div id="detail_section"></div>
|
||||
<sql>select * from invoices_detail where invoice_id=$[_invoice_id]</sql>
|
||||
<sql>select i.id as invnumber,
|
||||
u.name as name,
|
||||
u.company_name as company_name,
|
||||
u.email as email,
|
||||
date_format(i.created,'%m/%e/%Y') as invdate,
|
||||
i.status as status,
|
||||
i.user_id as user_id
|
||||
from invoices as i
|
||||
join users as u on i.user_id=u.id
|
||||
where i.id=$[@_invoice_id]</sql>
|
||||
<ifrow>
|
||||
<div style="width: 760px; max-width: 100%; margin: 0 auto; border: 1px solid rgba(197, 164, 90, 0.28); border-radius: 10px; padding: 20px; background: #0a0a0d; color: #e7e4dc; font-family: Arial, Helvetica, sans-serif; font-size: 14px;">
|
||||
<table style="width: 100%; border-collapse: collapse;" cellspacing="0" cellpadding="0"><tr>
|
||||
<td style="vertical-align: middle;"><img style="height: 90px; width: auto;" src="/assets/btglogo.png"></td>
|
||||
<td style="vertical-align: middle; text-align: right; color: #c5a45a; letter-spacing: 0.5px;">
|
||||
<div style="font-size: 24px; font-weight: 600;">Invoice: $[.invnumber]</div>
|
||||
<div style="color: #8d8c92; font-size: 12px; margin-top: 4px;">$[.status]</div>
|
||||
</td>
|
||||
</tr></table>
|
||||
<div style="height: 1px; background: rgba(197, 164, 90, 0.28); margin: 16px 0;"></div>
|
||||
<table style="border-collapse: collapse; margin-bottom: 16px; font-size: 16px; width: 100%;" cellspacing="0" cellpadding="0">
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px; border-bottom: 1px solid #000; width: 150px;">Account:</td>
|
||||
<td style="padding: 3px 16px 3px 0; border-bottom: 1px solid #000;">
|
||||
<select name="user_id" style="width: 100%; max-width: 360px;">
|
||||
<sql sessionid="1">select * from users</sql>
|
||||
<whilerow sessionid="1">
|
||||
<option value="$[1.id]"
|
||||
<if value1="$[.user_id]" value2="$[1.id]" type="eq">selected</if>
|
||||
>$[1.name]</option>
|
||||
</whilerow>
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px; border-bottom: 1px solid #000;">Company Name:</td><td style="color: #e7e4dc; text-align: left; padding: 3px 16px 3px 0; border-bottom: 1px solid #000;">$[.company_name]</td></tr>
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px; border-bottom: 1px solid #000;">E-Mail:</td><td style="color: #e7e4dc; text-align: left; padding: 3px 16px 3px 0; border-bottom: 1px solid #000;">$[.email]</td></tr>
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px;">Invoice Date:</td><td style="color: #e7e4dc; text-align: left; padding: 3px 16px 3px 0;">$[.invdate]</td></tr>
|
||||
</table>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; background: #16161c; border-radius: 6px;" cellspacing="0" cellpadding="0">
|
||||
<thead>
|
||||
<tr><td style="background-color: #1c1c24; color: #c5a45a; width: 54px; padding: 6px 4px; text-align: center; border-bottom: 1px solid #000;"></td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; width: 54px; padding: 6px 8px; text-align: center; border-bottom: 1px solid #000;">Qty</td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; padding: 6px 8px; border-bottom: 1px solid #000;">Item / Description</td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; width: 90px; padding: 6px 8px; text-align: center; border-bottom: 1px solid #000;">Unit Price</td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; width: 70px; padding: 6px 8px; text-align: center; border-bottom: 1px solid #000;">Tax</td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; width: 90px; padding: 6px 8px; text-align: center; border-bottom: 1px solid #000;">Amount</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="detail_section"></tbody>
|
||||
<tfoot>
|
||||
<tr><td colspan="5" style="text-align: right; padding: 10px 8px; color: #e3c47e; font-weight: bold;">Invoice Total:</td>
|
||||
<td id="totalsum" style="text-align: right; padding: 10px 8px; color: #e3c47e; font-weight: bold;"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<sql>select * from invoices_detail where invoice_id=$[@_invoice_id]</sql>
|
||||
<whilerow>
|
||||
<call pgm="/bin/sh" arg1="-c" arg2="jq -R -s . | sed 's#/#\\/#g'" input="$[.sku]" name="detail_sku_js">
|
||||
<call pgm="/bin/sh" arg1="-c" arg2="jq -R -s . | sed 's#/#\\/#g'" input="$[.description]" name="detail_description_js">
|
||||
<call pgm="/bin/sh" arg1="-c" arg2="jq -R -s . | sed 's#/#\\/#g'" input="$[.text]" name="detail_text_js">
|
||||
<call pgm="/bin/sh" arg1="-c" arg2="jq -R -s . | sed 's#/#\\/#g'" input="$[.type]" name="detail_type_js">
|
||||
<script>
|
||||
addLine('$[.quantity]','$[.sku]','$[.description]','$[.text]','$[.price]','$[.tax_amount]','$[.total]','$[.type]');
|
||||
</script>
|
||||
addLine('$[.quantity]',$[detail_sku_js],$[detail_description_js],$[detail_text_js],'$[.price]','$[.tax_amount]','$[.total]',$[detail_type_js]);
|
||||
</script>
|
||||
</whilerow>
|
||||
<button type="button" onclick="addLine('1','','','','0.00','0.00','0.00','');">+</button>
|
||||
<div id="totalsum" style="border: 1pt solid #000f; padding: 3px; width: 100px; text-align: right; float: right;"></div>
|
||||
<div style="margin-top: 10px;">
|
||||
<button type="button" class="button" onclick="addLine('1','','','','0.00','0.00','0.00','');">+ Add Line</button>
|
||||
</div>
|
||||
<script>
|
||||
totalInvoice();
|
||||
</script>
|
||||
<buttonbay>
|
||||
<buttonbay style="display: block; margin-top: 18px; text-align: right;">
|
||||
<linkbutton process="update_invoice"><div class="button">Update Invoice</div></linkbutton>
|
||||
</buttonbay>
|
||||
</entryform>
|
||||
</div>
|
||||
<else>
|
||||
<div style="width: 760px; max-width: 100%; margin: 40px auto; text-align: center; color: #8d8c92;">Invoice not found.</div>
|
||||
</ifrow>
|
||||
<br>
|
||||
</database>
|
||||
</contents>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#api POST /email/message
|
||||
#api POST /email/flag
|
||||
#api POST /email/delete
|
||||
#api POST /email/emptyfolder
|
||||
#api POST /email/trash
|
||||
#api POST /email/folders
|
||||
#api POST /email/search
|
||||
@@ -12,10 +13,15 @@
|
||||
#api POST /email/switchaccount
|
||||
<header name="Content-Type" value="application/json">
|
||||
<if value1="$[REQUEST_URI]" value2="/email/connect" type="eq">
|
||||
<if value1="$[host]" value2="" type="eq">
|
||||
<set name="host" value="mail.barant.com">
|
||||
</if>
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[user]" password="$[password]" sessionid="">
|
||||
<eval name="_imap_accounts"><imapaccounts action="add" list="$[@_imap_accounts]" user="$[user]" password="$[password]"></imapaccounts></eval>
|
||||
<imap config="./email.cfg" host="$[host]" user="$[user]" password="$[password]" sessionid="">
|
||||
<eval name="_imap_accounts"><imapaccounts action="add" list="$[@_imap_accounts]" host="$[host]" user="$[user]" password="$[password]"></imapaccounts></eval>
|
||||
<set name="@_imap_accounts" value="$[_imap_accounts]">
|
||||
<set name="_imap_host" value="$[host]">
|
||||
<set name="@_imap_host" value="$[host]">
|
||||
<set name="_imap_user" value="$[user]">
|
||||
<set name="@_imap_user" value="$[user]">
|
||||
<set name="_imap_password" value="$[password]">
|
||||
@@ -24,7 +30,6 @@
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/disconnect" type="eq">
|
||||
@@ -36,22 +41,25 @@
|
||||
</if>
|
||||
<eval name="_next_user"><imapaccounts action="firstuser" list="$[@_imap_accounts]"></imapaccounts></eval>
|
||||
<if value1="$[_next_user]" value2="" type="eq">
|
||||
<set name="@_imap_host" value="">
|
||||
<set name="@_imap_user" value="">
|
||||
<set name="@_imap_password" value="">
|
||||
{"ok":true,"active":""}
|
||||
<else>
|
||||
<try>
|
||||
<eval name="_next_host"><imapaccounts action="gethost" list="$[@_imap_accounts]" user="$[_next_user]"></imapaccounts></eval>
|
||||
<eval name="_next_password"><imapaccounts action="getpassword" list="$[@_imap_accounts]" user="$[_next_user]"></imapaccounts></eval>
|
||||
<imap config="./email.cfg" user="$[_next_user]" password="$[_next_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[_next_host]" user="$[_next_user]" password="$[_next_password]" sessionid="">
|
||||
<set name="@_imap_host" value="$[_next_host]">
|
||||
<set name="@_imap_user" value="$[_next_user]">
|
||||
<set name="@_imap_password" value="$[_next_password]">
|
||||
{"ok":true,"active":"$[_next_user]"}
|
||||
</imap>
|
||||
<catch name="_disconnecterr">
|
||||
<set name="@_imap_host" value="">
|
||||
<set name="@_imap_user" value="">
|
||||
<set name="@_imap_password" value="">
|
||||
{"ok":true,"active":""}
|
||||
</catch>
|
||||
</try>
|
||||
</if>
|
||||
<else>
|
||||
@@ -63,7 +71,7 @@
|
||||
</if>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/list" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<if value1="$[limit]" value2="" type="eq">
|
||||
<set name="limit" value="50">
|
||||
</if>
|
||||
@@ -71,67 +79,82 @@
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/message" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imapmessage sessionid="" folder="$[folder]" uid="$[uid]">
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/flag" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imapflag sessionid="" folder="$[folder]" uid="$[uid]" flag="$[flag]" set="$[set]">
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/delete" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imapdelete sessionid="" folder="$[folder]" uid="$[uid]">
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/emptyfolder" type="eq">
|
||||
<if value1="$[folder]" value2="Spam" type="eq">
|
||||
<set name="_emptyok" value="1">
|
||||
<else>
|
||||
<if value1="$[folder]" value2="Trash" type="eq">
|
||||
<set name="_emptyok" value="1">
|
||||
<else>
|
||||
<set name="_emptyok" value="0">
|
||||
</if>
|
||||
</if>
|
||||
<if value1="$[_emptyok]" value2="1" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imapemptyfolder sessionid="" folder="$[folder]">
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</try>
|
||||
<else>
|
||||
{"ok":false,"error":"Only the Spam and Trash folders can be emptied."}
|
||||
</if>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/trash" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imapmove sessionid="" folder="$[folder]" uid="$[uid]" destfolder="Trash">
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/folders" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imapfolders sessionid="">
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/search" type="eq">
|
||||
<try>
|
||||
<imap config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[@_imap_host]" user="$[@_imap_user]" password="$[@_imap_password]" sessionid="">
|
||||
<imapsearch sessionid="" folder="$[folder]" query="$[query]">
|
||||
</imap>
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"$[_imaperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/send" type="eq">
|
||||
@@ -139,7 +162,6 @@
|
||||
<smtpsend config="./email.cfg" user="$[@_imap_user]" password="$[@_imap_password]" to="$[to]" subject="$[subject]" html="$[body]">
|
||||
<catch name="_smtperr">
|
||||
{"ok":false,"error":"$[_smtperr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/accounts" type="eq">
|
||||
@@ -147,11 +169,13 @@
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/email/switchaccount" type="eq">
|
||||
<try>
|
||||
<eval name="_switch_host"><imapaccounts action="gethost" list="$[@_imap_accounts]" user="$[user]"></imapaccounts></eval>
|
||||
<eval name="_switch_password"><imapaccounts action="getpassword" list="$[@_imap_accounts]" user="$[user]"></imapaccounts></eval>
|
||||
<if value1="$[_switch_password]" value2="" type="eq">
|
||||
{"ok":false,"error":"That account is not connected."}
|
||||
<else>
|
||||
<imap config="./email.cfg" user="$[user]" password="$[_switch_password]" sessionid="">
|
||||
<imap config="./email.cfg" host="$[_switch_host]" user="$[user]" password="$[_switch_password]" sessionid="">
|
||||
<set name="@_imap_host" value="$[_switch_host]">
|
||||
<set name="@_imap_user" value="$[user]">
|
||||
<set name="@_imap_password" value="$[_switch_password]">
|
||||
{"ok":true}
|
||||
@@ -159,7 +183,6 @@
|
||||
</if>
|
||||
<catch name="_switcherr">
|
||||
{"ok":false,"error":"$[_switcherr]"}
|
||||
</catch>
|
||||
</try>
|
||||
<else>
|
||||
{"ok":false,"error":"Unknown route."}
|
||||
@@ -170,6 +193,7 @@
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
|
||||
@@ -5,54 +5,53 @@
|
||||
date_format(i.created,'%m/%e/%Y') as invdate,
|
||||
i.status as status,
|
||||
i.amount as amount
|
||||
from invoices as i
|
||||
from invoices as i
|
||||
join users as u on i.user_id=u.id
|
||||
where i.id=$[_invoice_id]</sql>
|
||||
where i.id=$[@_invoice_id]</sql>
|
||||
<set name="amount" value="$[.amount]">
|
||||
<div style="width: 500px; border: 1pt solid #000; padding: 3px; background: #fff;">
|
||||
<div><div style="display: inline-block;"><img style="width: 300px;" src="/assets/invoice_logo.png"></div>
|
||||
<div style="display: flex; float: right; align-items: center;">
|
||||
Invoice: $[.invnumber] $[.status]
|
||||
</div>
|
||||
</div>
|
||||
<div style="background: #eee;">
|
||||
<div>Name: $[.name]</div>
|
||||
<div>Company Name: $[.company_name]</div>
|
||||
<div>E-Mail $[.email]</div>
|
||||
<div>Invoice Date: $[.invdate]</div>
|
||||
</div>
|
||||
<div style="background: #ddd;">
|
||||
<sql>select * from invoices_detail where invoice_id=$[_invoice_id]</sql>
|
||||
<table style="border: 0px;" cellspacing="0">
|
||||
<tr><td style="background-color: #888; color: #fff; width: 50px; padding:3px; text-align: center; border: 1pt solid #ddd;">Qty</td>
|
||||
<td style="background-color: #888; color: #fff; width: 400px; padding: 3px; border: 1pt solid #ddd;">Description</td>
|
||||
<td style="background-color: #888; color: #fff; width: 60px; padding: 3px; text-align: center; border: 1pt solid #ddd;">Unit Price</td>
|
||||
<td style="background-color: #888; color: #fff; width: 60px; padding: 3px; text-align: center; border: 1pt solid #ddd;">Amount</td>
|
||||
<set name="invoicestatus" value="$[.status]">
|
||||
<div style="width: 600px; max-width: 100%; margin: 0 auto; border: 1px solid rgba(197, 164, 90, 0.28); border-radius: 10px; padding: 20px; background: #0a0a0d; color: #e7e4dc; font-family: Arial, Helvetica, sans-serif; font-size: 14px;">
|
||||
<table style="width: 100%; border-collapse: collapse;" cellspacing="0" cellpadding="0"><tr>
|
||||
<td style="vertical-align: middle;"><img style="height: 90px; width: auto;" src="https://barant.com/assets/btglogo.png"></td>
|
||||
<td style="vertical-align: middle; text-align: right; color: #c5a45a; letter-spacing: 0.5px;">
|
||||
<div style="font-size: 24px; font-weight: 600;">Invoice: $[.invnumber]</div>
|
||||
<div style="color: #8d8c92; font-size: 12px; margin-top: 4px;">$[.status]</div>
|
||||
</td>
|
||||
</tr></table>
|
||||
<div style="height: 1px; background: rgba(197, 164, 90, 0.28); margin: 16px 0;"></div>
|
||||
<table style="border-collapse: collapse; margin-bottom: 16px; font-size: 16px;" cellspacing="0" cellpadding="0">
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px; border-bottom: 1px solid #000;">Name:</td><td style="color: #e7e4dc; text-align: left; padding: 3px 16px 3px 0; border-bottom: 1px solid #000;">$[.name]</td></tr>
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px; border-bottom: 1px solid #000;">Company Name:</td><td style="color: #e7e4dc; text-align: left; padding: 3px 16px 3px 0; border-bottom: 1px solid #000;">$[.company_name]</td></tr>
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px; border-bottom: 1px solid #000;">E-Mail:</td><td style="color: #e7e4dc; text-align: left; padding: 3px 16px 3px 0; border-bottom: 1px solid #000;">$[.email]</td></tr>
|
||||
<tr><td style="color: #8d8c92; text-align: right; padding: 3px 10px 3px 16px;">Invoice Date:</td><td style="color: #e7e4dc; text-align: left; padding: 3px 16px 3px 0;">$[.invdate]</td></tr>
|
||||
</table>
|
||||
<sql>select * from invoices_detail where invoice_id=$[@_invoice_id]</sql>
|
||||
<table style="width: 100%; border-collapse: collapse; background: #16161c; border-radius: 6px;" cellspacing="0" cellpadding="0">
|
||||
<tr><td style="background-color: #1c1c24; color: #c5a45a; width: 50px; padding: 6px 8px; text-align: center; border-bottom: 1px solid #000;">Qty</td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; width: 400px; padding: 6px 8px; border-bottom: 1px solid #000;">Description</td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; width: 60px; padding: 6px 8px; text-align: center; border-bottom: 1px solid #000;">Unit Price</td>
|
||||
<td style="background-color: #1c1c24; color: #c5a45a; width: 60px; padding: 6px 8px; text-align: center; border-bottom: 1px solid #000;">Amount</td>
|
||||
</tr>
|
||||
<whilerow>
|
||||
<tr><td style="width: 50px; vertical-align: top; text-align: center; border: 1pt solid #ddd;">
|
||||
<tr><td style="width: 50px; vertical-align: top; text-align: center; color: #e7e4dc; padding: 6px 8px; border-bottom: 1px solid #000;">
|
||||
<if value1="$[.sku]" value2="***" type="ne">$[quantity]</if></td>
|
||||
<td style="width: 400px; border: 1pt solid #ddd;">
|
||||
<td style="width: 400px; vertical-align: top; color: #e7e4dc; padding: 6px 8px; border-bottom: 1px solid #000;">
|
||||
<if value1="$[.sku]" value2="***" type="ne">$[description]<br></if>
|
||||
$[text]</td>
|
||||
<td style="width: 60px; vertical-align: top; text-align: right; border: 1pt solid #ddd;">
|
||||
<td style="width: 60px; vertical-align: top; text-align: right; color: #e7e4dc; padding: 6px 8px; border-bottom: 1px solid #000;">
|
||||
<if value1="$[.sku]" value2="***" type="ne">$[price]</if></td>
|
||||
<td style="width: 60px; vertical-align: top; text-align: right; border: 1pt solid #ddd;">$[total]</td>
|
||||
<td style="width: 60px; vertical-align: top; text-align: right; color: #e7e4dc; padding: 6px 8px; border-bottom: 1px solid #000;">$[total]</td>
|
||||
</tr>
|
||||
</whilerow>
|
||||
<tr><td style="width: 50px; border: 0pt solid #000;"></td>
|
||||
<td style="width: 400px; text-align: right; border: 0pt solid #000;" colspan="2">Invoice Total:</td>
|
||||
<td style="width: 60px; text-align: right; border: 1pt solid #000;">$[amount]</td>
|
||||
<tr><td style="width: 50px;"></td>
|
||||
<td style="width: 400px; text-align: right; padding: 10px 8px; color: #e3c47e; font-weight: bold;" colspan="2">Invoice Total:</td>
|
||||
<td style="width: 60px; text-align: right; padding: 10px 8px; color: #e3c47e; font-weight: bold;">$[amount]</td>
|
||||
</tr>
|
||||
<tr><td colspan="4" style="width: 50px; border: 0pt solid #000;"><hr></td>
|
||||
</tr>
|
||||
<sql>select * from payments where invoice_id=$[_invoice_id]</sql>
|
||||
<whilerow>
|
||||
<tr><td style="width: 50px; border: 0pt solid #000;"></td>
|
||||
<td colspan="2" style="width: 400px; text-align: right; border: 0pt solid #000;">Credit Card: $[.card_brand] ending in $[.last_4_digits] expires on $[.exp_month]/$[.exp_year]</td>
|
||||
<td style="width: 60px; text-align: right; border: 1pt solid #000;">$[.amount]</td>
|
||||
</tr>
|
||||
</whilerow>
|
||||
</table>
|
||||
</div>
|
||||
</table>
|
||||
<sql>select * from payments where invoice_id=$[@_invoice_id]</sql>
|
||||
<whilerow>
|
||||
<div style="margin-top: 12px; text-align: right; color: #8d8c92; font-size: 12px;">
|
||||
Credit Card: $[.card_brand] ending in $[.last_4_digits] expires $[.exp_month]/$[.exp_year]
|
||||
</div>
|
||||
</whilerow>
|
||||
</div>
|
||||
@@ -1,67 +0,0 @@
|
||||
<page secure="yes" loggedin="no">
|
||||
<title>JUKEBOX</title>
|
||||
<contents>
|
||||
<script>
|
||||
var index = 0;
|
||||
const paths = [];
|
||||
const songs = [];
|
||||
function onselection(path,selection) {
|
||||
var name = path + '/' + selection;
|
||||
var player = document.getElementById('player');
|
||||
var audio = document.getElementById('audio');
|
||||
var title = document.getElementById('title');
|
||||
player.src = name;
|
||||
title.textContent = selection;
|
||||
audio.load();
|
||||
audio.play();
|
||||
}
|
||||
function selectNext() {
|
||||
var index = Math.floor(Math.random()*songs.length) + 1;
|
||||
onselection(paths[index],songs[index]);
|
||||
}
|
||||
</script>
|
||||
<retain name="download_file">
|
||||
<div style="display: inline-block;">
|
||||
<audio id="audio" controls onended="selectNext();">
|
||||
<source id="player" type="audio/mp3">
|
||||
</audio>
|
||||
</div>
|
||||
<div style="font-size: 20px; display: inline-block;" id="title">Click on a title below</div>
|
||||
<sectionframe title="Old Library">
|
||||
<div style="height: 300px; overflow-y: scroll;">
|
||||
<whiledir name="file" dir="musiclink/library">
|
||||
<if expr="left('$[file]',1) != '.' ">
|
||||
<div style="cursor: pointer;" onclick="onselection('musiclink/library','$[file]');">$[file]</div>
|
||||
<script>
|
||||
paths[index]='musiclink/library';
|
||||
songs[index++]='$[file]';
|
||||
</script>
|
||||
</if>
|
||||
</whiledir>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</sectionframe>
|
||||
|
||||
<sectionframe title="Ian's Library">
|
||||
<div style="height: 300px; overflow-y: scroll;">
|
||||
<whiledir name="file" dir="musiclink/ianslibrary">
|
||||
<if expr="left('$[file]',1) != '.' ">
|
||||
<div style="cursor: pointer;" onclick="onselection('musiclink/ianslibrary','$[file]');">$[file]</div>
|
||||
<script>
|
||||
paths[index]='musiclink/ianslibrary';
|
||||
songs[index++]='$[file]';
|
||||
</script>
|
||||
</if>
|
||||
</whiledir>
|
||||
</div>
|
||||
</sectionframe>
|
||||
<script>
|
||||
function printName(name) {
|
||||
document.write(name + '\n');
|
||||
}
|
||||
//songs.forEach(printName);
|
||||
</script>
|
||||
|
||||
</contents>
|
||||
</page>
|
||||
@@ -14,7 +14,20 @@
|
||||
<setenv name="_login_type">$[login_type]</setenv>
|
||||
<setenv name="_login_uuid">$[login_uuid]</setenv>
|
||||
<sql>update users set last_login=now() where id=$[login_id]</sql>
|
||||
<if value1="$[@start]" value2="" type="eq">
|
||||
<sql>select group_concat(to_base64(host),':',to_base64(email),':',to_base64(password) order by id separator ';') as accounts_list,
|
||||
(select host from email_accounts where user_id=$[login_id] order by id limit 1) as first_host,
|
||||
(select email from email_accounts where user_id=$[login_id] order by id limit 1) as first_email,
|
||||
(select password from email_accounts where user_id=$[login_id] order by id limit 1) as first_password
|
||||
from email_accounts where user_id=$[login_id]</sql>
|
||||
<ifrow>
|
||||
<if value1="$[.accounts_list]" value2="" type="ne">
|
||||
<set name="@_imap_accounts" value="$[.accounts_list]">
|
||||
<set name="@_imap_host" value="$[.first_host]">
|
||||
<set name="@_imap_user" value="$[.first_email]">
|
||||
<set name="@_imap_password" value="$[.first_password]">
|
||||
</if>
|
||||
</ifrow>
|
||||
<if value1="$[@start]" value2="" type="eq">
|
||||
<setpage name="account">
|
||||
<else>
|
||||
<setpage name="$[@start]">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#api POST /admin/mail-invoice
|
||||
<set name="host" value="localhost">
|
||||
<set name="database" value="barant">
|
||||
<set name="user" value="barant">
|
||||
<set name="password" value="uversa1960">
|
||||
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
|
||||
<header name="Content-Type" value="application/json">
|
||||
<if value1="$[@_login_type]" value2="admin" type="ne">
|
||||
{"ok":false,"error":"Not authorized."}
|
||||
<else>
|
||||
<if value1="$[@_invoice_id]" value2="" type="eq">
|
||||
{"ok":false,"error":"No invoice selected."}
|
||||
<else>
|
||||
<if value1="$[email]" value2="" type="eq">
|
||||
{"ok":false,"error":"Please enter an e-mail address."}
|
||||
<else>
|
||||
<try>
|
||||
<set name="_invoice_email_override" value="$[email]">
|
||||
<include file="send_invoice_email.method">{"ok":true}
|
||||
<catch name="_mailerr">
|
||||
{"ok":false,"error":"$[_mailerr]"}
|
||||
</try>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</database>
|
||||
@@ -0,0 +1,43 @@
|
||||
#api POST /admin/add-mail-account
|
||||
#api POST /admin/remove-mail-account
|
||||
<set name="host" value="localhost">
|
||||
<set name="database" value="barant">
|
||||
<set name="user" value="barant">
|
||||
<set name="password" value="uversa1960">
|
||||
<database name="$[database]" host="$[host]" user="$[user]" password="$[password]" sessionid="">
|
||||
<header name="Content-Type" value="application/json">
|
||||
<if value1="$[@_login_type]" value2="admin" type="ne">
|
||||
{"ok":false,"error":"Not authorized."}
|
||||
<else>
|
||||
<if value1="$[REQUEST_URI]" value2="/admin/add-mail-account" type="eq">
|
||||
<if value1="$[mail_host]" value2="" type="eq">
|
||||
{"ok":false,"error":"Please enter a mail server."}
|
||||
<else>
|
||||
<if value1="$[mail_email]" value2="" type="eq">
|
||||
{"ok":false,"error":"Please enter an e-mail address."}
|
||||
<else>
|
||||
<if value1="$[mail_password]" value2="" type="eq">
|
||||
{"ok":false,"error":"Please enter a password."}
|
||||
<else>
|
||||
<try>
|
||||
<imap config="./email.cfg" host="$[mail_host]" user="$[mail_email]" password="$[mail_password]" sessionid="">
|
||||
</imap>
|
||||
<sql>insert into email_accounts (user_id,email,host,password) values ($[@_login_id],'$[mail_email]','$[mail_host]','$[mail_password]')
|
||||
on duplicate key update host='$[mail_host]', password='$[mail_password]'</sql>
|
||||
{"ok":true}
|
||||
<catch name="_imaperr">
|
||||
{"ok":false,"error":"Could not connect with those credentials: $[_imaperr]"}
|
||||
</try>
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
<else>
|
||||
<if value1="$[account_id]" value2="" type="eq">
|
||||
{"ok":false,"error":"No account specified."}
|
||||
<else>
|
||||
<sql>delete from email_accounts where id=$[account_id] and user_id=$[@_login_id]</sql>
|
||||
{"ok":true}
|
||||
</if>
|
||||
</if>
|
||||
</if>
|
||||
</database>
|
||||
@@ -0,0 +1,7 @@
|
||||
<retain name="download_file">
|
||||
<sectionframe title="Ian's Library">
|
||||
<div class="music-song-list" id="music-list-ianslibrary"></div>
|
||||
</sectionframe>
|
||||
<script>
|
||||
__renderMusicFolder('music-list-ianslibrary', 'ianslibrary', '/assets/musiclink/ianslibrary');
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<retain name="download_file">
|
||||
<sectionframe title="Old Library">
|
||||
<div class="music-song-list" id="music-list-library"></div>
|
||||
</sectionframe>
|
||||
<script>
|
||||
__renderMusicFolder('music-list-library', 'library', '/assets/musiclink/library');
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<retain name="download_file">
|
||||
<sectionframe title="BARANT Audio Experiments">
|
||||
<div class="music-song-list" id="music-list-bae"></div>
|
||||
</sectionframe>
|
||||
<script>
|
||||
__renderMusicFolder('music-list-bae', 'bae', '/assets/musiclink/bae');
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<sectionframe title="Welcome to BARANT Systems">
|
||||
|
||||
<div style="width: 48%;">
|
||||
<p>We are BARANT Technology Group. Contact us for information on how we can
|
||||
help you with your technical objectives.</p>
|
||||
|
||||
<h1>Modular1 VST Synthesizer</h1>
|
||||
<p>Check out our <a href="/vst">Modular1 VST3
|
||||
Synthesizer</a> If you appreciate the art of modular analog
|
||||
synthesizers then you will really enjoy the Modular1 with all the
|
||||
features of an analog modular synthesizer put into a single VST3
|
||||
instrument. With the added features of polyphony and patches that
|
||||
change instantaneously this synthesizer is truly a powerhouse in the
|
||||
audio toolbox.</p>
|
||||
<a href="/vst"><img src="/assets/modular1.png" style="width: 200px;" align="left"></a>
|
||||
<p>Excellent quality and technical performance provide a performance level
|
||||
instrument for musicians or students who wish to learn sound wave
|
||||
synthesis techniques or for musicians to enable that old analog sound
|
||||
in their DAW recording system..</p>
|
||||
<p>Checkout the product page for more information on the capabilities
|
||||
of the Modular1 synthesizer.</p>
|
||||
</div>
|
||||
</sectionframe>
|
||||
@@ -1,7 +1,29 @@
|
||||
<sectionframe title="MAIN">
|
||||
<if value1="$[SUBPAGE]" value2="" type="eq">
|
||||
<linkbutton page="main" class="submenuitem-active">OVERVIEW</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="main">OVERVIEW</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="music" type="eq">
|
||||
<linkbutton page="main" subpage="music" class="submenuitem-active">MUSIC</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="main" subpage="music">MUSIC</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="library" type="eq">
|
||||
<linkbutton page="main" subpage="library" class="submenuitem-active">MATT'S LIBRARY</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="main" subpage="library">MATT'S LIBRARY</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="ianslibrary" type="eq">
|
||||
<linkbutton page="main" subpage="ianslibrary" class="submenuitem-active">IAN'S LIBRARY</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="main" subpage="ianslibrary">IAN'S LIBRARY</linkbutton><br>
|
||||
</if>
|
||||
</sectionframe>
|
||||
<sectionframe title="QUICK LINKS">
|
||||
<linkbutton page="services">SERVICES</linkbutton><br>
|
||||
<linkbutton page="hosting">HOSTING</linkbutton><br>
|
||||
<linkbutton page="siptrunk">SIP TRUNKING</linkbutton><br>
|
||||
<linkbutton page="services" subpage="hosting">HOSTING</linkbutton><br>
|
||||
<linkbutton page="services" subpage="siptrunking">SIP TRUNKING</linkbutton><br>
|
||||
<linkbutton page="opensource">OPEN SOURCE</linkbutton><br>
|
||||
<linkbutton page="contact">CONTACT</linkbutton>
|
||||
</sectionframe>
|
||||
@@ -2,28 +2,16 @@
|
||||
<title>BARANT Technology Group</title>
|
||||
<contents>
|
||||
<setenv name="_menubar"></setenv>
|
||||
<sectionframe title="Welcome to BARANT Systems">
|
||||
|
||||
<div style="width: 48%;">
|
||||
<p>We are BARANT Technology Group. Contact us for information on how we can
|
||||
help you with your technical objectives.</p>
|
||||
|
||||
<h1>Modular1 VST Synthesizer</h1>
|
||||
<p>Check out our <a href="/vst">Modular1 VST3
|
||||
Synthesizer</a> If you appreciate the art of modular analog
|
||||
synthesizers then you will really enjoy the Modular1 with all the
|
||||
features of an analog modular synthesizer put into a single VST3
|
||||
instrument. With the added features of polyphony and patches that
|
||||
change instantaneously this synthesizer is truly a powerhouse in the
|
||||
audio toolbox.</p>
|
||||
<a href="/vst"><img src="/assets/modular1.png" style="width: 200px;" align="left"></a>
|
||||
<p>Excellent quality and technical performance provide a performance level
|
||||
instrument for musicians or students who wish to learn sound wave
|
||||
synthesis techniques or for musicians to enable that old analog sound
|
||||
in their DAW recording system..</p>
|
||||
<p>Checkout the product page for more information on the capabilities
|
||||
of the Modular1 synthesizer.</p>
|
||||
</div>
|
||||
</sectionframe>
|
||||
<div id="main-subpage">
|
||||
<if value1="$[SUBPAGE]" value2="" type="eq">
|
||||
<include file="main-overview.subpage">
|
||||
<else>
|
||||
<try>
|
||||
<include file="main-$[SUBPAGE].subpage">
|
||||
<catch>
|
||||
<p>That page doesn't exist.</p>
|
||||
</try>
|
||||
</if>
|
||||
</div>
|
||||
</contents>
|
||||
</page>
|
||||
@@ -72,7 +72,7 @@ $[_BODY]
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>BARANT Technology Group</title>
|
||||
<link rel="stylesheet" href="/assets/main.css?v=20260908a">
|
||||
<link rel="stylesheet" href="/assets/main.css?v=20261006a">
|
||||
<link rel="stylesheet" href="/assets/control.css?v=20260902">
|
||||
</head>
|
||||
<body>
|
||||
@@ -88,13 +88,753 @@ $[_BODY]
|
||||
<div class="pageclass">
|
||||
<div class="heading">
|
||||
<div style="display: inline-block; width: 500px;">
|
||||
<a href="https://barant.com"><img style="width: 60px; height: 60px; margin: 10px; background-color: #000; border-radius: 6px; padding: 4px; box-sizing: border-box;" align="left" src="/assets/btglogo.png"></a>
|
||||
<div style="padding-top: 15px;">
|
||||
<div style="left: 80px; color: #c5a45a; font-size: 20pt; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600;">BARANT TECHNOLOGY GROUP</div>
|
||||
</div>
|
||||
<a href="https://barant.com"><img style="height: 100px; width: auto; margin: 10px; border-radius: 6px; box-sizing: border-box;" align="left" src="/assets/btglogo.png"></a>
|
||||
</div>
|
||||
<div id="loginmenu" style="float: right; text-align: right; margin-right: 10px; margin-top: 5px;"><include file="login.menu"></div>
|
||||
</div>
|
||||
<div id="globalplayer-band" class="globalplayer-band">
|
||||
<div id="globalplayer-playpause" class="globalplayer-btn" title="Play/Pause" onclick="__playerTogglePause();">
|
||||
<svg id="globalplayer-playicon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg id="globalplayer-pauseicon" viewBox="0 0 24 24" fill="currentColor" style="display:none;"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>
|
||||
</div>
|
||||
<div id="globalplayer-stop" class="globalplayer-btn" title="Stop" onclick="__playerStop();">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12"/></svg>
|
||||
</div>
|
||||
<div id="globalplayer-skip" class="globalplayer-btn" title="Skip" onclick="__playerAdvance();">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 5v14l8-7zM16 5h2v14h-2z"/></svg>
|
||||
</div>
|
||||
<div id="globalplayer-title" class="globalplayer-title">
|
||||
<div id="globalplayer-duration" class="globalplayer-duration"></div>
|
||||
<div id="globalplayer-random-indicator" class="globalplayer-random-indicator">RANDOM</div>
|
||||
<div id="globalplayer-title-text" class="globalplayer-title-text">Not Playing</div>
|
||||
<div id="globalplayer-progress" class="globalplayer-progress"><div id="globalplayer-progress-fill" class="globalplayer-progress-fill"></div><div id="globalplayer-progress-handle" class="globalplayer-progress-handle"></div></div>
|
||||
</div>
|
||||
<input type="range" id="globalplayer-volume" class="globalplayer-volume" min="0" max="100" value="<if value1="$[@_player_volume]" value2="" type="eq">100<else>$[@_player_volume]</if>" title="Volume" oninput="__playerSetVolume(this.value);" onchange="__playerPersistVolume(this.value);">
|
||||
<canvas id="globalplayer-eq" class="globalplayer-eq" width="60" height="32"></canvas>
|
||||
<div id="globalplayer-queue-toggle" class="globalplayer-btn globalplayer-btn-text" title="Playlist" onclick="__playerToggleQueue();">RANDOM</div>
|
||||
<audio id="globalaudio" style="display:none;"></audio>
|
||||
</div>
|
||||
<script type="application/json" id="globalplayer-saved-state">$[@_player_state]</script>
|
||||
<div id="globalplayer-queue" class="globalplayer-queue"></div>
|
||||
<script>
|
||||
// Global, persistent music player + playlist queue - lives here (outside
|
||||
// the #content area) specifically so it survives spa.js's per-navigation
|
||||
// content.innerHTML swap: this whole full-page-load branch of
|
||||
// mainpagetemplate.jet only ever renders once per real browser
|
||||
// navigation, never again on a client-side SPA page change (the
|
||||
// fragment branch further below renders none of this) - so this
|
||||
// audio element and the playlist array it drives keep running
|
||||
// uninterrupted across every subsequent in-app page click, exactly
|
||||
// like the request asked for (as long as you do not leave the domain).
|
||||
var __playlist = [];
|
||||
var __currentIndex = -1;
|
||||
var __isIdle = true;
|
||||
var __playToken = 0;
|
||||
// Turned on by clicking RANDOM while nothing is queued, turned back
|
||||
// off by clicking the same button (now showing HALT) - while on, the
|
||||
// queue auto-refills with another random pick instead of going idle
|
||||
// or sitting empty whenever it runs out, rather than that one click
|
||||
// only ever adding a single song.
|
||||
var __autoRandomMode = false;
|
||||
|
||||
// Display-only - the real audio.src below still uses entry.file in
|
||||
// full, extension included, since that is what the server actually
|
||||
// needs to find the right file on disk.
|
||||
function __stripExtension(filename) {
|
||||
var dot = filename.lastIndexOf('.');
|
||||
return dot > 0 ? filename.substring(0, dot) : filename;
|
||||
}
|
||||
|
||||
function __playerLoad(entry) {
|
||||
var audio = document.getElementById('globalaudio');
|
||||
var title = document.getElementById('globalplayer-title-text');
|
||||
document.getElementById('globalplayer-band').classList.add('globalplayer-open');
|
||||
__setProgress(0);
|
||||
document.getElementById('globalplayer-duration').textContent = '0:00';
|
||||
var myToken = ++__playToken;
|
||||
audio.src = entry.path + '/' + entry.file;
|
||||
title.textContent = __stripExtension(entry.file);
|
||||
audio.load();
|
||||
audio.play().catch(function(err) {
|
||||
// Same AbortError race already solved once for the old per-page
|
||||
// player (see the history of main-music.subpage) - a later selection
|
||||
// already owns recovery if it has since bumped __playToken.
|
||||
if (myToken === __playToken) __playerAdvance();
|
||||
});
|
||||
}
|
||||
|
||||
function __goIdle() {
|
||||
var audio = document.getElementById('globalaudio');
|
||||
audio.pause();
|
||||
__isIdle = true;
|
||||
document.getElementById('globalplayer-band').classList.remove('globalplayer-open');
|
||||
document.getElementById('globalplayer-title-text').textContent = 'Not Playing';
|
||||
__setProgress(0);
|
||||
document.getElementById('globalplayer-duration').textContent = '';
|
||||
}
|
||||
|
||||
// The song-picker row stays marked (checkmark + flash) via addToPlaylist
|
||||
// for as long as its entry is actually sitting in __playlist - this
|
||||
// clears that mark once a song is no longer there, whether it finished
|
||||
// naturally or was deleted via the remove button. Matches by data-path/
|
||||
// data-file rather than building a CSS attribute-selector string, since
|
||||
// real filenames can contain quotes/other characters that would need
|
||||
// escaping to use safely in one.
|
||||
function __unmarkSongRow(path, file) {
|
||||
document.querySelectorAll('.music-song-row').forEach(function(row) {
|
||||
if (row.dataset.path === path && row.dataset.file === file) {
|
||||
row.classList.remove('song-queued');
|
||||
row.classList.remove('song-queued-flash');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Shared by __playerAdvance and __removeFromPlaylist - whichever path
|
||||
// the queue became empty through, the same rule applies: auto-random
|
||||
// mode refills it with another pick instead of actually going idle.
|
||||
function __goIdleOrAutoFill() {
|
||||
if (__autoRandomMode) {
|
||||
__isIdle = true;
|
||||
__playRandomSong();
|
||||
} else {
|
||||
__goIdle();
|
||||
}
|
||||
}
|
||||
|
||||
function __playerAdvance() {
|
||||
var finished = __playlist[__currentIndex];
|
||||
if (__currentIndex + 1 < __playlist.length) {
|
||||
__currentIndex++;
|
||||
__playerLoad(__playlist[__currentIndex]);
|
||||
} else {
|
||||
__goIdleOrAutoFill();
|
||||
}
|
||||
if (finished) __unmarkSongRow(finished.path, finished.file);
|
||||
__renderQueue();
|
||||
}
|
||||
|
||||
// Removing a not-yet-played entry just drops it. Removing the
|
||||
// CURRENTLY playing one is treated like a skip: splice it out, then
|
||||
// whatever was queued right behind it has shifted down into this same
|
||||
// index, so loading __playlist[__currentIndex] again (unchanged value,
|
||||
// now pointing at a different entry) picks it up - or goes idle if
|
||||
// that was the last one left. A row for anything already played is
|
||||
// never rendered in the dropdown in the first place (see __renderQueue
|
||||
// - it only shows current-and-later), so there is no "delete history"
|
||||
// case to handle here.
|
||||
function __removeFromPlaylist(index) {
|
||||
var removed = __playlist[index];
|
||||
if (index === __currentIndex) {
|
||||
__playlist.splice(index, 1);
|
||||
if (index < __playlist.length) {
|
||||
__playerLoad(__playlist[__currentIndex]);
|
||||
} else {
|
||||
__goIdleOrAutoFill();
|
||||
}
|
||||
} else if (index > __currentIndex) {
|
||||
__playlist.splice(index, 1);
|
||||
}
|
||||
if (removed) __unmarkSongRow(removed.path, removed.file);
|
||||
__renderQueue();
|
||||
}
|
||||
|
||||
// Only ever called with indices from rows actually rendered in the
|
||||
// dropdown, which (see __renderQueue's own comment) never include the
|
||||
// currently-playing entry - so this only ever reorders the upcoming
|
||||
// portion of the queue, never disturbs __currentIndex itself.
|
||||
function __reorderPlaylist(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex) return;
|
||||
var item = __playlist.splice(fromIndex, 1)[0];
|
||||
__playlist.splice(toIndex, 0, item);
|
||||
__renderQueue();
|
||||
}
|
||||
|
||||
function __setProgress(ratio) {
|
||||
var pct = (Math.max(0, Math.min(1, ratio)) * 100) + '%';
|
||||
document.getElementById('globalplayer-progress-fill').style.width = pct;
|
||||
document.getElementById('globalplayer-progress-handle').style.left = pct;
|
||||
}
|
||||
|
||||
function __updateTimeDisplay(current, total) {
|
||||
document.getElementById('globalplayer-duration').textContent = __formatTime(current) + ' / ' + __formatTime(total);
|
||||
}
|
||||
|
||||
// Shared with the music/jukebox subpages' own song-picker lists (not
|
||||
// just the playlist dropdown) - a lightweight metadata-only probe so
|
||||
// each row can show its own track length without needing to play it
|
||||
// first.
|
||||
// All duration probes (both this and addToPlaylist's own) funnel
|
||||
// through one strictly-sequential queue - a song list can easily have
|
||||
// 100+ rows, and firing a probe per row all at once was saturating the
|
||||
// browser's per-origin connection limit (6 concurrent, in most
|
||||
// browsers) with background metadata fetches. A real click to actually
|
||||
// play something queues up behind whichever of those happened to grab
|
||||
// a connection first, which is what made selecting a song "not work or
|
||||
// take a long time" - processing one probe at a time, each starting
|
||||
// only once the previous finishes, keeps the other 5 connection slots
|
||||
// free for an actual user-initiated request at all times.
|
||||
var __durationQueue = [];
|
||||
var __durationQueueRunning = false;
|
||||
|
||||
function __queueDurationProbe(path, file, onReady) {
|
||||
__durationQueue.push({path: path, file: file, onReady: onReady});
|
||||
__runDurationQueue();
|
||||
}
|
||||
|
||||
function __runDurationQueue() {
|
||||
if (__durationQueueRunning || __durationQueue.length === 0) return;
|
||||
__durationQueueRunning = true;
|
||||
var item = __durationQueue.shift();
|
||||
var probe = new Audio();
|
||||
probe.preload = 'metadata';
|
||||
function done() {
|
||||
__durationQueueRunning = false;
|
||||
__runDurationQueue();
|
||||
}
|
||||
probe.addEventListener('loadedmetadata', function() {
|
||||
item.onReady(probe.duration);
|
||||
done();
|
||||
});
|
||||
probe.addEventListener('error', done);
|
||||
probe.src = item.path + '/' + item.file;
|
||||
}
|
||||
|
||||
function __loadSongDuration(path, file, el) {
|
||||
if (!el) return;
|
||||
__queueDurationProbe(path, file, function(duration) {
|
||||
el.textContent = __formatTime(duration);
|
||||
});
|
||||
}
|
||||
|
||||
function __formatTime(seconds) {
|
||||
if (!seconds || isNaN(seconds) || !isFinite(seconds)) return '0:00';
|
||||
seconds = Math.floor(seconds);
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor((seconds % 3600) / 60);
|
||||
var s = seconds % 60;
|
||||
var sStr = (s < 10 ? '0' : '') + s;
|
||||
if (h > 0) {
|
||||
var mStr = (m < 10 ? '0' : '') + m;
|
||||
return h + ':' + mStr + ':' + sStr;
|
||||
}
|
||||
return m + ':' + sStr;
|
||||
}
|
||||
|
||||
function __escapeHtml(s) {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Builds a music subpage's song list client-side from the fast JSON
|
||||
// listing route (GET /assets/musiclist/{subfolder}), rather than having
|
||||
// the server render one row per file via a directory-loop tag - that
|
||||
// path was genuinely slow for these particular folders (~1200+ files
|
||||
// each), since every literal HTML element inside a Jet loop body costs
|
||||
// real, compounding interpreter overhead per iteration. The container
|
||||
// appears instantly (empty) on navigation; this fills it in shortly
|
||||
// after via one fetch, in small batches via requestAnimationFrame
|
||||
// rather than one giant synchronous DOM write, so the list visibly
|
||||
// populates rather than the page just sitting blank until everything
|
||||
// is ready at once.
|
||||
function __renderMusicFolder(containerId, subfolder, basePath) {
|
||||
var container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
fetch('/assets/musiclist/' + subfolder)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var files = data.files || [];
|
||||
var i = 0;
|
||||
function appendBatch() {
|
||||
var end = Math.min(i + 100, files.length);
|
||||
for (; i < end; i++) {
|
||||
var file = files[i];
|
||||
var row = document.createElement('div');
|
||||
row.className = 'music-song-row';
|
||||
row.dataset.path = basePath;
|
||||
row.dataset.file = file;
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'music-song-name';
|
||||
nameSpan.textContent = __stripExtension(file);
|
||||
var durSpan = document.createElement('span');
|
||||
durSpan.className = 'music-song-duration';
|
||||
row.appendChild(nameSpan);
|
||||
row.appendChild(durSpan);
|
||||
row.addEventListener('click', (function(rowEl, path, fname) {
|
||||
return function() { addToPlaylist(rowEl, path, fname); };
|
||||
})(row, basePath, file));
|
||||
container.appendChild(row);
|
||||
__loadSongDuration(basePath, file, durSpan);
|
||||
}
|
||||
if (i < files.length) requestAnimationFrame(appendBatch);
|
||||
}
|
||||
appendBatch();
|
||||
})
|
||||
.catch(function(err) {
|
||||
container.textContent = 'Could not load the song list.';
|
||||
});
|
||||
}
|
||||
|
||||
function __playerSetVolume(val) {
|
||||
document.getElementById('globalaudio').volume = val / 100;
|
||||
}
|
||||
|
||||
// Only persists on change (slider release), not on every input event
|
||||
// while dragging, so dragging does not fire a request per pixel moved.
|
||||
function __playerPersistVolume(val) {
|
||||
fetch(window.location.pathname, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'PROCESS=setvolume&volume=' + encodeURIComponent(val)
|
||||
});
|
||||
}
|
||||
|
||||
// While idle (nothing playing, nothing queued) there is nothing for a
|
||||
// playlist dropdown to show, so the same button instead offers to pick
|
||||
// something at random - its label reflects whichever of those two
|
||||
// things it would actually do right now, recomputed any time the open/
|
||||
// closed state or __isIdle changes (see __renderQueue's own call to
|
||||
// this).
|
||||
function __updateQueueButtonLabel() {
|
||||
var btn = document.getElementById('globalplayer-queue-toggle');
|
||||
var queueOpen = document.getElementById('globalplayer-queue').classList.contains('globalplayer-queue-open');
|
||||
if (queueOpen) {
|
||||
btn.textContent = 'CLOSE';
|
||||
} else if (__autoRandomMode) {
|
||||
btn.textContent = 'HALT';
|
||||
} else if (!__hasQueuedSongs()) {
|
||||
btn.textContent = 'RANDOM';
|
||||
} else {
|
||||
btn.textContent = 'PLAYLIST';
|
||||
}
|
||||
document.getElementById('globalplayer-random-indicator').classList.toggle('globalplayer-random-indicator-visible', __autoRandomMode);
|
||||
}
|
||||
|
||||
function __hasQueuedSongs() {
|
||||
return !__isIdle && __currentIndex !== -1 && (__currentIndex + 1 < __playlist.length);
|
||||
}
|
||||
|
||||
function __playerToggleQueue() {
|
||||
// Already in auto-random mode - this button's only job right now is
|
||||
// to turn it back off (does not stop whatever is currently playing,
|
||||
// just stops it from auto-refilling once that finishes).
|
||||
if (__autoRandomMode) {
|
||||
__autoRandomMode = false;
|
||||
__updateQueueButtonLabel();
|
||||
return;
|
||||
}
|
||||
// Nothing queued (whether idle or something is already playing) -
|
||||
// turn auto-random mode on and queue the first pick right away rather
|
||||
// than making this a one-shot "add one song" click.
|
||||
if (!__hasQueuedSongs()) {
|
||||
__autoRandomMode = true;
|
||||
__playRandomSong();
|
||||
return;
|
||||
}
|
||||
document.getElementById('globalplayer-queue').classList.toggle('globalplayer-queue-open');
|
||||
__updateQueueButtonLabel();
|
||||
}
|
||||
|
||||
// Picks from whatever song-picker rows happen to be rendered on the
|
||||
// CURRENT page (music/jukebox's own .music-song-row elements) - there
|
||||
// is no server-side "list all songs" endpoint to fall back on, so this
|
||||
// only has songs to choose from when the page you are actually on has
|
||||
// some. Reuses addToPlaylist exactly as a real click would (same
|
||||
// element, same path/file), so the row's own checkmark/flash feedback
|
||||
// and the queue button's label both update exactly as they would for a
|
||||
// manual click.
|
||||
function __playRandomSong() {
|
||||
var rows = document.querySelectorAll('.music-song-row');
|
||||
if (rows.length === 0) return;
|
||||
var row = rows[Math.floor(Math.random() * rows.length)];
|
||||
addToPlaylist(row, row.dataset.path, row.dataset.file, true);
|
||||
}
|
||||
|
||||
// Shows the current song as the first entry, followed by whatever is
|
||||
// still queued up behind it - not a play history, just now-playing
|
||||
// plus up-next, rebuilt every time the queue or current position changes.
|
||||
// The currently-playing entry is deliberately left out of this list -
|
||||
// it's already shown up in the LCD title/progress bar, so the dropdown
|
||||
// now only ever shows what's queued up NEXT, not what's already
|
||||
// playing.
|
||||
|
||||
// Session persistence - survives a real full-page navigation/reload
|
||||
// the same way the volume slider's own session variable already does
|
||||
// (see setvolume.proc), using one JSON blob (one session variable,
|
||||
// one .proc) rather than several separate ones so the whole player
|
||||
// state saves/restores atomically together.
|
||||
// Runs once, at setup - reads the JSON a real full-page load embedded
|
||||
// server-side (see the script#globalplayer-saved-state tag, rendered
|
||||
// from the same session variable setplayerstate.proc writes) and picks
|
||||
// playback back up from there. A plain text node's content needs none
|
||||
// of the quote-escaping embedding this directly inside a JS string
|
||||
// literal would have required, since real song filenames/paths can
|
||||
// contain characters (quotes, etc.) that would otherwise have to be
|
||||
// escaped correctly to avoid corrupting the surrounding script.
|
||||
function __restoreSavedPlayerState() {
|
||||
var stateEl = document.getElementById('globalplayer-saved-state');
|
||||
if (!stateEl) return;
|
||||
var raw = stateEl.textContent.trim();
|
||||
if (!raw) return;
|
||||
var saved;
|
||||
try {
|
||||
saved = JSON.parse(atob(raw));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!saved || !saved.playlist || !saved.playlist.length) return;
|
||||
|
||||
__playlist = saved.playlist.map(function(p) { return {path: p.path, file: p.file, duration: null}; });
|
||||
__currentIndex = Math.min(Math.max(0, saved.currentIndex || 0), __playlist.length - 1);
|
||||
__isIdle = false;
|
||||
__autoRandomMode = !!saved.autoRandomMode;
|
||||
|
||||
var entry = __playlist[__currentIndex];
|
||||
var audio = document.getElementById('globalaudio');
|
||||
document.getElementById('globalplayer-band').classList.add('globalplayer-open');
|
||||
document.getElementById('globalplayer-title-text').textContent = __stripExtension(entry.file);
|
||||
audio.src = entry.path + '/' + entry.file;
|
||||
audio.load();
|
||||
|
||||
function onceReady() {
|
||||
audio.removeEventListener('loadedmetadata', onceReady);
|
||||
if (saved.position) audio.currentTime = saved.position;
|
||||
if (saved.playing) {
|
||||
// Browsers block unprompted autoplay without a real user gesture -
|
||||
// if that happens, this just stays loaded and paused at the
|
||||
// restored position instead of erroring, the same tolerance the
|
||||
// normal play()-failure handling elsewhere already has.
|
||||
audio.play().catch(function() {});
|
||||
}
|
||||
}
|
||||
audio.addEventListener('loadedmetadata', onceReady);
|
||||
}
|
||||
|
||||
function __buildPlayerState() {
|
||||
var audio = document.getElementById('globalaudio');
|
||||
return {
|
||||
playlist: __playlist.map(function(p) { return {path: p.path, file: p.file}; }),
|
||||
currentIndex: __currentIndex,
|
||||
position: audio.currentTime || 0,
|
||||
playing: !audio.paused,
|
||||
autoRandomMode: __autoRandomMode
|
||||
};
|
||||
}
|
||||
|
||||
// base64-encoded before it ever reaches the server, not just plain
|
||||
// JSON - the session store is itself JSON-backed (JFile), and a raw
|
||||
// JSON string containing quote characters round-trips back out of
|
||||
// the player-state session variable with its quotes re-escaped - the
|
||||
// text node ends up holding broken, double-escaped JSON rather than
|
||||
// the original. Base64 has no quote characters to collide with that
|
||||
// escaping in the first place, so this sidesteps the whole problem
|
||||
// rather than trying to out-guess exactly how many times to un-escape
|
||||
// it.
|
||||
function __persistPlayerState() {
|
||||
fetch(window.location.pathname, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'PROCESS=setplayerstate&state=' + encodeURIComponent(btoa(JSON.stringify(__buildPlayerState())))
|
||||
});
|
||||
}
|
||||
|
||||
function __renderQueue() {
|
||||
var queue = document.getElementById('globalplayer-queue');
|
||||
var html = '';
|
||||
if (!__hasQueuedSongs()) {
|
||||
// Nothing to show either way (idle, or playing with nothing queued
|
||||
// behind it) - leave it empty and force it closed rather than
|
||||
// displaying a placeholder message, matching __playerToggleQueue's
|
||||
// own refusal to open it in this state.
|
||||
queue.classList.remove('globalplayer-queue-open');
|
||||
} else {
|
||||
for (var i = __currentIndex + 1; i < __playlist.length; i++) {
|
||||
var name = __escapeHtml(__stripExtension(__playlist[i].file));
|
||||
var dur = (__playlist[i].duration != null) ? __formatTime(__playlist[i].duration) : '';
|
||||
html += '<div class="globalplayer-queue-item" draggable="true" data-index="' + i + '">'
|
||||
+ '<span class="globalplayer-queue-left">'
|
||||
+ '<span class="globalplayer-queue-handle" title="Drag to reorder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></span>'
|
||||
+ '<span class="globalplayer-queue-name">' + name + '</span>'
|
||||
+ '</span>'
|
||||
+ '<span class="globalplayer-queue-right">'
|
||||
+ '<span class="globalplayer-queue-duration">' + dur + '</span>'
|
||||
+ '<button type="button" class="globalplayer-queue-remove" title="Remove" onclick="__removeFromPlaylist(' + i + ');"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"/><path d="M9 7V4h6v3"/><path d="M6 7l1 13a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-13"/><path d="M10 11v6M14 11v6"/></svg></button>'
|
||||
+ '</span>'
|
||||
+ '</div>';
|
||||
}
|
||||
}
|
||||
queue.innerHTML = html;
|
||||
__updateQueueButtonLabel();
|
||||
__persistPlayerState();
|
||||
}
|
||||
|
||||
function __playerTogglePause() {
|
||||
if (__isIdle) return;
|
||||
var audio = document.getElementById('globalaudio');
|
||||
if (audio.paused) {
|
||||
audio.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
|
||||
// Distinct from pause: pause just halts playback in place with the
|
||||
// band staying open (see __playerTogglePause/the pause listener's own
|
||||
// comment) - stop clears the queue entirely and closes the band, a
|
||||
// genuine "shut the player off" rather than a resumable pause.
|
||||
function __playerStop() {
|
||||
__playlist = [];
|
||||
__currentIndex = -1;
|
||||
__goIdle();
|
||||
__renderQueue();
|
||||
}
|
||||
|
||||
// Shared entry point every music/jukebox subpage calls on a song click -
|
||||
// appends to the queue, and only starts playback immediately if the
|
||||
// queue was otherwise idle (nothing already playing/queued).
|
||||
// isAutoRandom is only ever true when __playRandomSong() itself is the
|
||||
// caller (a manual click on a song-picker row never passes it) -
|
||||
// picking a specific song yourself is a clear signal you want to take
|
||||
// back manual control, so that turns auto-random mode back off rather
|
||||
// than leaving the button stuck on HALT while a real, manually-chosen
|
||||
// entry sits in the queue underneath it.
|
||||
function addToPlaylist(el, path, file, isAutoRandom) {
|
||||
if (!isAutoRandom) __autoRandomMode = false;
|
||||
var entry = {path: path, file: file, duration: null};
|
||||
__playlist.push(entry);
|
||||
// The queue only ever stores path/file - duration is not otherwise
|
||||
// known until something actually plays it, so a lightweight probe
|
||||
// (metadata only, not the full file) fetches just enough to learn it
|
||||
// for the playlist dropdown's own display - shares the same
|
||||
// one-at-a-time queue the song-picker list's own probes use (see
|
||||
// __queueDurationProbe's own comment for why that matters).
|
||||
__queueDurationProbe(path, file, function(duration) {
|
||||
entry.duration = duration;
|
||||
__renderQueue();
|
||||
});
|
||||
if (__isIdle) {
|
||||
__isIdle = false;
|
||||
__currentIndex = __playlist.length - 1;
|
||||
__playerLoad(__playlist[__currentIndex]);
|
||||
}
|
||||
__renderQueue();
|
||||
if (el) {
|
||||
el.classList.add('song-queued');
|
||||
el.classList.remove('song-queued-flash');
|
||||
// Force a reflow so re-adding the flash class restarts the animation
|
||||
// even if this same row was already flashed once before.
|
||||
void el.offsetWidth;
|
||||
el.classList.add('song-queued-flash');
|
||||
}
|
||||
}
|
||||
|
||||
(function() {
|
||||
var audio = document.getElementById('globalaudio');
|
||||
var band = document.getElementById('globalplayer-band');
|
||||
var playIcon = document.getElementById('globalplayer-playicon');
|
||||
var pauseIcon = document.getElementById('globalplayer-pauseicon');
|
||||
var eqCanvas = document.getElementById('globalplayer-eq');
|
||||
var progressBar = document.getElementById('globalplayer-progress');
|
||||
|
||||
// Delegated on the queue container itself rather than attached per-row,
|
||||
// since __renderQueue() rebuilds every row's markup via innerHTML on
|
||||
// every change - listeners bound directly to a row would be destroyed
|
||||
// and need re-attaching on every single re-render. The container
|
||||
// element itself is never replaced, only its children, so binding here
|
||||
// once at setup survives any number of re-renders.
|
||||
var queueEl = document.getElementById('globalplayer-queue');
|
||||
var __dragSrcIndex = null;
|
||||
queueEl.addEventListener('dragstart', function(e) {
|
||||
var row = e.target.closest('.globalplayer-queue-item');
|
||||
if (!row || row.dataset.index === undefined) return;
|
||||
__dragSrcIndex = parseInt(row.dataset.index, 10);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
row.classList.add('globalplayer-queue-dragging');
|
||||
});
|
||||
queueEl.addEventListener('dragover', function(e) {
|
||||
var row = e.target.closest('.globalplayer-queue-item');
|
||||
if (!row || row.dataset.index === undefined) return;
|
||||
e.preventDefault();
|
||||
queueEl.querySelectorAll('.globalplayer-queue-dragover').forEach(function(el) {
|
||||
el.classList.remove('globalplayer-queue-dragover');
|
||||
});
|
||||
row.classList.add('globalplayer-queue-dragover');
|
||||
});
|
||||
queueEl.addEventListener('dragleave', function(e) {
|
||||
var row = e.target.closest('.globalplayer-queue-item');
|
||||
if (row) row.classList.remove('globalplayer-queue-dragover');
|
||||
});
|
||||
queueEl.addEventListener('drop', function(e) {
|
||||
var row = e.target.closest('.globalplayer-queue-item');
|
||||
e.preventDefault();
|
||||
if (row && row.dataset.index !== undefined && __dragSrcIndex !== null) {
|
||||
__reorderPlaylist(__dragSrcIndex, parseInt(row.dataset.index, 10));
|
||||
}
|
||||
__dragSrcIndex = null;
|
||||
});
|
||||
queueEl.addEventListener('dragend', function() {
|
||||
queueEl.querySelectorAll('.globalplayer-queue-dragging, .globalplayer-queue-dragover').forEach(function(el) {
|
||||
el.classList.remove('globalplayer-queue-dragging', 'globalplayer-queue-dragover');
|
||||
});
|
||||
__dragSrcIndex = null;
|
||||
});
|
||||
|
||||
// Click-or-drag seeking: setPointerCapture means every subsequent
|
||||
// move/up for this same pointer keeps routing here even once the
|
||||
// cursor leaves the bar's own bounds while still held down - the
|
||||
// standard way a scrubber stays grabbed mid-drag rather than losing
|
||||
// the gesture the moment the mouse strays off the (thin, 8px) bar.
|
||||
function seekFromEvent(e) {
|
||||
if (!audio.duration || isNaN(audio.duration)) return;
|
||||
var rect = progressBar.getBoundingClientRect();
|
||||
var ratio = (e.clientX - rect.left) / rect.width;
|
||||
ratio = Math.max(0, Math.min(1, ratio));
|
||||
audio.currentTime = ratio * audio.duration;
|
||||
__setProgress(ratio);
|
||||
__updateTimeDisplay(audio.currentTime, audio.duration);
|
||||
}
|
||||
progressBar.addEventListener('pointerdown', function(e) {
|
||||
progressBar.classList.add('globalplayer-progress-dragging');
|
||||
progressBar.setPointerCapture(e.pointerId);
|
||||
seekFromEvent(e);
|
||||
});
|
||||
progressBar.addEventListener('pointermove', function(e) {
|
||||
if (progressBar.classList.contains('globalplayer-progress-dragging'))
|
||||
seekFromEvent(e);
|
||||
});
|
||||
progressBar.addEventListener('pointerup', function(e) {
|
||||
progressBar.classList.remove('globalplayer-progress-dragging');
|
||||
progressBar.releasePointerCapture(e.pointerId);
|
||||
});
|
||||
var eqCtx = eqCanvas.getContext('2d');
|
||||
audio.volume = document.getElementById('globalplayer-volume').value / 100;
|
||||
audio.addEventListener('ended', function() { __playerAdvance(); });
|
||||
audio.addEventListener('loadedmetadata', function() {
|
||||
__updateTimeDisplay(audio.currentTime, audio.duration);
|
||||
});
|
||||
var __lastPersistTime = 0;
|
||||
audio.addEventListener('timeupdate', function() {
|
||||
if (!progressBar.classList.contains('globalplayer-progress-dragging') && audio.duration && !isNaN(audio.duration)) {
|
||||
__setProgress(audio.currentTime / audio.duration);
|
||||
__updateTimeDisplay(audio.currentTime, audio.duration);
|
||||
}
|
||||
// Position alone is not worth persisting on every tick (several
|
||||
// times a second) - throttled here rather than through
|
||||
// __renderQueue(), which only runs on actual playlist/track changes,
|
||||
// not on ordinary mid-song progress.
|
||||
var now = Date.now();
|
||||
if (now - __lastPersistTime > 5000) {
|
||||
__lastPersistTime = now;
|
||||
__persistPlayerState();
|
||||
}
|
||||
});
|
||||
|
||||
// Stereo FFT-style bar graph in the band itself. Web Audio requires a
|
||||
// real user gesture before an AudioContext will actually run, which a
|
||||
// play event always is - built lazily here, once, on the first real
|
||||
// play rather than up front at page load. A ChannelSplitter feeds one
|
||||
// AnalyserNode per channel so left and right are genuinely independent
|
||||
// readings, not the same mono data mirrored twice.
|
||||
var audioCtx = null, leftAnalyser = null, rightAnalyser = null;
|
||||
|
||||
function setupAudioAnalysis() {
|
||||
if (audioCtx) return;
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
var source = audioCtx.createMediaElementSource(audio);
|
||||
var splitter = audioCtx.createChannelSplitter(2);
|
||||
leftAnalyser = audioCtx.createAnalyser();
|
||||
rightAnalyser = audioCtx.createAnalyser();
|
||||
leftAnalyser.fftSize = 128;
|
||||
rightAnalyser.fftSize = 128;
|
||||
leftAnalyser.smoothingTimeConstant = 0.7;
|
||||
rightAnalyser.smoothingTimeConstant = 0.7;
|
||||
// createMediaElementSource() reroutes the audio element's own output
|
||||
// through the Web Audio graph exclusively - it no longer reaches
|
||||
// speakers on its own past this point, so this connection to
|
||||
// destination is what keeps playback audible, not optional wiring.
|
||||
source.connect(audioCtx.destination);
|
||||
source.connect(splitter);
|
||||
splitter.connect(leftAnalyser, 0);
|
||||
splitter.connect(rightAnalyser, 1);
|
||||
requestAnimationFrame(drawEQ);
|
||||
}
|
||||
|
||||
// Picks bars on a log scale rather than evenly across the raw bin
|
||||
// range - real music concentrates nearly all of its energy in the
|
||||
// first few linearly-indexed bins, so evenly-spaced bins beyond those
|
||||
// read as flat/empty and the whole graph reads as one solid block
|
||||
// instead of a varied spectrum. Spacing bar selection logarithmically
|
||||
// (the same way a real hardware equalizer's bands are laid out) picks
|
||||
// bins that are far enough apart in the actual data to show real
|
||||
// variation bar to bar.
|
||||
function logBin(i, count, bufferLength) {
|
||||
var t = (i + 1) / count;
|
||||
return Math.max(0, Math.min(bufferLength - 1, Math.floor(Math.pow(bufferLength, t)) - 1));
|
||||
}
|
||||
|
||||
function drawEQ() {
|
||||
requestAnimationFrame(drawEQ);
|
||||
var w = eqCanvas.width, h = eqCanvas.height;
|
||||
eqCtx.clearRect(0, 0, w, h);
|
||||
if (!leftAnalyser || !rightAnalyser || audio.paused) return;
|
||||
var bufferLength = leftAnalyser.frequencyBinCount;
|
||||
var leftData = new Uint8Array(bufferLength);
|
||||
var rightData = new Uint8Array(bufferLength);
|
||||
leftAnalyser.getByteFrequencyData(leftData);
|
||||
rightAnalyser.getByteFrequencyData(rightData);
|
||||
|
||||
// Two separate bar groups, left channel and right channel, each
|
||||
// rising from the bottom with a visible gap between bars and between
|
||||
// the two groups - reads as two independent stereo meters rather
|
||||
// than one shape mirrored around a center line.
|
||||
var barsPerChannel = 5;
|
||||
var gap = 2;
|
||||
var channelWidth = (w - gap) / 2;
|
||||
var barWidth = (channelWidth - gap * (barsPerChannel - 1)) / barsPerChannel;
|
||||
|
||||
eqCtx.fillStyle = '#1a8fc4';
|
||||
for (var i = 0; i < barsPerChannel; i++) {
|
||||
var bin = logBin(i, barsPerChannel, bufferLength);
|
||||
var barH = Math.max(2, (leftData[bin] / 255) * h);
|
||||
eqCtx.fillRect(i * (barWidth + gap), h - barH, barWidth, barH);
|
||||
}
|
||||
for (var i = 0; i < barsPerChannel; i++) {
|
||||
var bin = logBin(i, barsPerChannel, bufferLength);
|
||||
var barH = Math.max(2, (rightData[bin] / 255) * h);
|
||||
eqCtx.fillRect(channelWidth + gap * 2 + i * (barWidth + gap), h - barH, barWidth, barH);
|
||||
}
|
||||
}
|
||||
|
||||
audio.addEventListener('play', function() {
|
||||
playIcon.style.display = 'none';
|
||||
pauseIcon.style.display = '';
|
||||
if (!audioCtx) setupAudioAnalysis();
|
||||
else if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
__persistPlayerState();
|
||||
});
|
||||
audio.addEventListener('pause', function() {
|
||||
// Deliberately does NOT close the band - that is tied to __isIdle
|
||||
// (see __playerLoad/__goIdle) rather than the raw play/pause events,
|
||||
// so pausing just shows the paused icon with the band, LCD, and
|
||||
// scrubber all staying visible. It used to close here, which made a
|
||||
// plain pause look like the whole player had reset.
|
||||
playIcon.style.display = '';
|
||||
pauseIcon.style.display = 'none';
|
||||
__persistPlayerState();
|
||||
});
|
||||
|
||||
// A regular fetch() made this late in a page's life is not reliably
|
||||
// delivered by the browser - sendBeacon exists specifically to still
|
||||
// get a small POST out as the page is actually going away, so a
|
||||
// reload/tab-close/navigation right after a timeupdate tick does not
|
||||
// lose up to 5 seconds of position progress.
|
||||
window.addEventListener('pagehide', function() {
|
||||
var body = 'PROCESS=setplayerstate&state=' + encodeURIComponent(btoa(JSON.stringify(__buildPlayerState())));
|
||||
navigator.sendBeacon(window.location.pathname, new Blob([body], {type: 'application/x-www-form-urlencoded'}));
|
||||
});
|
||||
|
||||
__restoreSavedPlayerState();
|
||||
__renderQueue();
|
||||
})();
|
||||
</script>
|
||||
<div class="menubar" id="menubar">
|
||||
<include file="menu.bar">
|
||||
</div>
|
||||
@@ -115,6 +855,7 @@ $[_BODY]
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="spa-loading-indicator" class="spa-loading-indicator">Retrieving content...</div>
|
||||
<div class="footer" style="clear: both;">© 1999-2026 BARANT Systems, All Rights Reserved</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -131,7 +872,7 @@ $[_BODY]
|
||||
<div id="filemgr-list"><div id="filemgr-empty">No transfers yet.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/spa.js?v=20260913a"></script>
|
||||
<script src="/assets/spa.js?v=20261006"></script>
|
||||
<script src="/assets/documents.js?v=20260906a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
<td style="border-bottom: 1pt solid #bbb; width: 20px;"> </td>
|
||||
<tab page="main"><a href="/main">HOME</a></tab>
|
||||
<tab page="services"><a href="/services">SERVICES</a></tab>
|
||||
<tab page="hosting"><a href="/hosting">HOSTING</a></tab>
|
||||
<tab page="store"><a href="/store/createstore">CREATE STORE</a></tab>
|
||||
<tab page="documents"><a href="/documents">DOCUMENT MGMT</a></tab>
|
||||
<tab page="siptrunk"><a href="/siptrunk">SIP TRUNKING</a></tab>
|
||||
<tab page="vst"><a href="/vst">VST</a></tab>
|
||||
<tab page="wordpress"><a href="/wordpress">WORDPRESS</a></tab>
|
||||
<tab page="opensource"><a href="/opensource">OPEN SOURCE</a></tab>
|
||||
@@ -21,16 +18,18 @@
|
||||
<sql>select count(*) as count from cart_item where session_uuid='$[_session_id]'</sql>
|
||||
<ifrow>
|
||||
<if expr="$[.count] > 0">
|
||||
<tab page="cart"><a href="/cart">CART</a></tab>
|
||||
<tab page="cart"><a href="/cart">CART</a></tab>
|
||||
</if>
|
||||
</ifrow>
|
||||
</if>
|
||||
<if value1="$[_login_id]" value2="" type="ne">
|
||||
<tab page="account"><a href="/account">ACCOUNT</a></tab>
|
||||
<tab page="admin"><a href="/admin">ADMIN</a></tab>
|
||||
<tab page="account"><a href="/account">ACCOUNT</a></tab>
|
||||
<if value1="$[_login_type]" value2="admin" type="eq">
|
||||
<tab page="admin"><a href="/admin">ADMIN</a></tab>
|
||||
</if>
|
||||
<else>
|
||||
<tab page="login"><a href="/login">LOGIN</a></tab>
|
||||
<tab page="login"><a href="/login">LOGIN</a></tab>
|
||||
</if>
|
||||
<td style="border-bottom: 1pt solid #bbb; width: 10px;"> </td>
|
||||
<td style="border-bottom: 1pt solid #bbb; width: 10px;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -3,8 +3,8 @@
|
||||
<contents>
|
||||
<script>
|
||||
var index = 0;
|
||||
const paths = [];
|
||||
const songs = [];
|
||||
var paths = [];
|
||||
var songs = [];
|
||||
function onselection(id) {
|
||||
var name = '/get_modular_sample.cgi?id=' + id;
|
||||
var player = document.getElementById('player');
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<page secure="yes" loggedin="no">
|
||||
<title>BAE MUSIC</title>
|
||||
<contents>
|
||||
<script>
|
||||
var index = 0;
|
||||
const paths = [];
|
||||
const songs = [];
|
||||
function onselection(path,selection) {
|
||||
var name = path + '/' + selection;
|
||||
var player = document.getElementById('player');
|
||||
var audio = document.getElementById('audio');
|
||||
var title = document.getElementById('title');
|
||||
player.src = name;
|
||||
title.textContent = selection;
|
||||
audio.load();
|
||||
audio.play();
|
||||
}
|
||||
function selectNext() {
|
||||
var index = Math.floor(Math.random()*songs.length) + 1;
|
||||
onselection(paths[index],songs[index]);
|
||||
}
|
||||
</script>
|
||||
<retain name="download_file">
|
||||
<div style="display: inline-block;">
|
||||
<audio id="audio" controls onended="selectNext();">
|
||||
<source id="player" type="audio/mp3">
|
||||
</audio>
|
||||
</div>
|
||||
<div style="font-size: 20px; display: inline-block;" id="title">Click on a title below</div>
|
||||
<sectionframe title="BARANT Audio Experiments">
|
||||
<div>
|
||||
<whiledir name="file" dir="musiclink/bae">
|
||||
<if expr="left('$[file]',1) != '.' ">
|
||||
<div style="cursor: pointer;" onclick="onselection('musiclink/bae','$[file]');">$[file]</div>
|
||||
<script>
|
||||
paths[index]='musiclink/bae';
|
||||
songs[index++]='$[file]';
|
||||
</script>
|
||||
</if>
|
||||
</whiledir>
|
||||
</div>
|
||||
<script>
|
||||
</script>
|
||||
</sectionframe>
|
||||
|
||||
</contents>
|
||||
</page>
|
||||
@@ -1,7 +1,7 @@
|
||||
<page secure="yes" loggedin="yes">
|
||||
<title>PAY INVOICE</title>
|
||||
<script src="https://sandbox.web.squarecdn.com/v1/square.js></script>
|
||||
<contents>
|
||||
<script src="https://sandbox.web.squarecdn.com/v1/square.js"></script>
|
||||
<sectionframe title="Pay Invoice">
|
||||
<entryform>
|
||||
<sql>select * from invoices where id=$[_invoice_id]</sql>
|
||||
@@ -10,8 +10,8 @@
|
||||
<formdata prompt="Date:" data="$[created]">
|
||||
<formdata prompt="Amount Due:" data="$[amount]">
|
||||
<script>
|
||||
const appId = 'sandbox-sq0idb-GGY04gvAQJAmneJHgn_p8Q';
|
||||
const locationId = 'LEMFWBZPEE9ST';
|
||||
var appId = 'sandbox-sq0idb-GGY04gvAQJAmneJHgn_p8Q';
|
||||
var locationId = 'LEMFWBZPEE9ST';
|
||||
|
||||
async function initializeCard(payments) {
|
||||
const card = await payments.card();
|
||||
@@ -74,7 +74,7 @@
|
||||
statusContainer.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
async function initPayInvoice() {
|
||||
if (!window.Square) {
|
||||
throw new Error('Square.js failed to load properly');
|
||||
}
|
||||
@@ -122,7 +122,12 @@
|
||||
cardButton.addEventListener('click', async function (event) {
|
||||
await handlePaymentMethodSubmission(event, card);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading')
|
||||
document.addEventListener('DOMContentLoaded', initPayInvoice);
|
||||
else
|
||||
initPayInvoice();
|
||||
</script>
|
||||
|
||||
<div id="card-container"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script src="https://web.squarecdn.com/v1/square.js"></script>
|
||||
<script>
|
||||
<script>
|
||||
function processCardForm(token) {
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/pi.cgi', true);
|
||||
@@ -8,40 +8,60 @@
|
||||
console.log(">" + this.responseText + "<");
|
||||
if(this.responseText == "OK\n") {
|
||||
document.forms[0].PAGE.value = 'thankyou';
|
||||
document.forms[0].submit();
|
||||
} else {
|
||||
message.innerText = this.responseText;
|
||||
document.forms[0].requestSubmit();
|
||||
} else {
|
||||
var payBtn = document.getElementById('pay');
|
||||
payBtn.disabled = false;
|
||||
var processbanner = document.getElementById('process');
|
||||
processbanner.style.visibility = 'hidden';
|
||||
message.innerText = this.responseText;
|
||||
}
|
||||
};
|
||||
let data = 'id=$[_session_id]&token=' + token + '&tranonce=$[tranonce]';
|
||||
xhr.send(data);
|
||||
xhr.send(data);
|
||||
}
|
||||
var card;
|
||||
async function buildForm() {
|
||||
// let payments = Square.payments('sandbox-sq0idb-GGY04gvAQJAmneJHgn_p8Q','LEMFWBZPEE9ST');
|
||||
let payments = Square.payments('sq0idp-V4R4vEferpF9kr7d5x5abQ','LV0MRENX5ED3Z');
|
||||
const card = await payments.card({
|
||||
style: {
|
||||
'.input-container.is-focus': { borderColor: '#006AFF' },
|
||||
'.message-text.is-error': { color: '#BF0020' }
|
||||
input: {
|
||||
backgroundColor: '#1c1c24',
|
||||
color: '#e7e4dc',
|
||||
fontSize: '14px'
|
||||
},
|
||||
'input::placeholder': { color: '#8d8c92' },
|
||||
'.input-container': { borderColor: 'rgba(197, 164, 90, 0.28)' },
|
||||
'.input-container.is-focus': { borderColor: '#c5a45a' },
|
||||
'.input-container.is-error': { borderColor: '#a13a3a' },
|
||||
'.message-text': { color: '#8d8c92' },
|
||||
'.message-text.is-error': { color: '#a13a3a' }
|
||||
}
|
||||
});
|
||||
await card.attach('#card');
|
||||
let button = document.getElementById('pay');
|
||||
button.addEventListener('click', async () => {
|
||||
message.innerText = '';
|
||||
let result = await card.tokenize();
|
||||
if (result.status === 'OK') {
|
||||
button.disabled = true;
|
||||
let processbanner = document.getElementById('process');
|
||||
processbanner.style.visibility = 'visible';
|
||||
processCardForm(result.token);
|
||||
} else {
|
||||
let errorMessage = 'Please check your card details and try again.';
|
||||
if (result.errors && result.errors.length && result.errors[0].message)
|
||||
errorMessage = result.errors[0].message;
|
||||
message.innerText = errorMessage;
|
||||
}
|
||||
});
|
||||
}
|
||||
buildForm();
|
||||
buildForm();
|
||||
</script>
|
||||
<div>Please enter your payment information:</div>
|
||||
<div id="card" style="background-color: #ddd;width: 300px; padding: 5px;"></div>
|
||||
<div style="padding: 5px;"><button id= "pay" type="button">Pay</button></div>
|
||||
<div id="process" style="background-color: #0f0; border: 2px solid #000; padding:
|
||||
5px; display: block-inline; margin: 10px; visibility: hidden;"> Processing Payment - Please wait... </div>
|
||||
<div id="message" style="color: #f00; margin-top: 20px;"></div>
|
||||
<div class="checkout-pay-wrap">
|
||||
<div class="checkout-pay-label">Card Details</div>
|
||||
<div id="card" class="checkout-card-input"></div>
|
||||
<button id="pay" type="button" class="button checkout-pay-btn">Pay $$[amount]</button>
|
||||
<div id="process" class="checkout-processing">Processing Payment — Please wait…</div>
|
||||
<div id="message" class="checkout-error"></div>
|
||||
</div>
|
||||
@@ -1,3 +1,5 @@
|
||||
<sql>select extensions.id as 'X', extensions.exten as 'Phone Number' from phones.extensions where extensions.user_id=$[@_login_id]</sql>
|
||||
<ifrow>
|
||||
<sectionframe title="Phones">
|
||||
|
||||
<button>
|
||||
@@ -7,7 +9,8 @@
|
||||
<showtable page="request" process="selectrequest" name="request_id" value="X">
|
||||
select extensions.id as 'X',
|
||||
extensions.exten as 'Phone Number'
|
||||
from phones.extensions
|
||||
from phones.extensions where extensions.user_id=$[@_login_id]
|
||||
</showtable>
|
||||
|
||||
</sectionframe>
|
||||
</sectionframe>
|
||||
</ifrow>
|
||||
@@ -1,3 +1,3 @@
|
||||
<process>
|
||||
<setenv name="_invoice_id">$[invoice_id]</setenv>
|
||||
<set name="@_invoice_id" value="$[invoice_id]">
|
||||
</process>
|
||||
@@ -0,0 +1,27 @@
|
||||
<set name="_send_invoice_id" value="$[invoice_id]">
|
||||
<if value1="$[_send_invoice_id]" value2="" type="eq">
|
||||
<set name="_send_invoice_id" value="$[@_invoice_id]">
|
||||
</if>
|
||||
<sql>select u.email as email, u.name as name from invoices i join users u on i.user_id=u.id where i.id=$[_send_invoice_id]</sql>
|
||||
<ifrow>
|
||||
<set name="_invoice_email_to" value="$[.email]">
|
||||
<if value1="$[_invoice_email_override]" value2="" type="ne">
|
||||
<set name="_invoice_email_to" value="$[_invoice_email_override]">
|
||||
</if>
|
||||
<set name="_invoice_id" value="$[_send_invoice_id]">
|
||||
<eval name="_invoice_html">
|
||||
<include file="invoice.method">
|
||||
</eval>
|
||||
<eval name="_invoice_email_body">
|
||||
<html>
|
||||
<head></head>
|
||||
<body style="margin: 0; padding: 30px 10px; background: #0a0a0d; font-family: Arial, Helvetica, sans-serif;">
|
||||
$[_invoice_html]
|
||||
<p style="text-align: center; color: #8d8c92; font-size: 11px; margin-top: 20px;">BARANT Technology Group</p>
|
||||
</body>
|
||||
</html>
|
||||
</eval>
|
||||
<eval name="_invoice_smtpsend_discard">
|
||||
<smtpsend config="./email.cfg" user="noreply@barant.com" password="NgCe0RolitzgBKVHz2HKfnTd" to="$[_invoice_email_to]" subject="Your BARANT Technology Group Invoice #$[_send_invoice_id]" html="$[_invoice_email_body]">
|
||||
</eval>
|
||||
</ifrow>
|
||||
Executable → Regular
-5
@@ -1,5 +1,3 @@
|
||||
<page>
|
||||
<title>Document Management</title>
|
||||
<style>
|
||||
.price {display: table-cell;
|
||||
background-color: #fff;
|
||||
@@ -32,7 +30,6 @@
|
||||
#documents-list td a:hover, #documents-list td .documents-action:hover { text-decoration: underline; }
|
||||
#documents-empty { color: var(--bond-text-dim); padding: 10px 0; }
|
||||
</style>
|
||||
<contents>
|
||||
<sectionframe title="Document Management">
|
||||
<h1>Online Document Management</h1>
|
||||
<p>BARANT Technology Group now offers document management storage and
|
||||
@@ -52,5 +49,3 @@ documents for any activity with that account.</p>
|
||||
</sectionframe>
|
||||
<script src="/assets/documents-page.js?v=20260906a"></script>
|
||||
</if>
|
||||
</contents>
|
||||
</page>
|
||||
Executable → Regular
+2
-7
@@ -1,6 +1,3 @@
|
||||
<page>
|
||||
<title>HOSTING</title>
|
||||
<contents>
|
||||
<retain name="sku">
|
||||
<sectionframe title="Virtual Machine Hosting">
|
||||
|
||||
@@ -17,7 +14,7 @@ redundancy and fire protection.</p>
|
||||
<hr>
|
||||
<h2>Ubuntu 24.04 Host</h2>
|
||||
<p>Full Ubuntu 24.04 host with memory, CPU and storage size pricing
|
||||
tiers. Both monthly and annual rates available.</p>
|
||||
tiers. Both monthly and annual rates available.</p>
|
||||
<p>All our servers use SSD for high performance.</p>
|
||||
<table border="1">
|
||||
<tr>
|
||||
@@ -56,10 +53,8 @@ bandwidth and 50GB storage for voice-mail and recordings. Full root
|
||||
access. Full service support contacts available to maintain your
|
||||
phones and other VoIP devices. </p>
|
||||
<p>$49.95/month</p>
|
||||
<p>See our SIP trunking pricing and DID line options as well for a complete
|
||||
<p>See our <linkbutton page="services" subpage="siptrunking">SIP trunking pricing</linkbutton> and DID line options as well for a complete
|
||||
telephony solution.</p>
|
||||
<hr>
|
||||
</sectionframe>
|
||||
<set name="pagemenu" value="davidarant">
|
||||
</contents>
|
||||
</page>
|
||||
@@ -0,0 +1,45 @@
|
||||
<sectionframe title="Welcome to BARANT Systems">
|
||||
|
||||
<div style="width: 48%;">
|
||||
<p>We are BARANT Technology Group. Contact us for information on how we can
|
||||
help you with your technical objectives.</p>
|
||||
|
||||
<h1>Our Services</h1>
|
||||
<ul>
|
||||
<li>Software architecture and development on many platforms.
|
||||
<ul>
|
||||
<li>40+ years on IBM System/3x, AS/400, i Series and IBM i platform.</li>
|
||||
<li>20+ years .NET Web and Winform application development.</li>
|
||||
<li>10+ Android mobile development.</li>
|
||||
</ul>
|
||||
</li><br>
|
||||
<li>Database architecture and development on many platforms.
|
||||
<ul>
|
||||
<li>DB2/400</li>
|
||||
<li>Oracle</li>
|
||||
<li>MySQL</li>
|
||||
<li>PostgreSQL</li>
|
||||
<li>MSSQL</li>
|
||||
<li>Amazon Aurora</li>
|
||||
<li>Amazon DynamoDB</li>
|
||||
<li>MongoDB</li>
|
||||
</ul>
|
||||
</li><br>
|
||||
<li>Telephony and Call Center Solutions
|
||||
<ul>
|
||||
<li>25+ years in call center development and application integration.</li>
|
||||
<li>15+ years on open source FreePBX development and maintenance.</li>
|
||||
<li>10+ as <linkbutton page="services" subpage="siptrunking">SIP trunk provider</linkbutton> offering very competitive rates.</li>
|
||||
</ul>
|
||||
</li><br>
|
||||
<li>Audio Engineering products and projects
|
||||
<ul>
|
||||
<li>Product offering for VST synthesizers for modular analog simulation.</li>
|
||||
<li>Module development for super clean digitally pure wave harmonics.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
</div>
|
||||
</sectionframe>
|
||||
Executable → Regular
+3
-8
@@ -1,10 +1,8 @@
|
||||
<page>
|
||||
<title>SIP Trunking</title>
|
||||
<style>
|
||||
.price {display: table-cell;
|
||||
background-color: #fff;
|
||||
background-color: #fff;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #000;
|
||||
@@ -12,7 +10,6 @@
|
||||
}
|
||||
.number {font-size: 30px;}
|
||||
</style>
|
||||
<contents>
|
||||
<sectionframe title="SIP Trunking">
|
||||
<h1>For PBX Systems</h1>
|
||||
<p>Some of the best prices around for adding trunks to your PBX. We
|
||||
@@ -40,11 +37,9 @@ use these devices on our networks.</p>
|
||||
If your organization does not have or need a PBX but wish to have a
|
||||
phone for 1 or 2 lines then you can just ask for our individual phone
|
||||
service for any VoIP phone or VoIP converter for any old standard
|
||||
phone line. Please contact us for more information.
|
||||
phone line. Please contact us for more information.
|
||||
<h1>Don't have a PBX but want one?</h1>
|
||||
We can also host a PBX for you as well. Complete functionality
|
||||
utilizing FreePBX can be provided on the internet so you do not need
|
||||
to worry about the hardware. We manage and maintain everything for you.
|
||||
</sectionframe>
|
||||
</contents>
|
||||
</page>
|
||||
@@ -0,0 +1,22 @@
|
||||
<sectionframe title="SERVICES">
|
||||
<if value1="$[SUBPAGE]" value2="" type="eq">
|
||||
<linkbutton page="services" class="submenuitem-active">OVERVIEW</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="services">OVERVIEW</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="hosting" type="eq">
|
||||
<linkbutton page="services" subpage="hosting" class="submenuitem-active">HOSTING</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="services" subpage="hosting">HOSTING</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="siptrunking" type="eq">
|
||||
<linkbutton page="services" subpage="siptrunking" class="submenuitem-active">SIP TRUNKING</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="services" subpage="siptrunking">SIP TRUNKING</linkbutton><br>
|
||||
</if>
|
||||
<if value1="$[SUBPAGE]" value2="documents" type="eq">
|
||||
<linkbutton page="services" subpage="documents" class="submenuitem-active">DOCUMENTS</linkbutton><br>
|
||||
<else>
|
||||
<linkbutton page="services" subpage="documents">DOCUMENTS</linkbutton><br>
|
||||
</if>
|
||||
</sectionframe>
|
||||
@@ -2,50 +2,16 @@
|
||||
<title>BARANT Technology Group</title>
|
||||
<contents>
|
||||
<setenv name="_menubar"></setenv>
|
||||
<sectionframe title="Welcome to BARANT Systems">
|
||||
|
||||
<div style="width: 48%;">
|
||||
<p>We are BARANT Technology Group. Contact us for information on how we can
|
||||
help you with your technical objectives.</p>
|
||||
|
||||
<h1>Our Services</h1>
|
||||
<ul>
|
||||
<li>Software architecture and development on many platforms.
|
||||
<ul>
|
||||
<li>40+ years on IBM System/3x, AS/400, i Series and IBM i platform.</li>
|
||||
<li>20+ years .NET Web and Winform application development.</li>
|
||||
<li>10+ Android mobile development.</li>
|
||||
</ul>
|
||||
</li><br>
|
||||
<li>Database architecture and development on many platforms.
|
||||
<ul>
|
||||
<li>DB2/400</li>
|
||||
<li>Oracle</li>
|
||||
<li>MySQL</li>
|
||||
<li>PostgreSQL</li>
|
||||
<li>MSSQL</li>
|
||||
<li>Amazon Aurora</li>
|
||||
<li>Amazon DynamoDB</li>
|
||||
<li>MongoDB</li>
|
||||
</ul>
|
||||
</li><br>
|
||||
<li>Telephony and Call Center Solutions
|
||||
<ul>
|
||||
<li>25+ years in call center development and application integration.</li>
|
||||
<li>15+ years on open source FreePBX development and maintenance.</li>
|
||||
<li>10+ as SIP trunk provider offering very competitive rates.</li>
|
||||
</ul>
|
||||
</li><br>
|
||||
<li>Audio Engineering products and projects
|
||||
<ul>
|
||||
<li>Product offering for VST synthesizers for modular analog simulation.</li>
|
||||
<li>Module development for super clean digitally pure wave harmonics.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
</div>
|
||||
</sectionframe>
|
||||
<div id="services-subpage">
|
||||
<if value1="$[SUBPAGE]" value2="" type="eq">
|
||||
<include file="services-overview.subpage">
|
||||
<else>
|
||||
<try>
|
||||
<include file="services-$[SUBPAGE].subpage">
|
||||
<catch>
|
||||
<p>That services category doesn't exist.</p>
|
||||
</try>
|
||||
</if>
|
||||
</div>
|
||||
</contents>
|
||||
</page>
|
||||
@@ -0,0 +1 @@
|
||||
<setenv name="_player_state">$[state]</setenv>
|
||||
@@ -0,0 +1 @@
|
||||
<setenv name="_player_volume">$[volume]</setenv>
|
||||
@@ -1,10 +1,11 @@
|
||||
<process>
|
||||
|
||||
<sql>delete from invoices_detail where invoice_id=$[_invoice_id]</sql>
|
||||
<sql>delete from invoices_detail where invoice_id=$[@_invoice_id]</sql>
|
||||
<for start="0" end="$[linecount]" step="1" name="ix">
|
||||
<sql>insert into invoices_detail (invoice_id,sku,sku_suffix,quantity,description,text,price,total,tax_amount,type)
|
||||
values($[_invoice_id],'$[sku$[ix]]','',$[quantity$[ix]],'$[description$[ix]]','$[textarea$[ix]]',$[price$[ix]],$[total$[ix]],$[tax_amount$[ix]],'$[type$[ix]]')</sql>$[sqlerror]
|
||||
values($[@_invoice_id],'$[sku$[ix]]','',$[quantity$[ix]],'$[description$[ix]]','$[textarea$[ix]]',$[price$[ix]],$[total$[ix]],$[tax_amount$[ix]],'$[type$[ix]]')</sql>$[sqlerror]
|
||||
</for>
|
||||
<sql>update invoices set amount=$[invoice_total] where id=$[_invoice_id]</sql>
|
||||
<setpage name="admin_invoice">
|
||||
<sql>update invoices set amount=$[invoice_total], user_id=$[user_id] where id=$[@_invoice_id]</sql>
|
||||
<setpage name="admin">
|
||||
<set name="SUBPAGE" value="invoice">
|
||||
</process>
|
||||
Reference in new issue
Block a user