Compare commits

...

7 Commits

Author SHA1 Message Date
cf69c72f3c commit 06/10/2025 2025-10-06 08:30:09 +07:00
20dbc12b02 commit 02/10/2025 2025-10-02 16:03:28 +07:00
3768f4263b commit 02/10/2025 2025-10-02 13:41:26 +07:00
1e7adeba25 commit 02/10/2025 2025-10-02 13:17:52 +07:00
rdkartono
83a6ee9fd0 Merge branch 'master' of https://gitea.rdkartono.my.id/rdkartono/AAS_NewGeneration 2025-10-01 15:52:31 +07:00
rdkartono
e0f3ac2094 Commit 30/09/2025 2025-10-01 15:50:33 +07:00
c55db5e4f7 commit 01/10/2025 2025-10-01 13:57:20 +07:00
38 changed files with 1867 additions and 857 deletions

View File

@@ -60,3 +60,20 @@
align-items: center;
}
.pad-relay {
padding-left: 1.5rem;
padding-top: 0.5rem;
}
.pad-time {
margin: 0.7rem auto;
}
.pad-day {
margin-top: 0.5rem;
}
.class100 {
width: 100% !important;
}

View File

@@ -1,5 +1,6 @@
body {
background-color: #f8f9fd;
/*background-color: #edf1fb;*/
overflow-x: hidden;
width: 100%;
}
@@ -13,8 +14,9 @@ body {
}
.search {
display: flex;
align-items: center;
/*display: flex;*/
/*align-items: center;*/
margin-top: -0.2rem;
}
.text-header {
@@ -188,3 +190,68 @@ nav-item:focus {
border: none;
}
.pad-card {
padding-top: 1rem;
}
.pad-search {
padding-top: 0.5rem;
}
.pad-row-search {
margin-bottom: 0.7rem;
}
.bg-heading1 {
background-color: #c6d8ee;
color: #2d3578;
}
.bg-heading2 {
background-color: #dce5f4;
color: #2d3578;
}
.bg-heading3 {
background-color: #e9ecf8;
color: #2d3578;
}
.accordion-item {
/*background: rgba(255, 255, 255, 0.55);*/
/*backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);*/
/*border-radius: 16px;*/
/*box-shadow: 8px 8px 16px rgba(0, 0, 0, 0.12), -8px -8px 16px rgba(255, 255, 255, 0.6);*/
/*transition: all 0.3s ease;*/
}
.accordion-item.active {
/*background: rgba(45, 53, 120, 0.15);*/
/*box-shadow: inset 4px 4px 10px rgba(45, 53, 120, 0.25), inset -4px -4px 10px rgba(255, 255, 255, 0.8);*/
}
.bg-accordion {
border: white 2px;
/*border-radius: 10px;*/
background: #f8f9fd;
}
.card-channel {
border-radius: 12px;
background: #ffffff;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: #ffffff solid 2px !important;
/*padding-left: 2px;*/
/*padding-right: 5px;*/
padding-top: 12px;
padding-bottom: 12px;
}
.pad-accordion {
border-radius: 40px;
border: white solid 3px;
}

View File

@@ -1,23 +1,10 @@
/**
* @typedef {Object} BroadcastZone
* @property {number} index
* @property {string} description
* @property {String} SoundChannel
* @property {String} Box
* @property {String} Relay
*/
/**
* List of broadcast zones available
* @type {BroadcastZone[]}
*/
let BroadcastZoneList = [];
/**
* Currently selected broadcast zone row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedBroadcastZoneRow = null;
window.selectedBroadcastZoneRow = null;
/**
* Fill broadcast zone table body with values
@@ -31,23 +18,23 @@ function fill_broadcastzonetablebody(vv) {
<td>${item.index}</td>
<td>${item.description}</td>
<td>${item.soundChannel}</td>
<td>${item.box}</td>
<td>${item.relay}</td>
<td>${item.id}</td>
<td>${item.bp}</td>
</tr>`;
$('#broadcastzonetablebody').append(row);
let $addedrow = $('#broadcastzonetablebody tr:last');
$addedrow.click(function () {
if (selectedBroadcastZoneRow) {
selectedBroadcastZoneRow.find('td').css('background-color', '');
if (selectedBroadcastZoneRow.is($(this))) {
selectedBroadcastZoneRow = null;
if (window.selectedBroadcastZoneRow) {
window.selectedBroadcastZoneRow.find('td').css('background-color', '');
if (window.selectedBroadcastZoneRow.is($(this))) {
window.selectedBroadcastZoneRow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$(this).find('td').css('background-color', '#ffeeba');
selectedBroadcastZoneRow = $(this);
window.selectedBroadcastZoneRow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -55,26 +42,11 @@ function fill_broadcastzonetablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload broadcast zones from server
* @param {String} APIURL API URL endpoint (default "BroadcastZones/")
*/
function reloadBroadcastZones(APIURL = "BroadcastZones/") {
BroadcastZoneList = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadBroadcastZones : ", okdata)
BroadcastZoneList = okdata;
fill_broadcastzonetablebody(BroadcastZoneList);
} else console.log("reloadBroadcastZones: okdata is not array");
}, (errdata) => {
alert("Error loading broadcast zones : " + errdata.message);
});
}
$(document).ready(function () {
console.log("broadcastzones.js loaded successfully");
selectedBroadcastZoneRow = null;
window.selectedBroadcastZoneRow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnEdit = $('#btnEdit');
@@ -98,12 +70,12 @@ $(document).ready(function () {
$findzone.on('input', function () {
let searchTerm = $findzone.val().trim().toLowerCase();
if (searchTerm.length > 0) {
selectedBroadcastZoneRow = null;
let filtered = broadcastzonedata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.box.toLowerCase().includes(searchTerm) || item.soundChannel.toLowerCase().includes(searchTerm) || item.relay.toLowerCase().includes(searchTerm));
window.selectedBroadcastZoneRow = null;
let filtered = window.BroadcastZoneList.filter(item => item.description.toLowerCase().includes(searchTerm) || item.id.toLowerCase().includes(searchTerm) || item.soundChannel.toLowerCase().includes(searchTerm) || item.bp.toLowerCase().includes(searchTerm));
fill_broadcastzonetablebody(filtered);
} else {
selectedBroadcastZoneRow = null;
fill_broadcastzonetablebody(broadcastzonedata);
window.selectedBroadcastZoneRow = null;
fill_broadcastzonetablebody(window.BroadcastZoneList);
}
});
@@ -140,11 +112,14 @@ $(document).ready(function () {
}
}
reloadBroadcastZones(APIURL_BroadcastZone);
reloadBroadcastZones(APIURL_BroadcastZone, () => {
fill_broadcastzonetablebody(window.BroadcastZoneList);
});
$btnClear.click(() => {
DoClear(APIURL_BroadcastZone, "BroadcastZones", (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
fill_broadcastzonetablebody(window.BroadcastZoneList);
alert("Success clear broadcast zones: " + okdata.message);
}, (errdata) => {
alert("Error clear broadcast zones: " + errdata.message);
@@ -190,8 +165,10 @@ $(document).ready(function () {
Relay: relay
};
fetchAPI(APIURL_BroadcastZone + "Add", "POST", {}, bz, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success add new broadcast zone: " + okdata.message);
reloadBroadcastZones(APIURL_BroadcastZone, () => {
fill_broadcastzonetablebody(window.BroadcastZoneList);
alert("Success add new broadcast zone: " + okdata.message);
});
}, (errdata) => {
alert("Error add new broadcast zone: " + errdata.message);
});
@@ -204,8 +181,8 @@ $(document).ready(function () {
});
$btnRemove.click(() => {
if (selectedBroadcastZoneRow) {
let cells = selectedBroadcastZoneRow.find('td');
if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */
let bz = {
index: cells.eq(0).text(),
@@ -216,8 +193,10 @@ $(document).ready(function () {
};
if (confirm(`Are you sure to delete broadcast zone [${bz.index}] Description=${bz.description}?`)) {
fetchAPI(APIURL_BroadcastZone + "DeleteByIndex/" + bz.index, "DELETE", {}, null, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success delete broadcast zone: " + okdata.message);
reloadBroadcastZones(APIURL_BroadcastZone, () => {
fill_broadcastzonetablebody(window.BroadcastZoneList);
alert("Success delete broadcast zone: " + okdata.message);
});
}, (errdata) => {
alert("Error delete broadcast zone: " + errdata.message);
});
@@ -226,8 +205,8 @@ $(document).ready(function () {
});
$btnEdit.click(() => {
if (selectedBroadcastZoneRow) {
let cells = selectedBroadcastZoneRow.find('td');
if (window.selectedBroadcastZoneRow) {
let cells = window.selectedBroadcastZoneRow.find('td');
/** @type {BroadcastZone} */
let bz = {
index: cells.eq(0).text(),
@@ -236,15 +215,15 @@ $(document).ready(function () {
Box: cells.eq(3).text(),
Relay: cells.eq(4).text()
};
if (confirm(`Are you sure to edit broadcast zone [${bz.index}] Description=${bz.description} SoundChannel=${bz.SoundChannel} Box=${bz.Box} Relay=${bz.Relay}?`)) {
if (confirm(`Are you sure to edit broadcast zone [${bz.index}] Description=${bz.description} SoundChannel=${bz.SoundChannel} Box=${bz.id} Relay=${bz.bp}?`)) {
$broadcastzonemodal.modal('show');
clearBroadcastZoneModal();
$broadcastzoneindex.val(bz.index);
$broadcastzonedescription.val(bz.description);
$broadcastzonesoundchannel.val(bz.SoundChannel);
$broadcastzonebox.val(bz.Box);
if (bz.Relay) {
bz.Relay.split(';').forEach(relayId => {
$broadcastzonebox.val(bz.id);
if (bz.bp) {
bz.bp.split(';').forEach(relayId => {
let id = parseInt(relayId, 10);
cbRelay(id).prop('checked', true);
});
@@ -283,8 +262,10 @@ $(document).ready(function () {
Relay: relay
};
fetchAPI(APIURL_BroadcastZone + "UpdateByIndex/" + bz.index, "PATCH", {}, bzUpdate, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success edit broadcast zone: " + okdata.message);
reloadBroadcastZones(APIURL_BroadcastZone, () => {
fill_broadcastzonetablebody(window.BroadcastZoneList);
alert("Success edit broadcast zone: " + okdata.message);
});
}, (errdata) => {
alert("Error edit broadcast zone: " + errdata.message);
});
@@ -303,8 +284,10 @@ $(document).ready(function () {
$btnImport.click(() => {
DoImport(APIURL_BroadcastZone, (okdata) => {
reloadBroadcastZones(APIURL_BroadcastZone);
alert("Success import broadcast zones: " + okdata.message);
reloadBroadcastZones(APIURL_BroadcastZone, () => {
fill_broadcastzonetablebody(window.BroadcastZoneList);
alert("Success import broadcast zones: " + okdata.message);
});
}, (errdata) => {
alert("Error importing broadcast zones from XLSX: " + errdata.message);
});

View File

@@ -1,20 +1,9 @@
/**
* @typedef {Object} LanguageBank
* @property {number} index
* @property {string} tag
* @property {string} language
*
*/
/** List of Languagebank data loaded from server
* @type {LanguageBank[]}
*/
let languagebankdata = [];
/**
* Currently selected languagebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedlanguagerow = null;
window.selectedlanguagerow = null;
/**
* Fill languagebank table body with values
@@ -32,17 +21,17 @@ function fill_languagebanktablebody(vv) {
$('#languagebanktablebody').append(row);
let $addedrow = $('#languagebanktablebody tr:last');
$addedrow.click(function () {
if (selectedlanguagerow) {
selectedlanguagerow.find('td').css('background-color', '');
if (selectedlanguagerow.is($(this))) {
selectedlanguagerow = null;
if (window.selectedlanguagerow) {
window.selectedlanguagerow.find('td').css('background-color', '');
if (window.selectedlanguagerow.is($(this))) {
window.selectedlanguagerow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$addedrow.find('td').css('background-color', '#ffeeba');
selectedlanguagerow = $addedrow;
window.selectedlanguagerow = $addedrow;
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -50,28 +39,13 @@ function fill_languagebanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload language bank from server
* @param {string} APIURL API URL endpoint, default "LanguageLink/"
*/
function reloadLanguageBank(APIURL = "LanguageLink/") {
languagebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
languagebankdata = okdata;
selectedlanguagerow = null;
fill_languagebanktablebody(languagebankdata);
}
}, (errdata) => {
alert("Error loading languagebank : " + errdata.message);
});
}
$(document).ready(function () {
console.log('languagebank.js loaded');
$('#languagebanktablebody').empty();
selectedlanguagerow = null;
window.selectedlanguagerow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
@@ -106,20 +80,24 @@ $(document).ready(function () {
$findlanguage.on('input', function () {
let searchTerm = $findlanguage.val().toLowerCase();
if (searchTerm.length > 0) {
selectedlanguagerow = null;
let filtered = languagebankdata.filter(item => item.tag.toLowerCase().includes(searchTerm) || item.language.toLowerCase().includes(searchTerm));
window.selectedlanguagerow = null;
let filtered = window.languagebankdata.filter(item => item.tag.toLowerCase().includes(searchTerm) || item.language.toLowerCase().includes(searchTerm));
fill_languagebanktablebody(filtered);
} else {
selectedlanguagerow = null;
fill_languagebanktablebody(languagebankdata);
window.selectedlanguagerow = null;
fill_languagebanktablebody(window.languagebankdata);
}
});
reloadLanguageBank(APIURL);
reloadLanguageBank(APIURL, () => {
fill_languagebanktablebody(window.languagebankdata);
});
$btnClear.click(() => {
DoClear(APIURL, "LanguageLink", (okdata) => {
reloadLanguageBank(APIURL);
alert("Success clear languageLink : " + okdata.message);
reloadLanguageBank(APIURL, () => {
fill_languagebanktablebody(window.languagebankdata);
alert("Success clear languageLink : " + okdata.message);
});
}, (errdata) => {
alert("Error clear languageLink : " + errdata.message);
});
@@ -156,8 +134,10 @@ $(document).ready(function () {
language: langString
}
fetchAPI(APIURL + "Add", "POST", {}, ll, (okdata) => {
alert("Success add language : " + okdata.message);
reloadLanguageBank(APIURL);
reloadLanguageBank(APIURL, () => {
fill_languagebanktablebody(window.languagebankdata);
alert("Success add language : " + okdata.message);
});
}, (errdata) => {
alert("Error add language : " + errdata.message);
});
@@ -171,8 +151,8 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedlanguagerow) {
let cells = selectedlanguagerow.find('td');
if (window.selectedlanguagerow) {
let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */
let ll = {
index: cells.eq(0).text(),
@@ -181,8 +161,10 @@ $(document).ready(function () {
}
if (confirm(`Are you sure to delete language [${ll.index}] Tag=${ll.tag} Language=${ll.language}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + ll.index, "DELETE", {}, null, (okdata) => {
reloadLanguageBank(APIURL);
alert("Success delete language : " + okdata.message);
reloadLanguageBank(APIURL, () => {
fill_languagebanktablebody(window.languagebankdata);
alert("Success delete language : " + okdata.message);
});
}, (errdata) => {
alert("Error delete language : " + errdata.message);
});
@@ -190,8 +172,8 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedlanguagerow) {
let cells = selectedlanguagerow.find('td');
if (window.selectedlanguagerow) {
let cells = window.selectedlanguagerow.find('td');
/** @type {Language} */
let ll = {
index: cells.eq(0).text(),
@@ -239,8 +221,10 @@ $(document).ready(function () {
ll.tag = tag;
ll.language = langString;
fetchAPI(APIURL + "UpdateByIndex/" + ll.index, "PATCH", {}, ll, (okdata) => {
reloadLanguageBank(APIURL);
alert("Success edit language : " + okdata.message);
reloadLanguageBank(APIURL, () => {
fill_languagebanktablebody(window.languagebankdata);
alert("Success edit language : " + okdata.message);
});
}, (errdata) => {
alert("Error edit language : " + errdata.message);
});
@@ -262,8 +246,10 @@ $(document).ready(function () {
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadLanguageBank(APIURL);
alert("Success import languagebank : " + okdata.message);
reloadLanguageBank(APIURL, () => {
fill_languagebanktablebody(window.languagebankdata);
alert("Success import languagebank : " + okdata.message);
});
}, (errdata) => {
alert("Error importing languagebank from XLSX : " + errdata.message);
});

View File

@@ -10,7 +10,7 @@
/** List of Log data loaded from server
* @type {Log[]}
*/
let logdata = [];
window.logdata = [];
/**
* Fill log table body with values
@@ -18,9 +18,11 @@ let logdata = [];
*/
function fill_logtablebody(vv) {
$('#logtablebody').empty();
$('#btnExport').prop('disabled', true);
$('#searchfilter').prop('disabled', true);
if (!Array.isArray(vv) || vv.length === 0) return;
if (!Array.isArray(vv) || vv.length === 0) {
$('#btnExport').prop('disabled', true);
return;
}
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
@@ -33,7 +35,6 @@ function fill_logtablebody(vv) {
});
$('#tablesize').text("Table Size: " + vv.length);
$('#btnExport').prop('disabled', false);
$('#searchfilter').prop('disabled', false);
}
/**
@@ -47,10 +48,11 @@ function reloadLogs(APIURL = "Log/", date, filter) {
date: date,
filter: filter
})
window.logdata = [];
fetchAPI(APIURL + "List?" + params.toString(), "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
logdata = okdata;
fill_logtablebody(okdata);
window.logdata.push(...okdata);
fill_logtablebody(window.logdata);
}
}, (errdata) => {
alert("Error loading logs : " + errdata.message);
@@ -59,26 +61,22 @@ function reloadLogs(APIURL = "Log/", date, filter) {
$(document).ready(function () {
console.log("log.js ready");
const $logdate = $('#logdate');
const $searchfilter = $('#searchfilter');
const $logtable = $('#logtablebody')
const $btnExport = $('#btnExport');
let selectedlogdate = "";
let logfilter = "";
let APIURL = "Log/";
$logtable.empty();
$('#logtablebody').empty();
if (!$logdate.val()) {
if (!$('#logdate').val()) {
const today = new Date();
const dd = String(today.getDate()).padStart(2, '0');
const mm = String(today.getMonth() + 1).padStart(2, '0');
const yyyy = today.getFullYear();
$logdate.val(`${yyyy}-${mm}-${dd}`);
$('#logdate').val(`${yyyy}-${mm}-${dd}`);
selectedlogdate = `${dd}-${mm}-${yyyy}`;
reloadLogs(APIURL, selectedlogdate, logfilter);
}
$logdate.off('change').on('change', function () {
$('#logdate').off('change').on('change', function () {
const selected = $(this).val();
if (selected) {
const [year, month, day] = selected.split('-');
@@ -87,11 +85,11 @@ $(document).ready(function () {
}
});
$searchfilter.off('input').on('input', function () {
$('#searchfilter').off('input').on('input', function () {
logfilter = $(this).val();
reloadLogs(APIURL, selectedlogdate, logfilter);
});
$btnExport.off('click').on('click', function () {
$('#btnExport').off('click').on('click', function () {
DoExport(APIURL, "log.xlsx", { date: selectedlogdate, filter: logfilter });
});
});

View File

@@ -1,24 +1,9 @@
/**
* @typedef {Object} MessageBank
* @property {number} index
* @property {string} description
* @property {string} language
* @property {number} aNN_ID
* @property {string} voice_Type
* @property {string} message_Detail
* @property {string} message_TAGS
*/
/**
* List of Messagebank data loaded from server
* @type {MessageBank[]}
*/
let messagebankdata = [];
/**
* Currently selected messagebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedmessagerow = null;
window.selectedmessagerow = null;
/**
* Fill messagebank table body with values
@@ -40,17 +25,17 @@ function fill_messagebanktablebody(vv) {
$('#messagebanktablebody').append(row);
let $addedrow = $('#messagebanktablebody tr:last');
$addedrow.click(function () {
if (selectedmessagerow) {
selectedmessagerow.find('td').css('background-color', '');
if (selectedmessagerow.is($(this))) {
selectedmessagerow = null;
if (window.selectedmessagerow) {
window.selectedmessagerow.find('td').css('background-color', '');
if (window.selectedmessagerow.is($(this))) {
window.selectedmessagerow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$addedrow.find('td').css('background-color', '#ffeeba');
selectedmessagerow = $addedrow;
window.selectedmessagerow = $addedrow;
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -59,27 +44,12 @@ function fill_messagebanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload message bank from server
* @param {string} APIURL API URL endpoint, default "MessageBank/"
*/
function reloadMessageBank(APIURL = "MessageBank/") {
messagebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
messagebankdata = okdata;
selectedmessagerow = null;
fill_messagebanktablebody(messagebankdata);
}
}, (errdata) => {
alert("Error loading messagebank : " + errdata.message);
});
}
$(document).ready(function () {
console.log("messagebank.js loaded");
$('#messagebanktablebody').empty();
selectedmessagerow = null;
window.selectedmessagerow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
@@ -198,21 +168,25 @@ $(document).ready(function () {
$findmessage.on('input', function () {
let searchTerm = $findmessage.val().toLowerCase();
if (searchTerm.length > 0) {
selectedmessagerow = null;
let filtered = messagebankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.message_Detail.toLowerCase().includes(searchTerm) || item.message_TAGS.toLowerCase().includes(searchTerm));
window.selectedmessagerow = null;
let filtered = window.messagebankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.message_Detail.toLowerCase().includes(searchTerm) || item.message_TAGS.toLowerCase().includes(searchTerm));
fill_messagebanktablebody(filtered);
} else {
selectedmessagerow = null;
fill_messagebanktablebody(messagebankdata);
window.selectedmessagerow = null;
fill_messagebanktablebody(window.messagebankdata);
}
});
reloadMessageBank(APIURL);
reloadMessageBank(APIURL, () => {
fill_messagebanktablebody(window.messagebankdata);
});
$btnClear.click(() => {
DoClear(APIURL, "Messagebank", (okdata) => {
reloadMessageBank(APIURL);
alert("Success clear messagebank : " + okdata.message);
reloadMessageBank(APIURL, () => {
fill_messagebanktablebody(window.messagebankdata);
alert("Success clear messagebank : " + okdata.message);
});
}, (errdata) => {
alert("Error clear messagebank : " + errdata.message);
});
@@ -287,8 +261,10 @@ $(document).ready(function () {
};
// send to server using fetchAPI
fetchAPI(APIURL + "Add", "POST", mb, null, (okdata) => {
reloadMessageBank(APIURL);
alert("Success add new messagebank : " + okdata.message);
reloadMessageBank(APIURL, () => {
fill_messagebanktablebody(window.messagebankdata);
alert("Success add new messagebank : " + okdata.message);
});
}, (errdata) => {
alert("Error add new messagebank : " + errdata.message);
});
@@ -301,8 +277,8 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedmessagerow) {
let cells = selectedmessagerow.find('td');
if (window.selectedmessagerow) {
let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */
let mb = {
index: cells.eq(0).text(),
@@ -316,8 +292,10 @@ $(document).ready(function () {
if (confirm(`Are you sure to delete messagebank [${mb.index}] Description=${mb.description}? ANN_ID=${mb.aNN_ID} Language=${mb.language} Voice_Type=${mb.voice_Type} `)) {
fetchAPI(APIURL + "DeleteByIndex/" + mb.index, "DELETE", {}, null, (okdata) => {
reloadMessageBank(APIURL);
alert("Success delete messagebank : " + okdata.message);
reloadMessageBank(APIURL, () => {
fill_messagebanktablebody(window.messagebankdata);
alert("Success delete messagebank : " + okdata.message);
});
}, (errdata) => {
alert("Error delete messagebank : " + errdata.message);
});
@@ -325,8 +303,8 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedmessagerow) {
let cells = selectedmessagerow.find('td');
if (window.selectedmessagerow) {
let cells = window.selectedmessagerow.find('td');
/** @type {MessageBank} */
let mb = {
index: cells.eq(0).text(),
@@ -423,8 +401,10 @@ $(document).ready(function () {
Message_TAGS: messagetags
};
fetchAPI(APIURL + "UpdateByIndex/" + mb.index, "PATCH", mbUpdate, null, (okdata) => {
reloadMessageBank(APIURL);
alert("Success edit messagebank : " + okdata.message);
reloadMessageBank(APIURL, () => {
fill_messagebanktablebody(window.messagebankdata);
alert("Success edit messagebank : " + okdata.message);
});
}, (errdata) => {
alert("Error edit messagebank : " + errdata.message);
});
@@ -443,8 +423,11 @@ $(document).ready(function () {
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadMessageBank(APIURL);
alert("Success import messagebank : " + okdata.message);
reloadMessageBank(APIURL, () => {
fill_messagebanktablebody(window.messagebankdata);
alert("Success import messagebank : " + okdata.message);
});
}, (errdata) => {
alert("Error importing messagebank from XLSX : " + errdata.message);
});

View File

@@ -1,25 +1,9 @@
/**
* @typedef {Object} ScheduleBank
* @property {number} index
* @property {string} description
* @property {string} day
* @property {string} time
* @property {string} soundpath
* @property {number} repeat
* @property {boolean} enable
* @property {string} broadcastZones
* @property {string} language
*/
/** List of Schedulebank data loaded from server
* @type {ScheduleBank[]}
*/
let schedulebankdata = [];
/**
* Currently selected schedulebank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedschedulerow = null;
window.selectedschedulerow = null;
/**
* Fill schedulebank table body with values
@@ -61,22 +45,7 @@ function fill_schedulebanktablebody(vv) {
$('#tablesize').text("Table Size: " + vv.length);
}
/**
* Reload timer bank from server
* @param {string} APIURL API URL endpoint, default "ScheduleBank/"
*/
function reloadTimerBank(APIURL = "ScheduleBank/") {
schedulebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
schedulebankdata = okdata;
selectedschedulerow = null;
fill_schedulebanktablebody(schedulebankdata);
}
}, (errdata) => {
alert("Error loading schedulebank : " + errdata.message);
});
}
$(document).ready(function () {
console.log("schedulebank.js loaded successfully");
@@ -101,7 +70,7 @@ $(document).ready(function () {
let $schedulehour = $schedulemodal.find('#schedulehour');
// number input 0-59
let $scheduleminute = $schedulemodal.find('#scheduleminute');
// text input
// select for messagebank
let $schedulesoundpath = $schedulemodal.find('#schedulesoundpath');
// number input 0-5
let $schedulerepeat = $schedulemodal.find('#schedulerepeat');
@@ -137,8 +106,17 @@ $(document).ready(function () {
$scheduledescription.val('');
$schedulehour.val('0');
$scheduleminute.val('0');
$schedulesoundpath.val('');
$schedulerepeat.val('0');
$schedulesoundpath.empty();
if (Array.isArray(window.messagebankdata) && window.messagebankdata.length > 0) {
window.messagebankdata.forEach(item => {
let str = item.description+" ["+item.aNN_ID+"]";
let option = `<option value="${str}">${str}</option>`;
if ($schedulesoundpath.find(`option[value="${str}"]`).length === 0) $schedulesoundpath.append(option); // check if $schedulesoundpath already has this option
})
}
$schedulerepeat.val('1');
$scheduleenable.prop('checked', true);
$scheduleeveryday.prop('checked', false);
$schedulesunday.prop('checked', false);
@@ -158,24 +136,28 @@ $(document).ready(function () {
$findschedule.on('input', function () {
let searchTerm = $findschedule.val().toLowerCase();
if (searchTerm.length > 0) {
selectedtimerow = null;
let filtered = schedulebankdata.filter(item =>
window.selectedschedulerow = null;
let filtered = window.schedulebankdata.filter(item =>
item.description.toLowerCase().includes(searchTerm)
|| item.soundpath.toLowerCase().includes(searchTerm)
|| item.broadcastZones.toLowerCase().includes(searchTerm));
fill_schedulebanktablebody(filtered);
} else {
selectedtimerow = null;
fill_schedulebanktablebody(schedulebankdata);
window.selectedschedulerow = null;
fill_schedulebanktablebody(window.schedulebankdata);
}
});
reloadTimerBank(APIURL);
reloadTimerBank(APIURL, () => {
fill_schedulebanktablebody(window.schedulebankdata);
});
$btnClear.click(() => {
DoClear(APIURL, "Timerbank", (okdata) => {
reloadTimerBank(APIURL);
alert("Success clear schedulebank : " + okdata.message);
reloadTimerBank(APIURL,() => {
fill_schedulebanktablebody(window.schedulebankdata);
alert("Success clear schedulebank : " + okdata.message);
});
}, (errdata) => {
alert("Error clear schedulebank : " + errdata.message);
});
@@ -236,8 +218,10 @@ $(document).ready(function () {
};
fetchAPI(APIURL + "Add", "POST", {}, scheduleObj, (okdata) => {
alert("Success add schedule: " + okdata.message);
reloadTimerBank(APIURL);
reloadTimerBank(APIURL, () => {
fill_schedulebanktablebody(window.schedulebankdata);
alert("Success add schedule: " + okdata.message);
});
}, (errdata) => {
alert("Error add schedule: " + errdata.message);
});
@@ -246,8 +230,8 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedtimerow) {
let cells = selectedtimerow.find('td');
if (window.selectedschedulerow) {
let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */
let sr = {
index: cells.eq(0).text(),
@@ -262,8 +246,10 @@ $(document).ready(function () {
}
if (confirm(`Are you sure to delete schedule [${sr.index}] Description=${sr.description}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + sr.index, "DELETE", {}, null, (okdata) => {
reloadTimerBank(APIURL);
alert("Success delete schedule : " + okdata.message);
reloadTimerBank(APIURL, () => {
fill_schedulebanktablebody(window.schedulebankdata);
alert("Success delete schedule : " + okdata.message);
});
}, (errdata) => {
alert("Error delete schedule : " + errdata.message);
});
@@ -271,8 +257,8 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedtimerow) {
let cells = selectedtimerow.find('td');
if (window.selectedschedulerow) {
let cells = window.selectedschedulerow.find('td');
/** @type {ScheduleBank} */
let sr = {
index: cells.eq(0).text(),
@@ -380,8 +366,10 @@ $(document).ready(function () {
};
fetchAPI(APIURL + "UpdateByIndex/" + sr.index, "PATCH", {}, scheduleObj, (okdata) => {
alert("Success edit schedule: " + okdata.message);
reloadTimerBank(APIURL);
reloadTimerBank(APIURL, () => {
fill_schedulebanktablebody(window.schedulebankdata);
alert("Success edit schedule: " + okdata.message);
});
}, (errdata) => {
alert("Error edit schedule: " + errdata.message);
});
@@ -396,8 +384,10 @@ $(document).ready(function () {
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadTimerBank(APIURL);
alert("Success import schedulebank from XLSX : " + okdata.message);
reloadTimerBank(APIURL, () => {
fill_schedulebanktablebody(window.schedulebankdata);
alert("Success import schedulebank from XLSX : " + okdata.message);
});
}, (errdata) => {
alert("Error importing schedulebank from XLSX : " + errdata.message);
});

View File

@@ -2,23 +2,280 @@
* List of voice types available
* @type {string[]}
*/
let voiceTypes = [];
window.voiceTypes = [];
/**
* List of categories available
* @type {string[]}
*/
let categories = [];
window.categories = [];
/**
* List of languages available
* @type {string[]}
*/
let languages = [];
window.languages = [];
/**
* List of scheduled days available
* @type {string[]}
*/
let scheduledays = []
window.scheduledays = []
/**
* @typedef {Object} MessageBank
* @property {number} index
* @property {string} description
* @property {string} language
* @property {number} aNN_ID
* @property {string} voice_Type
* @property {string} message_Detail
* @property {string} message_TAGS
*/
/**
* List of Messagebank data loaded from server
* taruh di sini karena dipakai di banyak tempat
* @type {MessageBank[]}
*/
window.messagebankdata ??= [];
/**
* Reload message bank from server
* @param {string} APIURL API URL endpoint, default "MessageBank/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadMessageBank(APIURL = "MessageBank/", callback=null) {
window.messagebankdata ??= [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.messagebankdata.push(...okdata);
console.log("Loaded " + window.messagebankdata.length + " message bank items");
window.selectedmessagerow = null;
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading messagebank : " + errdata.message);
});
}
/**
* @typedef {Object} ScheduleBank
* @property {number} index
* @property {string} description
* @property {string} day
* @property {string} time
* @property {string} soundpath
* @property {number} repeat
* @property {boolean} enable
* @property {string} broadcastZones
* @property {string} language
*/
/** List of Schedulebank data loaded from server
* @type {ScheduleBank[]}
*/
window.schedulebankdata = [];
/**
* Reload timer bank from server
* taruh di sini karena dipakai di banyak tempat
* @param {string} APIURL API URL endpoint, default "ScheduleBank/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadTimerBank(APIURL = "ScheduleBank/", callback=null) {
window.schedulebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.schedulebankdata.push(...okdata);
selectedschedulerow = null;
console.log("Loaded " + window.schedulebankdata.length + " schedule bank items");
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading schedulebank : " + errdata.message);
});
}
/**
* @typedef {Object} BroadcastZone
* @property {number} index
* @property {string} description
* @property {String} SoundChannel
* @property {String} Box
* @property {String} Relay
*/
/**
* List of broadcast zones available
* @type {BroadcastZone[]}
*/
window.BroadcastZoneList ??= [];
/**
* Reload broadcast zones from server
* taruh di sini karena dipakai di banyak tempat
* @param {String} APIURL API URL endpoint (default "BroadcastZones/")
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadBroadcastZones(APIURL = "BroadcastZones/", callback=null) {
window.BroadcastZoneList = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadBroadcastZones : ", okdata)
window.BroadcastZoneList.push(...okdata);
console.log("Loaded " + window.BroadcastZoneList.length + " broadcast zone items");
window.selectedbroadcastzonerow = null;
if (callback && typeof callback === 'function') callback();
} else console.log("reloadBroadcastZones: okdata is not array");
}, (errdata) => {
alert("Error loading broadcast zones : " + errdata.message);
});
}
/**
* @typedef {Object} SoundBank
* @property {number} index
* @property {string} description
* @property {string} tag
* @property {string} category
* @property {string} language
* @property {string} voiceType
* @property {string} path
*/
/**
* @typedef {Object} Select2item
* @property {number} id
* @property {string} text
*/
/**
* List of Soundbank data loaded from server
* taruh di sini karena dipakai di banyak tempat
* @type {SoundBank[]}
*/
window.soundbankdata = [];
/**
* List of sound files in the soundbank directory, that ends with .wav or .mp3
* taruh di sini karena dipakai di banyak tempat
* @type {string[]}
*/
window.soundbankfiles = [];
/**
* Select2 data source
* See https://select2.org/data-sources/formats
* @type {Select2item[]}
*/
window.select2data = [];
/**
* Reload sound bank from server
* @param {String} APIURL API URL endpoint, default "SoundBank/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadSoundBank(APIURL = "SoundBank/", callback=null) {
window.soundbankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.soundbankdata.push(...okdata);
window.selectedsoundrow = null;
console.log("Loaded " + window.soundbankdata.length + " sound bank items");
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading soundbank : " + errdata.message);
});
}
/**
* Reload soundbank files from server
* @param {String} APIURL API URL endpoint (default "SoundBank/")
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadSoundbankFiles(APIURL = "SoundBank/",callback=null) {
window.soundbankfiles = [];
fetchAPI(APIURL + "ListFiles", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
window.soundbankfiles = okdata.filter(item => item.trim().length > 0);
// refill select2data
window.select2data = window.soundbankfiles.map((item, index) => ({ id: index + 1, text: item }));
console.log("Loaded " + window.soundbankfiles.length + " sound bank files");
if (callback && typeof callback === 'function') callback();
} else console.log("reloadSoundbankFiles: okdata is not array");
}, (errdata) => {
alert("Error loading soundbank files : " + errdata.message);
});
}
/**
* @typedef {Object} SoundChannel
* @property {number} index - The index of the sound channel.
* @property {string} channel - The name of the sound channel.
* @property {string} ip - The IP address associated with the sound channel.
*/
/**
* @type {SoundChannel[]}
*/
window.soundChannels = [];
/**
* Reload sound channels from server
* @param {String} APIURL API URL endpoint (default "SoundChannel/")
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadSoundChannel(APIURL = "SoundChannel/", callback=null) {
window.soundChannels = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadSoundChannel : ", okdata)
window.soundChannels.push(...okdata);
window.selectedsoundchannelrow = null;
console.log("Loaded " + window.soundChannels.length + " sound channel items");
if (callback && typeof callback === 'function') callback();
} else console.log("reloadSoundChannel: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
});
}
/**
* @typedef {Object} LanguageBank
* @property {number} index
* @property {string} tag
* @property {string} language
*
*/
/** List of Languagebank data loaded from server
* @type {LanguageBank[]}
*/
window.languagebankdata = [];
/**
* Reload language bank from server
* @param {string} APIURL API URL endpoint, default "LanguageLink/"
* @param {Function|null} callback Optional callback function to execute after loading
*/
function reloadLanguageBank(APIURL = "LanguageLink/", callback=null) {
window.languagebankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.languagebankdata.push(...okdata);
window.selectedlanguagerow = null;
console.log("Loaded " + window.languagebankdata.length + " language bank items");
if (callback && typeof callback === 'function') callback();
}
}, (errdata) => {
alert("Error loading languagebank : " + errdata.message);
});
}
/**
* Create a list item element
@@ -34,7 +291,7 @@ function ListItem(text, className = "") {
* WebSocket connection
* @type {WebSocket}
*/
let ws = null;
window.ws = null;
/**
* Send a command to the WebSocket server.
@@ -42,8 +299,8 @@ let ws = null;
* @param {String} data data to send
*/
function sendCommand(command, data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command, data }));
if (window.ws.readyState === WebSocket.OPEN) {
window.ws.send(JSON.stringify({ command, data }));
}
}
@@ -118,11 +375,11 @@ function fetchImg(url, cbOK, cbError) {
* Reload voice types from server
*/
function getVoiceTypes() {
voiceTypes = [];
window.voiceTypes = [];
fetchAPI("VoiceType", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
voiceTypes = okdata.filter(item => item.trim().length > 0);
window.voiceTypes = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + voiceTypes.length + " voice types : " + voiceTypes.join(", "));
} else console.log("getVoiceTypes: okdata is not array");
}, (errdata) => {
@@ -134,11 +391,11 @@ function getVoiceTypes() {
* Reload categories from server
*/
function getCategories() {
categories = [];
window.categories = [];
fetchAPI("Category", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
categories = okdata.filter(item => item.trim().length > 0);
window.categories = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + categories.length + " categories : " + categories.join(", "));
} else console.log("getCategories: okdata is not array");
}, (errdata) => {
@@ -150,11 +407,11 @@ function getCategories() {
* Reload languages from server
*/
function getLanguages() {
languages = [];
window.languages = [];
fetchAPI("Language", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
languages = okdata.filter(item => item.trim().length > 0);
window.languages = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + languages.length + " languages : " + languages.join(", ") );
} else console.log("getLanguages: okdata is not array");
}, (errdata) => {
@@ -166,11 +423,11 @@ function getLanguages() {
* Reload scheduled days from server
*/
function getScheduledDays() {
scheduledays = [];
window.scheduledays = [];
fetchAPI("ScheduleDay", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
scheduledays = okdata.filter(item => item.trim().length > 0);
window.scheduledays = okdata.filter(item => item.trim().length > 0);
//console.log("Loaded " + scheduledays.length + " scheduled days : " + scheduledays.join(", ") );
} else console.log("getScheduledDays: okdata is not array");
}, (errdata) => {
@@ -273,43 +530,36 @@ function DoImport(APIURL, cbOK, cbError) {
fileInput.remove();
}
let $onlineindicator = null;
let $cpustatus = null;
let $ramstatus = null;
let $diskstatus = null;
let $networkstatus = null;
let $datetimetext = null;
let greencircle = null;
let redcircle = null;
window.greencircle = null;
window.redcircle = null;
/**
* App entry point
*/
$(document).ready(function () {
document.title = "Automatic Announcement System"
fetchImg('green_circle.png', (url) => { greencircle = url; }, (err) => { console.error("Error loading green_circle.png : ", err); });
fetchImg('red_circle.png', (url) => { redcircle = url; }, (err) => { console.error("Error loading red_circle.png : ", err); });
if (window.greencircle === null){
fetchImg('green_circle.png', (url) => { window.greencircle = url; }, (err) => { console.error("Error loading green_circle.png : ", err); });
}
if (window.redcircle === null){
fetchImg('red_circle.png', (url) => { window.redcircle = url; }, (err) => { console.error("Error loading red_circle.png : ", err); });
}
const wsURL = window.location.pathname + '/ws'
if (chrome && chrome.runtime && chrome.runtime.lastError) {
alert("Runtime error: " + chrome.runtime.lastError.message);
return;
}
$onlineindicator = $('#onlineindicator');
$cpustatus = $('#cpustatus');
$ramstatus = $('#ramstatus');
$diskstatus = $('#diskstatus');
$networkstatus = $('#networkstatus');
$datetimetext = $('#datetimetext');
// reset status indicators
function resetStatusIndicators() {
$onlineindicator.attr('src', redcircle);
$cpustatus.text("CPU : N/A");
$ramstatus.text("RAM : N/A");
$diskstatus.text("Disk : N/A");
$networkstatus.text("Network : N/A");
$datetimetext.text("Date/Time : N/A");
$('#onlineindicator').attr('src', window.redcircle);
$('#cpustatus').text("CPU : N/A");
$('#ramstatus').text("RAM : N/A");
$('#diskstatus').text("Disk : N/A");
$('#networkstatus').text("Network : N/A");
$('#datetimetext').text("Date/Time : N/A");
}
@@ -318,47 +568,56 @@ $(document).ready(function () {
getCategories();
getLanguages();
getScheduledDays();
reloadMessageBank();
reloadTimerBank();
reloadBroadcastZones();
reloadSoundBank();
reloadSoundbankFiles();
reloadSoundChannel();
reloadLanguageBank();
// Initialize WebSocket connection
ws = new WebSocket(wsURL);
window.ws = new WebSocket(wsURL);
ws.onopen = () => {
window.ws.onopen = () => {
console.log('WebSocket connection established');
$onlineindicator.attr('src', greencircle);
$('#onlineindicator').attr('src', window.greencircle);
};
ws.onmessage = (event) => {
window.ws.onmessage = (event) => {
if ($('#onlineindicator').attr('src') !== window.greencircle) {
$('#onlineindicator').attr('src', window.greencircle);
}
let rep = JSON.parse(event.data);
let cmd = rep.reply
let data = rep.data;
if (cmd && cmd.length > 0) {
switch (cmd) {
case "getCPUStatus":
$cpustatus.text("CPU : " + data)
$('#cpustatus').text("CPU : " + data)
break;
case "getMemoryStatus":
$ramstatus.text("RAM : " + data)
$('#ramstatus').text("RAM : " + data)
break;
case "getDiskStatus":
$diskstatus.text("Disk : " + data)
$('#diskstatus').text("Disk : " + data)
break;
case "getNetworkStatus":
$networkstatus.text("Network : " + data)
$('#networkstatus').text("Network : " + data)
break;
case "getSystemTime":
$datetimetext.text(data)
$('#datetimetext').text(data)
break;
}
}
};
ws.onclose = () => {
window.ws.onclose = () => {
console.log('WebSocket connection closed');
resetStatusIndicators();
};
// ws.onerror = (error) => {
// window.ws.onerror = (error) => {
// console.error('WebSocket error:', error);
// };
@@ -457,6 +716,18 @@ $(document).ready(function () {
}
});
})
$('#usermanagement').click(() => {
sidemenu.hide();
$('#content').load('usermanagement.html', function (response, status, xhr) {
if (status === "success") {
console.log("User Management content loaded successfully");
// pindah ke usermanagement.js
} else {
console.error("Error loading user management content:", xhr.status, xhr.statusText);
}
});
});
$('#settinglink').click(() => {
sidemenu.hide();
$('#content').load('setting.html', function (response, status, xhr) {

View File

@@ -1,61 +1,11 @@
/**
* @typedef {Object} SoundBank
* @property {number} index
* @property {string} description
* @property {string} tag
* @property {string} category
* @property {string} language
* @property {string} voiceType
* @property {string} path
*/
/**
* @typedef {Object} Select2item
* @property {number} id
* @property {string} text
*/
/**
* List of Soundbank data loaded from server
* @type {SoundBank[]}
*/
let soundbankdata = [];
/**
* Currently selected soundbank row in the table
* @type {JQuery<HTMLElement>|null}
*/
let selectedsoundrow = null;
window.selectedsoundrow = null;
/**
* List of sound files in the soundbank directory, that ends with .wav or .mp3
* @type {string[]}
*/
let soundbankfiles = [];
/**
* Select2 data source
* See https://select2.org/data-sources/formats
* @type {Select2item[]}
*/
let select2data = [];
/**
* Reload sound bank from server
* @param {String} APIURL API URL endpoint, default "SoundBank/"
*/
function reloadSoundBank(APIURL = "SoundBank/") {
soundbankdata = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
soundbankdata = okdata;
selectedsoundrow = null;
fill_soundbanktablebody(soundbankdata);
}
}, (errdata) => {
alert("Error loading soundbank : " + errdata.message);
});
}
/**
* Fill soundbank table body with values
@@ -63,6 +13,7 @@ function reloadSoundBank(APIURL = "SoundBank/") {
*/
function fill_soundbanktablebody(vv) {
$('#soundbanktablebody').empty();
console.log("Filling soundbank table with " + vv.length + " items");
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
const row = `<tr>
@@ -77,17 +28,17 @@ function fill_soundbanktablebody(vv) {
$('#soundbanktablebody').append(row);
let $addedrow = $('#soundbanktablebody tr:last');
$addedrow.on('click', function () {
if (selectedsoundrow) {
selectedsoundrow.find('td').css('background-color', '');
if (selectedsoundrow.is($(this))) {
selectedsoundrow = null;
if (window.selectedsoundrow) {
window.selectedsoundrow.find('td').css('background-color', '');
if (window.selectedsoundrow.is($(this))) {
window.selectedsoundrow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$(this).find('td').css('background-color', '#ffeeba');
selectedsoundrow = $(this);
window.selectedsoundrow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
@@ -98,29 +49,13 @@ function fill_soundbanktablebody(vv) {
}
/**
* Reload soundbank files from server
* @param {String} APIURL API URL endpoint (default "SoundBank/")
*/
function reloadSoundbankFiles(APIURL = "SoundBank/") {
soundbankfiles = [];
fetchAPI(APIURL + "ListFiles", "GET", {}, null, (okdata) => {
// okdata is a string contains elements separated by semicolon ;
if (Array.isArray(okdata)) {
soundbankfiles = okdata.filter(item => item.trim().length > 0);
// refill select2data
select2data = soundbankfiles.map((item, index) => ({ id: index + 1, text: item }));
} else console.log("reloadSoundbankFiles: okdata is not array");
}, (errdata) => {
alert("Error loading soundbank files : " + errdata.message);
});
}
$(document).ready(function () {
console.log("soundbank.js loaded successfully");
reloadSoundbankFiles();
$('#soundbanktablebody').empty();
selectedsoundrow = null;
window.selectedsoundrow = null;
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
@@ -166,28 +101,33 @@ $(document).ready(function () {
$modalvoicetype.val(null);
// fill modalpath options from soundbankfiles[]
// TODO read https://jeesite.com/front/jquery-select2/4.0/index.htm
console.log("select2data has " + select2data.length + " items");
console.log("window.select2data has " + window.select2data.length + " items");
$('#modalpath').select2({
data: select2data
data: window.select2data
})
}
reloadSoundBank(APIURL);
reloadSoundBank(APIURL, () => {
fill_soundbanktablebody(window.soundbankdata);
});
$('#findsoundbank').on('input', function () {
let searchTerm = $(this).val().trim().toLowerCase();
if (searchTerm.length > 0) {
selectedsoundrow = null;
let filtered = soundbankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.tag.toLowerCase().includes(searchTerm) || item.path.toLowerCase().includes(searchTerm));
window.selectedsoundrow = null;
let filtered = window.soundbankdata.filter(item => item.description.toLowerCase().includes(searchTerm) || item.tag.toLowerCase().includes(searchTerm) || item.path.toLowerCase().includes(searchTerm));
fill_soundbanktablebody(filtered);
} else {
selectedsoundrow = null;
fill_soundbanktablebody(soundbankdata);
window.selectedsoundrow = null;
fill_soundbanktablebody(window.soundbankdata);
}
});
$btnClear.click(() => {
DoClear(APIURL, "Soundbank", (okdata) => {
reloadSoundBank(APIURL);
alert("Success clear soundbank : " + okdata.message);
reloadSoundBank(APIURL, () => {
fill_soundbanktablebody(window.soundbankdata);
alert("Success clear soundbank : " + okdata.message);
});
}, (errdata) => {
alert("Error clear soundbank : " + errdata.message);
});
@@ -208,8 +148,8 @@ $(document).ready(function () {
});
});
$btnRemove.click(() => {
if (selectedsoundrow) {
let cells = selectedsoundrow.find('td');
if (window.selectedsoundrow) {
let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */
let sb = {
index: cells.eq(0).text(),
@@ -222,8 +162,10 @@ $(document).ready(function () {
}
if (confirm(`Are you sure to delete soundbank [${sb.index}] Description=${sb.description} Tag=${sb.tag}?`)) {
fetchAPI(APIURL + "DeleteByIndex/" + sb.index, "DELETE", {}, null, (okdata) => {
reloadSoundBank(APIURL);
alert("Success delete soundbank : " + okdata.message);
reloadSoundBank(APIURL, () => {
fill_soundbanktablebody(window.soundbankdata);
alert("Success delete soundbank : " + okdata.message);
});
}, (errdata) => {
alert("Error delete soundbank : " + errdata.message);
});
@@ -231,8 +173,8 @@ $(document).ready(function () {
}
});
$btnEdit.click(() => {
if (selectedsoundrow) {
let cells = selectedsoundrow.find('td');
if (window.selectedsoundrow) {
let cells = window.selectedsoundrow.find('td');
/** @type {SoundBank} */
let sb = {
index: cells.eq(0).text(),
@@ -265,8 +207,10 @@ $(document).ready(function () {
});
$btnImport.click(() => {
DoImport(APIURL, (okdata) => {
reloadSoundBank(APIURL);
alert("Success import soundbank : " + okdata.message);
reloadSoundBank(APIURL, () => {
fill_soundbanktablebody(window.soundbankdata);
alert("Success import soundbank : " + okdata.message);
});
}, (errdata) => {
alert("Error importing soundbank from XLSX : " + errdata.message);
});

View File

@@ -1,27 +1,18 @@
/**
* @typedef {Object} SoundChannel
* @property {number} index - The index of the sound channel.
* @property {string} channel - The name of the sound channel.
* @property {string} ip - The IP address associated with the sound channel.
*/
/**
* @type {SoundChannel[]}
*/
let soundChannels = [];
// Currently selected sound channel row in the table
let selectedSoundChannel = null;
window.selectedSoundChannel = null;
/**
* Fills the sound channel table body with the provided data.
* @param {SoundChannel[]} vv Sound channel data to populate the table.
*/
function fill_soundchanneltablebody(vv) {
const $tbody = $('#soundchanneltablebody');
const $btnEditSoundChannel = $('#btnEditSoundChannel');
const $tablesizeSoundChannel = $('#tablesizeSoundChannel');
let $tbody = $('#soundchanneltablebody');
let $btnEditSoundChannel = $('#btnEditSoundChannel');
let $tablesizeSoundChannel = $('#tablesizeSoundChannel');
$tbody.empty();
$tablesizeSoundChannel.text('Table Length : N/A');
if (!Array.isArray(vv) || vv.length === 0) return;
@@ -50,22 +41,7 @@ function fill_soundchanneltablebody(vv) {
$tablesizeSoundChannel.text("Table Size: " + vv.length);
}
/**
* Reload sound channels from server
* @param {String} APIURL API URL endpoint (default "SoundChannel/")
*/
function reloadSoundChannel(APIURL = "SoundChannel/") {
SoundChannelList = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
//console.log("reloadSoundChannel : ", okdata)
SoundChannelList = okdata;
fill_soundchanneltablebody(SoundChannelList);
} else console.log("reloadSoundChannel: okdata is not array");
}, (errdata) => {
alert("Error loading sound channels : " + errdata.message);
});
}
$(document).ready(function () {
console.log("soundchannel.js loaded successfully");
@@ -85,12 +61,14 @@ $(document).ready(function () {
$findsoundchannel.on('input', function () {
let searchTerm = $(this).val().toLowerCase();
if (searchTerm.length==0){
fill_soundchanneltablebody(SoundChannelList);
window.selectedSoundChannel = null;
fill_soundchanneltablebody(window.soundChannels);
} else {
let filteredChannels = SoundChannelList.filter(channel =>
channel.index.toString().includes(searchTerm) ||
channel.description.toLowerCase().includes(searchTerm) ||
channel.ip.toLowerCase().includes(searchTerm)
window.selectedSoundChannel = null;
let filteredChannels = window.soundChannels.filter(xx =>
xx.index.toString().includes(searchTerm) ||
xx.channel.toLowerCase().includes(searchTerm) ||
xx.ip.toLowerCase().includes(searchTerm)
);
fill_soundchanneltablebody(filteredChannels);
}
@@ -105,11 +83,15 @@ $(document).ready(function () {
$soundchannelip.val('');
}
reloadSoundChannel(API_SoundChannel);
reloadSoundChannel(API_SoundChannel, () => {
fill_soundchanneltablebody(window.soundChannels);
});
$btnReinitializeSoundChannel.click(() => {
DoClear(API_SoundChannel, "SoundChannels", (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success clear sound channels: " + okdata.message);
reloadSoundChannel(API_SoundChannel, () => {
fill_soundchanneltablebody(window.soundChannels);
alert("Success clear sound channels: " + okdata.message);
});
}, (errdata) => {
alert("Error clear sound channels: " + errdata.message);
});
@@ -153,8 +135,10 @@ $(document).ready(function () {
fetchAPI(API_SoundChannel + "UpdateByIndex/" + newsc.index, "PATCH", {}, newsc, (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success edit sound channel: " + okdata.message);
reloadSoundChannel(API_SoundChannel, () => {
fill_soundchanneltablebody(window.soundChannels);
alert("Success edit sound channel: " + okdata.message);
});
}, (errdata) => {
alert("Error edit sound channel: " + errdata.message);
});
@@ -174,8 +158,10 @@ $(document).ready(function () {
$btnImportSoundChannel.click(() => {
DoImport(API_SoundChannel, (okdata) => {
reloadSoundChannel(API_SoundChannel);
alert("Success import sound channels: " + okdata.message);
reloadSoundChannel(API_SoundChannel, () => {
fill_soundchanneltablebody(window.soundChannels);
alert("Success import sound channels: " + okdata.message);
});
}, (errdata) => {
alert("Error importing sound channels from XLSX: " + errdata.message);
});

View File

@@ -0,0 +1,128 @@
/**
* @typedef {Object} UserDB
* @property {number} index Index number
* @property {string} username Username
* @property {string} password Password (plain)
* @property {string} location Location
* @property {string} soundbank_tags Soundbank variable tags separated by semicolon ;
* @property {string} messagebank_ann_id Messagebank announcement ID separated by semicolon ;
* @property {string} broadcastzones Broadcast zones separated by semicolon ;
*/
/** List of UserDB data loaded from server
* @type {UserDB[]}
*/
window.userdb = [];
/**
* Currently selected user row in table
* @type {JQuery<HTMLElement>|null}
*/
window.selecteduserrow = null;
/**
* Fill user table body with values
* @param {UserDB[]} vv values to fill
*/
function fill_usertablebody(vv) {
$('#usertablebody').empty();
if (!Array.isArray(vv) || vv.length === 0) {
$('#btnExport').prop('disabled', true);
return;
}
vv.forEach(item => {
const row = `<tr>
<td>${item.index}</td>
<td>${item.datenya}</td>
<td>${item.timenya}</td>
<td>${item.machine}</td>
<td>${item.description}</td>
</tr>`;
$('#usertablebody').append(row);
let $addedrow = $('#usertablebody tr:last');
$addedrow.on('click', function () {
if (window.selecteduserrow) {
window.selecteduserrow.find('td').css('background-color', '');
if (window.selecteduserrow.is($(this))) {
window.selecteduserrow = null;
$('#btnRemove').prop('disabled', true);
$('#btnEdit').prop('disabled', true);
return;
}
}
$(this).find('td').css('background-color', '#ffeeba');
window.selecteduserrow = $(this);
$('#btnRemove').prop('disabled', false);
$('#btnEdit').prop('disabled', false);
});
});
$('#tablesize').text("Table Size: " + vv.length);
$('#btnExport').prop('disabled', false);
}
/**
* Reload UserDB from server with date and filter
* @param {String} APIURL API URL endpoint , default "User/"
*/
function reloaduserDB(APIURL = "User/") {
window.userdb = [];
fetchAPI(APIURL + "List", "GET", {}, null, (okdata) => {
if (Array.isArray(okdata)) {
window.userdb.push(...okdata);
fill_usertablebody(window.userdb);
}
}, (errdata) => {
alert("Error loading user database : " + errdata.message);
});
}
$(document).ready(function () {
console.log("usermanagement.js ready");
let $usertablebody = $('#usertablebody');
let $finduser = $('#finduser');
let $btnClear = $('#btnClear');
let $btnAdd = $('#btnAdd');
let $btnRemove = $('#btnRemove');
let $btnEdit = $('#btnEdit');
let $btnExport = $('#btnExport');
let $btnImport = $('#btnImport');
let APIURL = "User/";
// add / edit modal elements
let $addmodal = $('#addmodal');
let $modalindex = $('#modalindex');
let $modalusername = $('#modalusername');
let $modalpassword = $('#modalpassword');
let $modalverifypassword = $('#modalverifypassword');
let $modalsoundbank = $('#modalsoundbank');
let $modalmessagebank = $('#modalmessagebank');
let $modalbroadcastzones = $('#modalbroadcastzones');
let $btnShowSoundbankModal = $('#btnShowSoundbankModal');
let $btnShowMessagebankModal = $('#btnShowMessagebankModal');
let $btnShowBroaadcastZoneModal = $('#btnShowBroaadcastZoneModal');
let $usermanagementsave = $('#usermanagementsave');
let $usermanagementclose = $('#usermanagementclose');
// soundbank selection modal elements
let $soundbankmodal = $('#soundbankmodal');
let $soundbankselection = $('#soundbankselection');
let $soundbankselectionsave = $('#soundbankselectionsave');
let $soundbankselectionclose = $('#soundbankselectionclose');
// broadcast zone selection modal elements
let $broadcastzonemodal = $('#broadcastzonemodal');
let $broadcastzoneselection = $('#broadcastzoneselection');
let $broadcastzoneselectionsave = $('#broadcastzoneselectionsave');
let $broadcastzoneselectionclose = $('#broadcastzoneselectionclose');
// messagebank selection modal elements
let $messagebankmodal = $('#messagebankmodal');
let $messagebankselection = $('#messagebankselection');
let $messagebankselectionsave = $('#messagebankselectionsave');
let $messagebankselectionclose = $('#messagebankselectionclose');
$usertablebody.empty();
$finduser.on('input', function () {
});
});

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
@@ -24,34 +24,34 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 form-control input-add" type="text" id="broadcastzoneindex" placeholder="index"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 form-control input-add" type="text" id="broadcastzoneindex" placeholder="index"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input type="text" id="broadcastzonedescription" class="form-control input-add" placeholder="description"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="broadcastzonedescription" class="form-control input-add" placeholder="description"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Sound Channel</p>
</div>
<div class="col"><select id="broadcastzonesoundchannel" class="form-control input-add" placeholder="sound channel"></select></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="broadcastzonesoundchannel" class="form-control input-add" placeholder="sound channel"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Box</p>
</div>
<div class="col"><input type="text" id="broadcastzonebox" class="form-control input-add" placeholder="Box ID"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="broadcastzonebox" class="form-control input-add" placeholder="Box ID"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Relay</p>
</div>
<div class="col">
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8 pad-relay">
<div class="row">
<div class="col-3">
<div class="row">
@@ -167,9 +167,9 @@
</div>
<div class="row">
<div class="accordion" role="tablist" id="accordion-1">
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-1" aria-expanded="true" aria-controls="accordion-1 .item-1">Sound Channel</button></h2>
<div class="accordion-collapse collapse show item-1" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-item pad-accordion">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed bg-heading1" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-1" aria-expanded="false" aria-controls="accordion-1 .item-1">Sound Channel</button></h2>
<div class="accordion-collapse collapse item-1" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<div class="row">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
@@ -178,11 +178,11 @@
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="findsoundchannel" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnReinitializeSoundChannel" type="button">Re-Initialize</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-edit" id="btnEditSoundChannel" type="button">Edit</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExportSoundChannel" type="button">Export</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnImportSoundChannel" type="button">Import</button></div>
<div class="row pad-search">
<div class="col-6 col-sm-6 col-md-3 col-lg-3 col-xl-3"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnReinitializeSoundChannel" type="button">Re-Initialize</button></div>
<div class="col-6 col-sm-6 col-md-3 col-lg-3 col-xl-3"><button class="btn w-100 pad-button btn-round-basic color-edit" id="btnEditSoundChannel" type="button">Edit</button></div>
<div class="col-6 col-md-3 col-lg-3 col-xl-3 col-sm--6"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExportSoundChannel" type="button">Export</button></div>
<div class="col-6 col-sm-6 col-md-3 col-lg-3 col-xl-3"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnImportSoundChannel" type="button">Import</button></div>
</div>
<div class="row">
<div class="col">
@@ -206,9 +206,9 @@
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="false" aria-controls="accordion-1 .item-2">Broadcast Zones</button></h2>
<div class="accordion-collapse collapse item-2" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-item pad-accordion">
<h2 class="accordion-header" role="tab"><button class="accordion-button bg-heading2" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="true" aria-controls="accordion-1 .item-2">Broadcast Zones</button></h2>
<div class="accordion-collapse collapse show item-2" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<div class="row">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
@@ -217,7 +217,7 @@
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="findzone" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="row pad-search">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnClear" type="button">Clear</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnAdd" type="button">Add</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-remove" id="btnRemove" type="button">Remove</button></div>
@@ -238,8 +238,8 @@
<th class="col-sm-1">No</th>
<th class="col-sm-2">Description</th>
<th class="col-sm-2">SoundChannel</th>
<th class="col-sm-2">Box</th>
<th class="col">Relay</th>
<th class="col-sm-2">ID</th>
<th class="col">BP</th>
</tr>
</thead>
<tbody id="broadcastzonetablebody"></tbody>
@@ -259,25 +259,25 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col-4">
<p>Index</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25" type="text" id="soundchannelindex" readonly="" placeholder="index"></div>
<div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-25 form-control input-add" type="text" id="soundchannelindex" readonly="" placeholder="index"></div>
</div>
<div class="row">
<div class="col-4">
<p>Description</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input class="w-100" type="text" id="soundchanneldescription" placeholder="Description"></div>
<div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-100 form-control input-add" type="text" id="soundchanneldescription" placeholder="Description"></div>
</div>
<div class="row">
<div class="col-4">
<p>IP Address</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">IP Address</p>
</div>
<div class="col"><input class="w-100" type="text" id="soundchannelip" placeholder="IP Address"></div>
<div class="col-8 col-sm-8 col-md-8 col-xl-8 co-lg-8"><input class="w-100 form-control input-add" type="text" id="soundchannelip" placeholder="IP Address"></div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-light" id="soundchannelclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" id="soundchannelsave" type="button">Save</button></div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="soundchannelclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="soundchannelsave" type="button">Save</button></div>
</div>
</div>
</div>

View File

@@ -60,6 +60,10 @@
<path d="M19,7H9C7.9,7,7,7.9,7,9v10c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V9C21,7.9,20.1,7,19,7z M19,9v2H9V9H19z M13,15v-2h2v2H13z M15,17v2h-2v-2H15z M11,15H9v-2h2V15z M17,13h2v2h-2V13z M9,17h2v2H9V17z M17,19v-2h2v2H17z M6,17H5c-1.1,0-2-0.9-2-2V5 c0-1.1,0.9-2,2-2h10c1.1,0,2,0.9,2,2v1h-2V5H5v10h1V17z"></path>
</g>
</svg>&nbsp; &nbsp;Log</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="usermanagement" href="#"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" class="me-2 icon-menu" style="font-size: 20px;">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"></path>
</svg>&nbsp;User Management</a></li>
<li class="nav-item"><a class="nav-link link-body-emphasis text-menu" id="settinglink" href="#"><svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor" class="me-2 icon-menu" style="font-size: 20px;">
<g>
<path d="M0,0h24v24H0V0z" fill="none"></path>

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
@@ -16,7 +16,7 @@
<h2 style="text-align: center;">Language Link</h2>
</div>
</div>
<div class="row">
<div class="row pad-row-search">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<p class="text-add">Search</p>
@@ -58,22 +58,22 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 form-control input-add" type="text" id="languagelinkindex" readonly=""></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 form-control input-add" type="text" id="languagelinkindex" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Tag</p>
</div>
<div class="col"><input type="text" id="languagelinktag" class="form-control input-add"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="languagelinktag" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Language</p>
</div>
<div class="col text-add">
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8 text-add">
<div class="form-check"><input class="form-check-input" type="checkbox" id="langId" name="languages[]" value="id"><label class="form-check-label" for="langId">Indonesia</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="langLocal" name="languages[]" value="id"><label class="form-check-label" for="langId-1">Local</label></div>
<div class="form-check"><input class="form-check-input" type="checkbox" id="langEn" name="languages[]" value="en"><label class="form-check-label" for="langEn">English</label></div>

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
@@ -17,15 +17,16 @@
</div>
</div>
<div class="row">
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2">
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2">
<p class="text-add">Select Log Date</p>
</div>
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2"><input id="logdate" class="form-control" type="date"></div>
<div class="col-md-4 col-lg-4 col-xl-4"><button class="btn btn-primary w-100 h-100" id="btnExport" type="button">Export</button></div>
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2">
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2"><input id="logdate" class="form-control" type="date"></div>
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExport" type="button">Export</button></div>
<div class="col-md-2 col-lg-2 col-xl-2"></div>
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2">
<p class="text-add">Search</p>
</div>
<div class="col-6 col-sm col-md-2 col-lg-2 col-xl-2"><input type="text" id="searchfilter" class="form-control" placeholder="Search Filter"></div>
<div class="col-4 col-sm-4 col-md-2 col-lg-2 col-xl-2"><input type="text" id="searchfilter" class="form-control" placeholder="Search Filter"></div>
</div>
<div class="row">
<div class="col">

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
@@ -16,12 +16,12 @@
<h2 style="text-align: center;">Message Bank</h2>
</div>
</div>
<div class="row">
<div class="row pad-row-search">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2">
<p class="text-add">Search</p>
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="findmessage" placeholder="Search keyword" name="findsoundbank"></div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control pad-search" type="text" id="findmessage" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnClear" type="button">Clear</button></div>
@@ -62,22 +62,22 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 input-add form-control" type="text" id="messageindex" placeholder="Index" readonly=""></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 input-add form-control" type="text" id="messageindex" placeholder="Index" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input type="text" id="messagedescription" class="input-add form-control" placeholder="Description"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="messagedescription" class="input-add form-control" placeholder="Description"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Language</p>
</div>
<div class="col"><select id="messagelanguage" class="input-add form-control">
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="messagelanguage" class="input-add form-control">
<option value="INDONESIA">Indonesia</option>
<option value="LOCAL">Local</option>
<option value="ENGLISH">English</option>
@@ -87,16 +87,16 @@
</select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">ANN ID</p>
</div>
<div class="col"><input type="number" id="messageannid" class="input-add form-control" min="1" max="100" value="1" step="1" placeholder="Announcement ID"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="number" id="messageannid" class="input-add form-control" min="1" max="100" value="1" step="1" placeholder="Announcement ID"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Voice Type</p>
</div>
<div class="col"><select id="messagevoicetype" class="input-add form-control">
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="messagevoicetype" class="input-add form-control">
<option value="VOICE_1">Voice 1</option>
<option value="VOICE_2">Voice 2</option>
<option value="VOICE_3">Voice 3</option>

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
@@ -62,46 +62,46 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 input-add form-control" type="text" id="modalindex" readonly=""></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 input-add form-control" type="text" id="modalindex" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input type="text" id="modaldescription" class="form-control input-add"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modaldescription" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">TAG</p>
</div>
<div class="col"><input type="text" id="modaltag" class="form-control input-add"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modaltag" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Category</p>
</div>
<div class="col"><select id="modalcategory" class="input-add form-select"></select></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="modalcategory" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Language</p>
</div>
<div class="col"><select id="modallanguage" class="input-add form-select"></select></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="modallanguage" class="input-add form-select"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Voice Type</p>
</div>
<div class="col"><select id="modalvoicetype" class="input-add form-select"></select></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="modalvoicetype" class="input-add form-select"></select></div>
</div>
<div class="row" style="height: 100px;">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Path</p>
</div>
<div class="col"><select class="w-100 js-example-basic-single" id="modalpath" name="modalpath"></select></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select class="w-100 js-example-basic-single input-add form-select" id="modalpath" name="modalpath"></select></div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="soundbankclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="soundbanksave" type="button">Save</button></div>

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
@@ -12,13 +12,13 @@
<body>
<div class="card" id="streamercard">
<div class="card-body">
<div class="card-body card-channel">
<h4 class="card-title" id="streamertitle">Channel 01</h4>
<div class="row">
<div class="col-8">
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
<h6 class="text-muted mb-2" id="streamerip">IP :&nbsp;192.168.10.10</h6>
</div>
<div class="col">
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
<h6 class="text-muted mb-2" id="streamerbuffer">Free : 64KB</h6>
</div>
</div>

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_03Sept25</title>
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
@@ -16,7 +16,7 @@
<h2 style="text-align: center;">Schedule Bank</h2>
</div>
</div>
<div class="row">
<div class="row pad-row-search">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<p class="text-add">Search</p>
@@ -63,110 +63,110 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col"><input class="w-25 input-add form-control" type="text" id="scheduleid" readonly=""></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 input-add form-control" type="text" id="scheduleid" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Description</p>
</div>
<div class="col"><input type="text" id="scheduledescription" class="input-add form-control"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Day</p>
</div>
<div class="col">
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="scheduleeveryday" name="dayselection" value="Everyday"><label class="form-check-label" for="formCheck-1">Everyday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulesunday" name="dayselection" value="Sunday"><label class="form-check-label" for="formCheck-8">Sunday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulemonday" name="dayselection" value="Monday"><label class="form-check-label" for="formCheck-7">Monday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="scheduletuesday" name="dayselection" value="Tuesday"><label class="form-check-label" for="formCheck-6">Tuesday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulewednesday" name="dayselection" value="Wednesday"><label class="form-check-label" for="formCheck-5">Wednesday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulethursday" name="dayselection" value="Thursday"><label class="form-check-label" for="formCheck-4">Thursday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulefriday" name="dayselection" value="Friday"><label class="form-check-label" for="formCheck-3">Friday</label></div>
</div>
</div>
<div class="row">
<div class="row pad-day">
<div class="col">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulesaturday" name="dayselection" value="Saturday"><label class="form-check-label" for="formCheck-2">Saturday</label></div>
</div>
</div>
<div class="row">
<div class="col">
<div class="col-7 col-sm-7 col-md-7 col-lg-6 col-xl-6 pad-day">
<div class="form-check"><input class="form-check-input" type="radio" id="schedulespecialdate" name="dayselection"><label class="form-check-label" for="formCheck-9">Special Date</label></div>
</div>
<div class="col"><input id="scheduledate" type="date"></div>
<div class="col-sm-5 col-md-5 col-lg-6 col-xl-6"><input id="scheduledate" class="form-control" type="date"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Time</p>
</div>
<div class="col">
<div class="row w-100 h-100">
<div class="col-3"><input class="w-100 h-100" type="number" id="schedulehour" value="0" min="0" max="23" step="1"></div>
<div class="col-1">
<p class="w-100 h-100">(H)</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3"><input type="number" id="schedulehour" class="input-add form-control class100" value="0" min="0" max="23" step="1"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-3 col-xl-3">
<p class="pad-time">(H)</p>
</div>
<div class="col-3"><input class="w-100 h-100" type="number" id="scheduleminute" value="0" min="0" max="59" step="1"></div>
<div class="col-1">
<p class="w-100 h-100">(M)</p>
<div class="col-4 col-sm-4 col-md-4 col-lg-3 col-xl-3"><input class="w-100 input-add form-control" type="number" id="scheduleminute" value="0" min="0" max="59" step="1"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-3 col-xl-3">
<p class="pad-time">(M)</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Sound Path</p>
</div>
<div class="col"><input type="text" id="schedulesoundpath" class="input-add form-control"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><select id="schedulesoundpath" class="input-add form-control"></select></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Repeat</p>
</div>
<div class="col"><input class="w-25 form-select input-add" type="number" id="schedulerepeat" min="0" max="5" step="1" value="0"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 form-select input-add" type="number" id="schedulerepeat" min="0" max="5" step="1" value="0"></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Enable</p>
</div>
<div class="col"><input type="checkbox" id="scheduleenable" class="form-check-input form-check text-add" checked=""></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="checkbox" id="scheduleenable" class="form-check-input form-check text-add" checked=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-3">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Broadcast Zones</p>
</div>
<div class="col border p-2" id="schedulezones"></div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8 border" id="schedulezones"></div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="scheduleclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic class25 color-add" id="schedulesave" type="button">Save</button></div>

View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>AAS_NewGen_30Sept25</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Login-Form-Basic-icons.css">
<link rel="stylesheet" href="assets/css/styles.css">
</head>
<body class="bg-body">
<div class="row">
<div class="col w-100 h-100 pad-header">
<h2 style="text-align: center;">User Management</h2>
</div>
</div>
<div class="row">
<div class="col-md-7 col-lg-7 col-xl-7"></div>
<div class="col-2 col-sm-2 col-md-2 col-lg-2 col-xl-2 search">
<p class="text-add">Search</p>
</div>
<div class="col-10 col-sm-10 col-md-3 col-lg-3 col-xl-3"><input class="w-100 form-control" type="text" id="finduser" placeholder="Search keyword" name="findsoundbank"></div>
</div>
<div class="row">
<div class="col">
<p id="tablesize" class="text-add" style="text-align: center;">Table Length : N/A</p>
</div>
</div>
<div class="row">
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnClear" type="button">Clear</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-add" id="btnAdd" type="button">Add</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-remove" id="btnRemove" type="button">Remove</button></div>
<div class="col-3 col-sm-3 col-md-3 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-edit" id="btnEdit" type="button">Edit</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnExport" type="button">Export</button></div>
<div class="col-6 col-sm-6 col-md-6 col-lg-2 col-xl-2"><button class="btn w-100 pad-button btn-round-basic color-import" id="btnImport" type="button">Import</button></div>
</div>
<div class="row">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="col-sm-1">No</th>
<th class="col-sm-1">Username</th>
<th class="col-sm-1">Location</th>
<th class="col">Soundbank</th>
<th class="col-sm-2">Messagebank</th>
<th class="col-sm-2">Broadcast Zones</th>
</tr>
</thead>
<tbody id="usertablebody"></tbody>
</table>
</div>
</div>
<div class="modal fade border-0" role="dialog" tabindex="-1" id="addmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header show">
<h4 class="modal-title align-content-center">Add / Edit User</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Index</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input class="w-25 input-add form-control" type="text" id="modalindex" readonly=""></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Username</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalusername" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Password</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="password" id="modalpassword" class="form-control input-add"></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Verify Password</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="password" id="modalverifypassword" class="input-add form-control"></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Sound Bank</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalsoundbank" class="input-add"><button class="btn btn-primary" id="btnShowSoundbankModal" type="button">Select</button></div>
</div>
<div class="row">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Message Bank</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalmessagebank" class="input-add"><button class="btn btn-primary" id="btnShowMessagebankModal" type="button">Select</button></div>
</div>
<div class="row" style="height: 100px;">
<div class="col-4 col-sm-4 col-md-4 col-lg-4 col-xl-4">
<p class="text-add">Broadcast Zones</p>
</div>
<div class="col-8 col-sm-8 col-md-8 col-lg-8 col-xl-8"><input type="text" id="modalbroadcastzones" class="input-add" name="modalpath"><button class="btn btn-primary" id="btnShowBroaadcastZoneModal" type="button">Select</button></div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-round-basic color-edit class25" id="usermanagementclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-round-basic color-add class25" id="usermanagementsave" type="button">Save</button></div>
</div>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="soundbankmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Sound Bank Selection</h4>
</div>
<div class="modal-body">
<p>(mon, ganti list checkbox, dengan id=soundbankselection</p>
</div>
<div class="modal-footer"><button class="btn btn-light" id="soundbankselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" id="soundbankselectionsave" type="button">Save</button></div>
</div>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="broadcastzonemodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Broadcast Zones Selection</h4>
</div>
<div class="modal-body">
<p>(mon, ganti list checkbox, dengan id=broadcastzoneselection</p>
</div>
<div class="modal-footer"><button class="btn btn-light" id="broadcastzoneselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" id="broadcastzoneselectionsave" type="button">Save</button></div>
</div>
</div>
</div>
<div class="modal fade" role="dialog" tabindex="-1" id="messagebankmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Message Bank Selection</h4>
</div>
<div class="modal-body">
<p>(mon, ganti list checkbox, dengan id=messagebankselection</p>
</div>
<div class="modal-footer"><button class="btn btn-light" id="messagebankselectionclose" type="button" data-bs-dismiss="modal">Close</button><button class="btn btn-primary" id="messagebankselectionsave" type="button">Save</button></div>
</div>
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/usermanagement.js"></script>
</body>
</html>

View File

@@ -1,4 +1,5 @@
import audio.AudioPlayer
import audio.UDPReceiver
import barix.BarixConnection
import barix.TCP_Barix_Command_Server
import com.sun.jna.Platform
@@ -20,7 +21,10 @@ import kotlin.concurrent.fixedRateTimer
lateinit var db: MariaDB
lateinit var audioPlayer: AudioPlayer
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
lateinit var udpreceiver: UDPReceiver
const val version = "0.0.2 (23/09/2025)"
// AAS 64 channels
const val max_channel = 64
// dipakai untuk pilih voice type, bisa diganti via web nanti
var selected_voice = VoiceType.VOICE_1.name
@@ -74,16 +78,22 @@ fun main() {
))
web.Start()
udpreceiver = UDPReceiver()
if (udpreceiver.Start()) {
Logger.info { "UDP Receiver started on port 5002" }
} else {
Logger.error { "Failed to start UDP Receiver on port 5002" }
}
val androidserver = TCP_Android_Command_Server()
androidserver.StartTcpServer(5003){
Logger.info { it }
db.logDB.Add(Log.NewLog("ANDROID", it))
}
val barixserver = TCP_Barix_Command_Server()
barixserver.StartTcpServer { cmd ->
Logger.info { cmd }
//Logger.info { cmd }
val _streamer = StreamerOutputs[cmd.ipaddress]
val _sc = db.soundchannelDB.List.find { it.ip == cmd.ipaddress }
if (_streamer == null) {
@@ -95,7 +105,8 @@ fun main() {
_bc.bufferRemain = cmd.buffremain
_bc.statusData = cmd.statusdata
StreamerOutputs[cmd.ipaddress] = _bc
}
Logger.info { "Created new Streamer Output for channel ${_sc.channel} with IP ${cmd.ipaddress}" }
} else Logger.warn { "soundChannelDB doesn't have soundchannel with IP ${cmd.ipaddress}" }
} else {
// sudah ada, update data
@@ -123,6 +134,7 @@ fun main() {
androidserver.StopTcpCommand()
onlinechecker.cancel()
web.Stop()
udpreceiver.Stop()
audioPlayer.Close()
db.close()
Logger.info { "All services stopped, exiting application." }

View File

@@ -7,15 +7,18 @@ import codes.Somecodes.Companion.SoundbankResult_directory
import codes.Somecodes.Companion.ValidFile
import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.dateformat1
import codes.Somecodes.Companion.datetimeformat1
import codes.Somecodes.Companion.timeformat2
import content.Category
import content.Language
import content.ScheduleDay
import database.Messagebank
import database.QueueTable
import database.Soundbank
import org.tinylog.Logger
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.util.function.Consumer
@@ -36,16 +39,24 @@ class MainExtension01 {
*/
fun AllBroadcastZonesValid(bz: List<String>): Boolean {
if (bz.isNotEmpty()) {
println("StreamerOutputs: $StreamerOutputs")
val validchannels = bz
// check apakah tiap zone ada di database broadcast zones
.filter { z1 ->
db.broadcastDB.List.find { z2 -> z2.SoundChannel == z1 } != null
println("Checking broadcast zone $z1")
val xx = db.broadcastDB.List.find { z2 -> z2.description == z1 }
println("Found in DB: $xx")
xx != null
}
// check apakah tiap zone ada di SoundChannelList dan Online
.filter { z3 ->
StreamerOutputs.any { sc -> sc.value.channel == z3 && sc.value.isOnline() }
println("Checking if zone $z3 is in StreamerOutputs and online")
val xx = StreamerOutputs.values.find { it.channel == z3 }
println("Is online: $xx")
xx!= null
}
println("Valid channels: $validchannels")
// kalau jumlah valid channel sama dengan jumlah broadcast zone, berarti semua valid
return validchannels.size == bz.size
}
@@ -481,6 +492,7 @@ class MainExtension01 {
fun Read_Queue_Paging(){
db.queuepagingDB.Get()
for (qp in db.queuepagingDB.List) {
println("Processing QueuePaging $qp")
if (qp.BroadcastZones.isNotBlank()) {
val zz = qp.BroadcastZones.split(";")
if (AllBroadcastZonesValid(zz)) {
@@ -612,10 +624,14 @@ class MainExtension01 {
fun Read_Queue_Table(){
db.queuetableDB.Get()
db.queuetableDB.List.forEach { qa ->
println("Processing QueueTable $qa")
if (qa.BroadcastZones.isNotEmpty()) {
val zz = qa.BroadcastZones.split(";")
println("Broadcast zones: $zz")
if (AllBroadcastZonesValid(zz)) {
println("All broadcast zones valid")
if (AllBroadcastZoneIdle(zz)) {
println("All broadcast zones idle")
if (qa.Type == "SOUNDBANK") {
val variables = Get_Soundbank_Data(qa.SB_TAGS)
val languages = qa.Language.split(";")
@@ -809,7 +825,19 @@ class MainExtension01 {
it.Day == ddmmyyyy
}
if (specialdate != null) {
// TODO Masukin ke queue table sebagai schedule special date
val qt = QueueTable(
0u,
LocalDateTime.now().format(datetimeformat1),
"AAS",
"TIMER",
specialdate.Description,
specialdate.Soundpath,
specialdate.BroadcastZones,
1.toUInt(),
specialdate.Language
)
db.queuetableDB.Add(qt)
return
}
// cek weekly schedule
@@ -825,7 +853,19 @@ class MainExtension01 {
}
}
if (weekly != null) {
// TODO Masukin ke queue table sebagai schedule weekly
val qt = QueueTable(
0u,
LocalDateTime.now().format(datetimeformat1),
"AAS",
"TIMER",
weekly.Description,
weekly.Soundpath,
weekly.BroadcastZones,
1.toUInt(),
weekly.Language
)
db.queuetableDB.Add(qt)
return
}
// check daily schedule
@@ -833,7 +873,19 @@ class MainExtension01 {
it.Day == ScheduleDay.Everyday.name
}
if (daily != null) {
// TODO Masukin ke queue table sebagai schedule daily
val qt = QueueTable(
0u,
LocalDateTime.now().format(datetimeformat1),
"AAS",
"TIMER",
daily.Description,
daily.Soundpath,
daily.BroadcastZones,
1.toUInt(),
daily.Language
)
db.queuetableDB.Add(qt)
return
}
}

View File

@@ -126,6 +126,14 @@ class AudioPlayer (var samplingrate: Int) {
return result
}
fun WavWriter(data: ByteArray, target: String, callback: BiConsumer<Boolean, String>) {
val source = AudioFileInfo()
source.bytes = data
source.fileName = "In-Memory Data"
val sources = listOf(source)
WavWriter(sources, target, callback)
}
/**
* Writes the audio data from the sources to a WAV file.
* @param sources List of AudioFileInfo objects containing the audio data to write.

79
src/audio/UDPReceiver.kt Normal file
View File

@@ -0,0 +1,79 @@
package audio
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.DatagramSocket
import java.util.function.Consumer
@Suppress("unused")
/**
* UDPReceiver is a class that listens for UDP packets on a specified port.
* It is designed to run in a separate thread and can be stopped when no longer needed.
* @param portnumber The port to listen for incoming UDP packets (default is 5002
*/
class UDPReceiver(val portnumber: Int = 5002) {
private lateinit var socket : DatagramSocket
private var isRunning = false
private val dataCallback = mutableMapOf<String, Consumer<ByteArray>>()
/**
* Start listening for UDP packets on the specified port.
* @return true if successful, false otherwise
*/
fun Start() : Boolean{
return try {
socket = DatagramSocket(portnumber)
isRunning = true
CoroutineScope(Dispatchers.IO).launch {
while(isRunning){
try {
val buffer = ByteArray(2048)
val packet = java.net.DatagramPacket(buffer, buffer.size)
socket.receive(packet)
val data = ByteArray(packet.length)
System.arraycopy(packet.data, 0, data, 0, packet.length)
dataCallback[packet.address.hostAddress].let {
it?.accept(data)
}
} catch (e: Exception) {
if (isRunning) {
println("Error receiving UDP packet: ${e.message}")
}
}
}
}
true
} catch (e: Exception) {
false
}
}
/**
* Register a callback function to be called when data is received from the specified IP address.
* @param ipaddress The IP address to listen for incoming UDP packets.
* @param callback A callback function that will be called when data is received from the specified IP address.
*/
fun RequestDataFrom(ipaddress: String, callback: Consumer<ByteArray>){
dataCallback[ipaddress] = callback
}
/**
* Unregister the callback function for the specified IP address.
* @param ipaddress The IP address to stop listening for incoming UDP packets.
*/
fun StopRequestDataFrom(ipaddress: String){
dataCallback.remove(ipaddress)
}
/**
* Stop listening for UDP packets and close the socket.
*/
fun Stop(){
if (isRunning){
isRunning = false
socket.close()
}
}
}

View File

@@ -16,6 +16,7 @@ import java.net.InetSocketAddress
import java.util.function.BiConsumer
import java.util.function.Consumer
@Deprecated("Sepertinya gak jadi pake")
@Suppress("unused")
/**
* UDPReceiverToFile is a class that listens for UDP packets on a specified address and port

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Deprecated("Sepertinya gak jadi pake ini")
@Suppress("unused")
class UDPSenderFromFile(val fileName: String, val bytesPerPackage: Int=1024, targetIP: Array<String>, targetPort: Int ) {
val bass: Bass = Bass.Instance

View File

@@ -9,11 +9,11 @@ import java.util.function.Consumer
@Suppress("unused")
class TCP_Barix_Command_Server {
private var tcpserver: ServerSocket? = null
private var job: Job? = null
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
private val regex = """\$\\"STATUSBARIX;(\d+);(\d+);?(\d)?\\"$"""
private val regex = """STATUSBARIX;(\d+);(\d+);?(\d)?"""
private val pattern = Regex(regex)
/**
@@ -27,51 +27,44 @@ class TCP_Barix_Command_Server {
val tcp = ServerSocket(port)
tcpserver = tcp
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP server started" }
Logger.info { "TCP StreamerOutput server started on port $port" }
while (isActive) {
if (tcpserver?.isClosed == true) break
if (tcpserver.isClosed) break
try {
tcpserver?.accept().use { socket ->
{
CoroutineScope(Dispatchers.Main).launch {
if (socket != null) {
val key : String = socket.inetAddress.hostAddress+":"+socket.port
socketMap[key] = socket
Logger.info { "Start communicating with $key" }
socket.getInputStream().use { din ->
{
while (isActive) {
if (din.available()>0){
val bb = ByteArray(din.available())
din.read(bb)
// B4A format, 4 bytes di depan adalah size
val str = String(bb)
if (ValidString(str)) {
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
pattern.find(str)?.let { matchResult ->
val (vu, buffremain, statusdata) = matchResult.destructured
val status = BarixStatus(
socket.inetAddress.hostAddress,
vu.toInt(),
buffremain.toInt(),
statusdata.toIntOrNull() ?: 0
)
Logger.info { "Received valid command from $key : $status" }
cb.accept(status)
} ?: run {
Logger.warn { "Invalid command format from $key : $str" }
}
}
}
}
}
}
Logger.info { "Finished communicating with $key" }
socketMap.remove(key)
}
val socket = tcpserver.accept()
CoroutineScope(Dispatchers.IO).launch {
val key : String = socket.inetAddress.hostAddress
socketMap[key] = socket
Logger.info { "Start communicating with Streamer Output with IP : $key" }
val din = socket.getInputStream()
while (isActive) {
if (din.available()>0){
val bb = ByteArray(din.available())
din.read(bb)
// B4A format, 4 bytes di depan adalah size
val str = String(bb, 4, bb.size - 4)
if (ValidString(str)) {
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
pattern.find(str)?.let { matchResult ->
val (vu, buffremain, statusdata) = matchResult.destructured
val status = BarixStatus(
socket.inetAddress.hostAddress,
vu.toInt(),
buffremain.toInt(),
statusdata.toIntOrNull() ?: 0
)
//Logger.info { "Received valid command from $key : $status" }
cb.accept(status)
} ?: run {
Logger.warn { "Invalid command format from $key : $str" }
}
}
}
}
Logger.info { "Finished communicating with Streamer Output with IP $key" }
socketMap.remove(key)
}
} catch (ex: Exception) {
@@ -94,20 +87,18 @@ class TCP_Barix_Command_Server {
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver?.close()
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job?.join()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
} finally {
tcpserver = null
}
return false
}

View File

@@ -0,0 +1,45 @@
package commandServer
import codes.Somecodes.Companion.PagingResult_directory
import codes.Somecodes.Companion.filenameformat
import org.tinylog.Logger
import java.io.ByteArrayOutputStream
import java.nio.file.Path
import java.time.LocalDateTime
/**
* Class to handle a paging job, storing incoming audio data and metadata.
* @param fromIP The IP address from which the paging data is received.
* @param broadcastzones The zones to which the paging is broadcasted, is a semicolon-separated string.
*/
class PagingJob(val fromIP: String, val broadcastzones: String) {
val filePath : Path = PagingResult_directory.resolve(LocalDateTime.now().format(filenameformat)+"_RAW.wav")
private val bos : ByteArrayOutputStream = ByteArrayOutputStream()
var totalBytesReceived = 0; private set
var isRunning = true; private set
/**
* Adds incoming audio data to the job.
* @param data The byte array containing audio data.
* @param length The number of bytes to write from the data array.
*/
fun addData(data: ByteArray, length: Int) {
Logger.info{"PagingJob from $fromIP, zones: $broadcastzones, received $length bytes"}
bos.write(data, 0, length)
totalBytesReceived += length
}
/**
* Retrieves the accumulated audio data as a byte array.
* @return A byte array containing all received audio data.
*/
fun GetData(): ByteArray {
return bos.toByteArray()
}
fun Close(){
bos.close()
isRunning = false
}
}

View File

@@ -1,6 +1,13 @@
package commandServer
import audioPlayer
import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.datetimeformat1
import content.Language
import database.Messagebank
import database.QueuePaging
import database.QueueTable
import database.Soundbank
import db
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -9,19 +16,23 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tinylog.Logger
import udpreceiver
import java.net.ServerSocket
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.LocalDateTime
import java.util.function.Consumer
import kotlin.io.path.absolutePathString
@Suppress("unused")
class TCP_Android_Command_Server {
private var tcpserver: ServerSocket? = null
private var job: Job? = null
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
lateinit var logcb: Consumer<String>
private val listUserLogin = mutableListOf<userLogin>()
private val listOnGoingPaging = mutableMapOf<String, PagingJob>()
/**
* Start TCP Command Server
@@ -35,15 +46,16 @@ class TCP_Android_Command_Server {
val tcp = ServerSocket(port)
tcpserver = tcp
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP server started" }
Logger.info { "TCP Android server started on port $port" }
while (isActive) {
if (tcpserver?.isClosed == true) break
if (tcpserver.isClosed) break
try {
tcpserver?.accept().use { socket ->
tcpserver.accept().use { socket ->
{
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
if (socket != null) {
val key: String = socket.inetAddress.hostAddress + ":" + socket.port
// key is IP address only
val key: String = socket.inetAddress.hostAddress
socketMap[key] = socket
Logger.info { "Start communicating with $key" }
socket.getInputStream().let { din ->
@@ -108,23 +120,31 @@ class TCP_Android_Command_Server {
return ByteArray(0)
}
/**
* Process command from Android client
* @param key The client IP address
* @param cmd The command string
* @param cb Callback to send reply string
*/
private fun process_command(key: String, cmd: String, cb: Consumer<String>) {
Logger.info { "Command from $key : $cmd" }
val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }.map { it.uppercase() }
when (parts[0]) {
"GETLOGIN" -> {
// Android login request
val username = parts.getOrElse(1) { "" }
val password = parts.getOrElse(2) { "" }
if (ValidString(username) && ValidString(password)) {
if (db.userDB.List.any{it.username==username && it.password==password}) {
cb.accept("LOGIN;TRUE@")
val existing = listUserLogin.find { it.ip == key}
if (existing!=null){
existing.username = username
} else{
listUserLogin.add(userLogin(key, username))
}
cb.accept("LOGIN;TRUE@")
logcb.accept("Android Login success from $key as $username")
return
} else {
logcb.accept("Android Login failed from $key as $username")
cb.accept("LOGIN;FALSE@")
@@ -135,32 +155,252 @@ class TCP_Android_Command_Server {
}
}
"PCMFILE_START" -> {
// TODO read coding here
"PCMFILE_START","STARTPAGINGAND" -> {
val zones = parts.getOrElse(3) { "" }.replace(",",";")
if (ValidString(zones)){
// create pagingjob
val pj = PagingJob(key, zones)
// masukin ke list
listOnGoingPaging[key] = pj
// start minta data dari udpreceiver
udpreceiver.RequestDataFrom(key){
// push data ke paging job
pj.addData(it, it.size)
}
logcb.accept("Paging started from Android $key")
cb.accept(parts[0]+";OK@")
return
} else logcb.accept("Paging start from Android $key failed, empty zones")
cb.accept(parts[0]+";NG@")
}
"PCMFILE_STOP" -> {
// TODO read coding here
}
"PCMFILE_STOP","STOPPAGINGAND" -> {
val pj = listOnGoingPaging[key]
if (pj!=null){
listOnGoingPaging.remove(key)
udpreceiver.StopRequestDataFrom(key)
logcb.accept("Paging stopped from Android $key")
cb.accept(parts[0]+";OK@")
// get remaining data
val data = pj.GetData()
pj.Close()
audioPlayer.WavWriter(data, pj.filePath.absolutePathString()){
success, message ->
if (success){
// insert to paging queue
val qp = QueuePaging(
0u,
LocalDateTime.now().format(datetimeformat1),
"PAGING",
"NORMAL",
pj.filePath.absolutePathString(),
pj.broadcastzones
)
if (db.queuepagingDB.Add(qp)){
logcb.accept("Paging audio inserted to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}")
cb.accept(parts[0]+";OK@")
} else {
logcb.accept("Failed to insert paging audio to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}")
cb.accept(parts[0]+";NG@")
}
"STARTPAGINGAND" -> {
// TODO read coding here
}
} else {
logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : $message")
cb.accept(parts[0]+";NG@")
}
}
} else {
logcb.accept("Paging stop from Android $key failed, no ongoing paging")
cb.accept(parts[0]+";NG@")
}
"STOPPAGINGAND" -> {
// TODO read coding here
}
"CANCELPAGINGAND" -> {
// TODO read coding here
val pj = listOnGoingPaging[key]
if (pj!=null){
pj.Close()
listOnGoingPaging.remove(key)
udpreceiver.StopRequestDataFrom(key)
logcb.accept("Paging from Android $key cancelled")
cb.accept("CANCELPAGINGAND;OK@")
return
} else logcb.accept("Paging cancel from Android $key failed, no ongoing paging")
cb.accept("CANCELPAGINGAND;NG@")
}
"STARTINITIALIZE" -> {
// TODO read coding here
// pengiriman variabel ke Android
val username = parts.getOrElse(1) { "" }
if (ValidString(username)){
val userlogin = listUserLogin.find { it.username == username }
if (userlogin != null){
val userdb = db.userDB.List.find { it.username == username }
if (userdb != null){
val result = StringBuilder()
result.append("ZONE")
userdb.broadcastzones.split(";").map { it.trim() }.filter { it.isNotBlank() }.forEach {
result.append(";")
result.append(it)
}
result.append("@")
val VARMESSAGES = mutableListOf<Messagebank>()
userdb.messagebank_ann_id
// messagebank_ann_id adalah rentengan ANN_ID (digit) yang dipisah dengan ;
.split(";")
// trim dulu
.map { it.trim() }
// bukan string kosong antar dua tanda ;
.filter { it.isNotBlank() }
// beneran digit semua
.filter { xx -> xx.all{it.isDigit()} }
// iterasi setiap ANN_ID
.forEach { annid ->
// masukin ke VARMESSAGES yang unik secara ANN_ID dan Language
val xx = db.messageDB.List
.filter{ it.ANN_ID == annid.toUInt() }
.distinctBy { it.Language }
VARMESSAGES.addAll(xx)
}
result.append("MSGTOTAL;").append(VARMESSAGES.size).append("@")
// VAR AP TOTAL
val VARAPTOTAL = mutableListOf<Soundbank>()
val sb_split = userdb.soundbank_tags.split(";").map { it.trim() }.filter { it.isNotBlank() }
sb_split.forEach {
val sb = db.Find_Soundbank_AirplaneName(it).firstOrNull()
if (sb != null) VARAPTOTAL.add(sb)
}
result.append("VARAPTOTAL;").append(VARAPTOTAL.size).append("@")
// VAR CITY TOTAL
val VARCITYTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_City(it).firstOrNull()
if (sb != null) VARCITYTOTAL.add(sb)
}
result.append("VARCITYTOTAL;").append(VARCITYTOTAL.size).append("@")
// VAR PLACES TOTAL
val VARPLACESTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Places(it).firstOrNull()
if (sb != null) VARPLACESTOTAL.add(sb)
}
result.append("VARPLACESTOTAL;").append(VARPLACESTOTAL.size).append("@")
// VAR SHALAT TOTAL
val VARSHALATTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Shalat(it).firstOrNull()
if (sb != null) VARSHALATTOTAL.add(sb)
}
result.append("VARSHALATTOTAL;").append(VARSHALATTOTAL.size).append("@")
// VAR SEQUENCE TOTAL
val VARSEQUENCETOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Sequence(it).firstOrNull()
if (sb != null) VARSEQUENCETOTAL.add(sb)
}
// VAR REASON TOTAL
val VARREASONTOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Reason(it).firstOrNull()
if (sb != null) VARREASONTOTAL.add(sb)
}
result.append("VARREASONTOTAL;").append(VARREASONTOTAL.size).append("@")
// VAR PROCEDURE TOTAL
val VARPROCEDURETOTAL = mutableListOf<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Procedure(it).firstOrNull()
if (sb != null) VARPROCEDURETOTAL.add(sb)
}
result.append("VARPROCEDURETOTAL;").append(VARPROCEDURETOTAL.size).append("@")
// send to sender
cb.accept(result.toString())
result.clear()
//Append MSG, for Android only Indonesia and English
VARMESSAGES.groupBy { it.ANN_ID }.forEach { (ann_id, value) ->
result.append("MSG;").append(ann_id)
result.append(";")
value.find { it.Language== Language.INDONESIA.name }?.let {result.append(it.Message_Detail)} ?: result.append("NA")
result.append(";")
value.find {it.Language== Language.ENGLISH.name }?.let {result.append(it.Message_Detail)} ?: result.append("NA")
result.append("@")
}
// append VARAP
VARAPTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARAP;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARCITY
VARCITYTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARCITY;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARPLACES
VARPLACESTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARPLACES;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARSHALAT
VARSHALATTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARSHALAT;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARSEQUENCE
VARSEQUENCETOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARSEQUENCE;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARREASON
VARREASONTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARREASON;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// append VARPROCEDURE
VARPROCEDURETOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
result.append("VARPROCEDURE;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@")
}
// send to sender
cb.accept(result.toString())
logcb.accept("All variables sent to $key with username $username")
return
} else logcb.accept("STARTINITIALIZE failed from $key with username $username not found in userDB")
} else logcb.accept("STARTINITIALIZE failed from $key with unregistered username $username")
} else logcb.accept("STARTINITIALIZE failed from $key with empty username")
cb.accept("STARTINITIALIZE;FALSE@")
}
"BROADCASTAND" -> {
// TODO read coding here
// semi auto dari android, masukin ke queue table
val desc = parts.getOrElse(1) { "" }
val lang = parts.getOrElse(2) { "" }.replace(",",";")
val tags = parts.getOrElse(3) { "" }.replace(",",";")
val zone = parts.getOrElse(4) { "" }.replace(",",";")
if (ValidString(desc)){
if (ValidString(lang)){
if (ValidString(tags)){
if (ValidString(zone)){
val qt = QueueTable(
0u,
LocalDateTime.now().format(datetimeformat1),
"ANDROID",
"SOUNDBANK",
desc,
tags,
zone,
1u,
lang
)
if (db.queuetableDB.Add(qt)){
logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} inserted. Message: $desc;$lang;$tags;$zone")
cb.accept("BROADCASTAND;OK@")
return
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, cannot add to queue table")
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty zone")
} else logcb.accept("Broadcsast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty tags")
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty language")
} else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty description")
cb.accept("NG@")
}
else -> {
@@ -176,20 +416,18 @@ class TCP_Android_Command_Server {
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver?.close()
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job?.join()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
} finally {
tcpserver = null
}
return false
}

View File

@@ -14,8 +14,8 @@ import java.util.function.Consumer
@Suppress("unused")
class TCP_PC_Command_Server {
private var tcpserver: ServerSocket? = null
private var job: Job? = null
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
/**
@@ -31,11 +31,11 @@ class TCP_PC_Command_Server {
job = CoroutineScope(Dispatchers.IO).launch {
Logger.info { "TCP server started" }
while (isActive) {
if (tcpserver?.isClosed == true) break
if (tcpserver.isClosed) break
try {
tcpserver?.accept().use { socket ->
tcpserver.accept().use { socket ->
{
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
if (socket != null) {
val key : String = socket.inetAddress.hostAddress+":"+socket.port
socketMap[key] = socket
@@ -81,20 +81,18 @@ class TCP_PC_Command_Server {
*/
fun StopTcpCommand(): Boolean {
try {
tcpserver?.close()
tcpserver.close()
runBlocking {
socketMap.values.forEach {
it.close()
}
socketMap.clear()
job?.join()
job.join()
}
Logger.info { "StopTcpCommand success" }
return true
} catch (e: Exception) {
Logger.error { "Failed to StopTcpServer, Message : ${e.message}" }
} finally {
tcpserver = null
}
return false
}

View File

@@ -13,5 +13,6 @@ enum class Category(name: String) {
Year("Year"),
Birthday("Birthday"),
Reason("Reason"),
Sequence("Sequence"),
Procedure("Procedure");
}

View File

@@ -20,4 +20,8 @@ data class Log(
return Log(0u, date, time, machine, description)
}
}
override fun toString() : String {
return "$datenya $timenya [$machine] $description"
}
}

View File

@@ -2,14 +2,17 @@ package database
import codes.Somecodes.Companion.ValiDateForLogHtml
import codes.Somecodes.Companion.toJsonString
import content.Category
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import max_channel
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.tinylog.Logger
import java.sql.Connection
import java.sql.DriverManager
import java.util.function.Consumer
import kotlin.math.max
/**
* A class to manage a connection to a MariaDB database.
@@ -1463,7 +1466,7 @@ class MariaDB(
val countResult = statement?.executeQuery("SELECT COUNT(*) AS count FROM ${super.dbName}")
if (countResult?.next() == true) {
val count = countResult.getInt("count")
if (count == 0) {
if (count < max_channel) {
Logger.info("SoundChannel table is empty, populating with default channels" as Any)
Clear()
}
@@ -1577,9 +1580,9 @@ class MariaDB(
statement?.executeUpdate("TRUNCATE TABLE ${super.dbName}")
Logger.info("${super.dbName} table cleared" as Any)
List.clear()
// create new rows from 1 to 64 with description "Channel 01" to "Channel 64" and empty ip
for (i in 1..64) {
val channel = String.format("Channel %02d", i)
// create new rows from 1 to 64 with description "Channel 1" to "Channel 64" and empty ip
for (i in 1..max_channel) {
val channel = String.format("Channel %d", i)
val insertStatement =
connection.prepareStatement("INSERT INTO ${super.dbName} (channel, ip) VALUES (?, ?)")
insertStatement?.setString(1, channel)
@@ -1727,13 +1730,13 @@ class MariaDB(
statement?.setString(4, data.description)
val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Log added: [$data.datenya $data.timenya] [$data.machine] $data.description" as Any)
Logger.info{"Log added : $data"}
return true
} else {
Logger.warn("No log entry added for: [$data.datenya $data.timenya] [$data.machine] $data.description" as Any)
Logger.warn{"Failed to add log entry : $data"}
}
} catch (e: Exception) {
Logger.error("Error adding log entry: ${e.message}" as Any)
Logger.error{"Error adding log entry: ${e.message}"}
}
return false
}
@@ -2239,6 +2242,63 @@ class MariaDB(
return null
}
/**
* Find all city soundbank by tag
* @param tag The tags to search for
* @return a list of Soundbank with Category City and matching tag
*/
fun Find_Soundbank_City(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List
.filter{ it.Category== Category.City.name }
.filter { it.TAG.lowercase()==lowerTag}
}
fun Find_Soundbank_AirplaneName(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List
.filter{ it.Category== Category.Airplane_Name.name }
.filter { it.TAG.lowercase()==lowerTag}
}
fun Find_Soundbank_Places(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List
.filter{ it.Category== Category.Places.name }
.filter { it.TAG.lowercase()==lowerTag}
}
fun Find_Soundbank_Shalat(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List
.filter{ it.Category== Category.Shalat.name }
.filter { it.TAG.lowercase()==lowerTag}
}
fun Find_Soundbank_Sequence(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List
.filter{ it.Category== Category.Sequence.name }
.filter { it.TAG.lowercase()==lowerTag}
}
fun Find_Soundbank_Reason(tag: String) : List<Soundbank>{
val lowerTag = tag.lowercase()
return soundDB.List
.filter{ it.Category== Category.Reason.name }
.filter { it.TAG.lowercase()==lowerTag}
}
fun Find_Soundbank_Procedure(tag: String) : List<Soundbank> {
val lowerTag = tag.lowercase()
return soundDB.List
.filter { it.Category == Category.Procedure.name }
.filter { it.TAG.lowercase() == lowerTag }
}

View File

@@ -1,4 +1,7 @@
package database
@Suppress("unused")
data class QueuePaging(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var BroadcastZones: String)
data class QueuePaging(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var BroadcastZones: String){
override fun toString(): String {
return "QueuePaging(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', BroadcastZones='$BroadcastZones')"
}
}

View File

@@ -1,4 +1,8 @@
package database
@Suppress("unused")
data class QueueTable(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var SB_TAGS: String, var BroadcastZones: String, var Repeat: UInt, var Language: String)
data class QueueTable(var index: UInt, var Date_Time: String, var Source: String, var Type: String, var Message: String, var SB_TAGS: String, var BroadcastZones: String, var Repeat: UInt, var Language: String){
override fun toString(): String {
return "QueueTable(index=$index, Date_Time='$Date_Time', Source='$Source', Type='$Type', Message='$Message', SB_TAGS='$SB_TAGS', BroadcastZones='$BroadcastZones', Repeat=$Repeat, Language='$Language')"
}
}

View File

@@ -16,7 +16,6 @@ import content.Language
import content.ScheduleDay
import content.VoiceType
import database.BroadcastZones
import database.BroadcastZonesHtml
import database.LanguageLink
import database.MariaDB
import database.Messagebank
@@ -652,6 +651,9 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
it.status(500).result(objectmapper.writeValueAsString(resultMessage("Failed to truncate schedulebank table")))
}
}
post("Add"){
// TODO add new schedule
}
delete("DeleteByIndex/{index}") {
// delete by index
val index = it.pathParam("index").toUIntOrNull()
@@ -831,17 +833,17 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
path("BroadcastZones"){
get("List") {
// TODO : temporary, convert BroadcastZones to BroadcastZonesHtml, karena harus revisi javascript di Bootstrap Studio
val newlist: ArrayList<BroadcastZonesHtml> = db.broadcastDB.List.map { xx ->
BroadcastZonesHtml(
xx.index,
xx.description,
xx.SoundChannel,
xx.id,
xx.bp
)
} as ArrayList<BroadcastZonesHtml>
// val newlist: ArrayList<BroadcastZonesHtml> = db.broadcastDB.List.map { xx ->
// BroadcastZonesHtml(
// xx.index,
// xx.description,
// xx.SoundChannel,
// xx.id,
// xx.bp
// )
// } as ArrayList<BroadcastZonesHtml>
it.result(MariaDB.ArrayListtoString(newlist))
it.result(MariaDB.ArrayListtoString(db.broadcastDB.List))
}
delete("List"){
// truncate broadcast zones table