- Add uploadHeader.cfm API for 1200px header images - Add saveBrandColor.cfm API for hex color storage - Add Branding section to menu builder sidebar - Fix header upload path and permissions - Various beacon and service point API improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
231 lines
6.4 KiB
Text
231 lines
6.4 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
|
|
<cfcontent type="text/html; charset=utf-8" reset="true">
|
|
|
|
<cfoutput>
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Payfrit Admin - Beacons</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
input, button, select { padding: 8px; margin: 6px 0; width: 420px; max-width: 100%; }
|
|
button { width: auto; cursor: pointer; }
|
|
table { border-collapse: collapse; width: 100%; margin-top: 12px; }
|
|
th, td { border: 1px solid ##ddd; padding: 8px; }
|
|
th { background: ##f5f5f5; text-align: left; }
|
|
.row { display: flex; gap: 24px; flex-wrap: wrap; }
|
|
.card { border: 1px solid ##ddd; padding: 14px; border-radius: 10px; flex: 1; min-width: 320px; }
|
|
pre { background: ##111; color: ##0f0; padding: 10px; overflow: auto; border-radius: 8px; min-height: 140px; }
|
|
.warn { color: ##b00; font-weight: bold; }
|
|
.ok { color: ##060; font-weight: bold; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>Beacons</h2>
|
|
<div class="warn">Required: BeaconName</div>
|
|
<div class="ok" id="jsStatus">(JS not loaded yet)</div>
|
|
|
|
<div class="row">
|
|
<div class="card">
|
|
<h3>Create / Update</h3>
|
|
|
|
<div>
|
|
<label>BeaconID (blank = create)</label><br>
|
|
<input id="BeaconID" placeholder="e.g. 123">
|
|
</div>
|
|
|
|
<div>
|
|
<label>BeaconName (required)</label><br>
|
|
<input id="BeaconName" placeholder="Front Door" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label>UUID (iBeacon-style)</label><br>
|
|
<input id="UUID" placeholder="E2C56DB5-DFFB-48D2-B060-D0F5A71096E0">
|
|
</div>
|
|
|
|
<div>
|
|
<label>NamespaceId (Eddystone-UID first 20 hex chars)</label><br>
|
|
<input id="NamespaceId" placeholder="0A1B2C3D4E5F60718293">
|
|
</div>
|
|
|
|
<div>
|
|
<label>InstanceId (Eddystone-UID last 12 hex chars)</label><br>
|
|
<input id="InstanceId" placeholder="A1B2C3D4E5F6">
|
|
</div>
|
|
|
|
<div>
|
|
<label>IsActive</label><br>
|
|
<select id="IsActive">
|
|
<option value="1" selected>1 (Active)</option>
|
|
<option value="0">0 (Inactive)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button type="button" onclick="saveBeacon()">Save</button>
|
|
<button type="button" onclick="refresh()">Refresh List</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Delete (soft)</h3>
|
|
<div>
|
|
<label>BeaconID</label><br>
|
|
<input id="DelBeaconID" placeholder="e.g. 123">
|
|
</div>
|
|
<button type="button" onclick="deleteBeacon()">Delete</button>
|
|
|
|
<h3>Response</h3>
|
|
<pre id="resp"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Beacon List (click row to load)</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>BeaconID</th>
|
|
<th>Name</th>
|
|
<th>UUID</th>
|
|
<th>NamespaceId</th>
|
|
<th>InstanceId</th>
|
|
<th>IsActive</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rows"></tbody>
|
|
</table>
|
|
|
|
<script>
|
|
(function boot(){
|
|
document.getElementById("jsStatus").textContent = "JS loaded OK - BusinessID: #request.BusinessID#";
|
|
})();
|
|
|
|
const BUSINESS_ID = #val(request.BusinessID)#;
|
|
|
|
function show(o){
|
|
document.getElementById("resp").textContent = JSON.stringify(o, null, 2);
|
|
}
|
|
|
|
const API_BASE = "/biz.payfrit.com/api";
|
|
|
|
async function api(path, bodyObj) {
|
|
const fullPath = API_BASE + path;
|
|
bodyObj = bodyObj || {};
|
|
bodyObj.BusinessID = BUSINESS_ID;
|
|
|
|
try {
|
|
const res = await fetch(fullPath, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(bodyObj)
|
|
});
|
|
|
|
const txt = await res.text();
|
|
|
|
try {
|
|
const parsed = JSON.parse(txt);
|
|
parsed._httpStatus = res.status;
|
|
parsed._path = fullPath;
|
|
return parsed;
|
|
} catch (e) {
|
|
return {
|
|
OK:false,
|
|
ERROR:"non_json",
|
|
_httpStatus: res.status,
|
|
_path: fullPath,
|
|
RAW: txt
|
|
};
|
|
}
|
|
} catch (e) {
|
|
return { OK:false, ERROR:"network_error", MESSAGE:String(e), _path: fullPath };
|
|
}
|
|
}
|
|
|
|
function valIntOrNull(id){
|
|
const v = (document.getElementById(id).value||"").trim();
|
|
if (!v) return null;
|
|
const n = parseInt(v, 10);
|
|
return isNaN(n) ? null : n;
|
|
}
|
|
|
|
function escapeHtml(s){
|
|
return (""+s)
|
|
.replaceAll("&","&")
|
|
.replaceAll("<","<")
|
|
.replaceAll(">",">")
|
|
.replaceAll('"',""")
|
|
.replaceAll("'","&##039;");
|
|
}
|
|
|
|
function loadIntoForm(b){
|
|
document.getElementById("BeaconID").value = b.BeaconID || "";
|
|
document.getElementById("BeaconName").value = b.BeaconName || "";
|
|
document.getElementById("UUID").value = b.UUID || "";
|
|
document.getElementById("NamespaceId").value = b.NamespaceId || "";
|
|
document.getElementById("InstanceId").value = b.InstanceId || "";
|
|
document.getElementById("IsActive").value = ("" + (b.IsActive ?? 1));
|
|
document.getElementById("DelBeaconID").value = b.BeaconID || "";
|
|
}
|
|
|
|
async function refresh() {
|
|
const out = await api("/beacons/list.cfm", {});
|
|
show(out);
|
|
|
|
const tbody = document.getElementById("rows");
|
|
tbody.innerHTML = "";
|
|
|
|
const items = out.BEACONS || [];
|
|
for (const b of items) {
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td>${b.BeaconID}</td>
|
|
<td>${escapeHtml(b.BeaconName||"")}</td>
|
|
<td>${escapeHtml(b.UUID||"")}</td>
|
|
<td>${escapeHtml(b.NamespaceId||"")}</td>
|
|
<td>${escapeHtml(b.InstanceId||"")}</td>
|
|
<td>${b.IsActive}</td>
|
|
`;
|
|
tr.style.cursor = "pointer";
|
|
tr.onclick = () => loadIntoForm(b);
|
|
tbody.appendChild(tr);
|
|
}
|
|
}
|
|
|
|
async function saveBeacon() {
|
|
const name = (document.getElementById("BeaconName").value || "").trim();
|
|
if (!name) {
|
|
show({ OK:false, ERROR:"missing_beacon_name", MESSAGE:"BeaconName is required" });
|
|
return;
|
|
}
|
|
|
|
const body = {
|
|
BeaconID: valIntOrNull("BeaconID"),
|
|
BeaconName: name,
|
|
UUID: (document.getElementById("UUID").value || "").trim(),
|
|
NamespaceId: (document.getElementById("NamespaceId").value || "").trim(),
|
|
InstanceId: (document.getElementById("InstanceId").value || "").trim(),
|
|
IsActive: parseInt(document.getElementById("IsActive").value, 10)
|
|
};
|
|
if (!body.BeaconID) delete body.BeaconID;
|
|
|
|
const out = await api("/beacons/save.cfm", body);
|
|
show(out);
|
|
await refresh();
|
|
}
|
|
|
|
async function deleteBeacon() {
|
|
const id = valIntOrNull("DelBeaconID");
|
|
if (!id) { show({OK:false,ERROR:"missing_beacon_id"}); return; }
|
|
|
|
const out = await api("/beacons/delete.cfm", { BeaconID: id });
|
|
show(out);
|
|
await refresh();
|
|
}
|
|
|
|
refresh();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
</cfoutput>
|