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:
Brad ArantandClaude Sonnet 5 committed 2026-10-06 17:42:01 +00:00
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

+64 -6
View File
@@ -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');
});
}
+530 -15
View File
@@ -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;
}
+28
View File
@@ -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
+421
View File
@@ -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">&#9670;&nbsp;B · A · R · A · N · T&nbsp;&#9670;</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>
+83
View File
@@ -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>
+17
View File
@@ -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>
+11 -103
View File
@@ -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>
+13 -1
View File
@@ -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&hellip;">
<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>
+118 -3
View File
@@ -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&hellip;</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>
+25 -4
View File
@@ -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">
+27 -3
View File
@@ -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>
-11
View File
@@ -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 -1
View File
@@ -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>
+2 -2
View File
@@ -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)!='_'">
+29
View File
@@ -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>
+21
View File
@@ -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>
+113 -59
View File
@@ -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>
+46 -22
View File
@@ -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>
+38 -39
View File
@@ -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>
-67
View File
@@ -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 -1
View File
@@ -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]">
+26
View File
@@ -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>
+43
View File
@@ -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>
+7
View File
@@ -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>
+24 -2
View File
@@ -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>
+11 -23
View File
@@ -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>
+747 -6
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// 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;">&copy; 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>
+7 -8
View File
@@ -9,10 +9,7 @@
<td style="border-bottom: 1pt solid #bbb; width: 20px;">&nbsp;</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;">&nbsp;</td>
<td style="border-bottom: 1pt solid #bbb; width: 10px;">&nbsp;</td>
</tr>
</table>
+2 -2
View File
@@ -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');
-47
View File
@@ -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>
+10 -5
View File
@@ -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>
+35 -15
View File
@@ -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 &mdash; Please wait&hellip;</div>
<div id="message" class="checkout-error"></div>
</div>
+5 -2
View File
@@ -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 -1
View File
@@ -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>
@@ -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>
@@ -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>
@@ -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>
+22
View File
@@ -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>
+11 -45
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
<setenv name="_player_state">$[state]</setenv>
+1
View File
@@ -0,0 +1 @@
<setenv name="_player_volume">$[volume]</setenv>
+5 -4
View File
@@ -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>