302 lines
10 KiB
Text
302 lines
10 KiB
Text
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Payfrit Admin - Beacon ↔ ServicePoint</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||
select, input, button { padding: 8px; margin: 6px 0; width: 520px; max-width: 100%; }
|
||
button { width: auto; cursor: pointer; }
|
||
.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: 160px; }
|
||
table { border-collapse: collapse; width: 100%; margin-top: 12px; }
|
||
th, td { border: 1px solid #ddd; padding: 8px; }
|
||
th { background: #f5f5f5; text-align: left; }
|
||
.ok { color: #060; font-weight: bold; }
|
||
.warn { color: #b00; font-weight: bold; }
|
||
.mini { font-size: 12px; color: #555; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h2>Beacon ↔ ServicePoint Relationships</h2>
|
||
<div class="ok" id="jsStatus">(JS not loaded yet)</div>
|
||
<div class="warn" id="counts"></div>
|
||
|
||
<div class="row">
|
||
<div class="card">
|
||
<h3>Create / Update Relationship</h3>
|
||
<div>
|
||
<label>Beacon</label><br>
|
||
<select id="BeaconSelect"></select>
|
||
</div>
|
||
<div>
|
||
<label>ServicePoint</label><br>
|
||
<select id="ServicePointSelect"></select>
|
||
</div>
|
||
<div>
|
||
<label>Notes</label><br>
|
||
<input id="Notes" placeholder="Optional">
|
||
</div>
|
||
<button type="button" onclick="saveRel()">Link</button>
|
||
<button type="button" onclick="refreshAll()">Refresh</button>
|
||
|
||
<h3>Last Action Response</h3>
|
||
<div class="mini">This shows the raw response from save/delete so it can’t be overwritten by refreshAll.</div>
|
||
<pre id="respAction"></pre>
|
||
|
||
<h3>Current Data Snapshot</h3>
|
||
<div class="mini">This shows the latest Beacons/ServicePoints/Assignments bundle.</div>
|
||
<pre id="respSnapshot"></pre>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Delete Relationship</h3>
|
||
<div>
|
||
<label>Relationship ID (lt_Beacon_Businesses_ServicePointID)</label><br>
|
||
<input id="RelID" placeholder="Click a row below to fill this">
|
||
</div>
|
||
<button type="button" onclick="deleteRel()">Delete</button>
|
||
<div style="margin-top:8px;" class="mini">
|
||
Tip: Delete first to re-assign a Beacon (since assigned beacons are hidden from the dropdown).
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3>Existing Relationships (click row to copy ID)</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Beacon</th>
|
||
<th>ServicePoint</th>
|
||
<th>Notes</th>
|
||
<th>CreatedAt</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="rows"></tbody>
|
||
</table>
|
||
|
||
<script>
|
||
(function boot(){
|
||
document.getElementById("jsStatus").textContent = "JS loaded OK";
|
||
})();
|
||
|
||
function showAction(o){ document.getElementById("respAction").textContent = JSON.stringify(o, null, 2); }
|
||
function showSnapshot(o){ document.getElementById("respSnapshot").textContent = JSON.stringify(o, null, 2); }
|
||
|
||
const API_BASE = "/biz.payfrit.com/api";
|
||
|
||
// 1:1 MODE (for now)
|
||
const HIDE_ASSIGNED_BEACONS = true;
|
||
const HIDE_ASSIGNED_SERVICEPOINTS = true;
|
||
|
||
async function api(path, bodyObj) {
|
||
const fullPath = API_BASE + path;
|
||
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 {
|
||
return { OK:false, ERROR:"non_json", RAW:txt, _httpStatus: res.status, _path: fullPath };
|
||
}
|
||
} catch (e) {
|
||
return { OK:false, ERROR:"network_error", MESSAGE:String(e), _path: fullPath };
|
||
}
|
||
}
|
||
|
||
function escapeHtml(s){
|
||
return (""+s)
|
||
.replaceAll("&","&")
|
||
.replaceAll("<","<")
|
||
.replaceAll(">",">")
|
||
.replaceAll('"',""")
|
||
.replaceAll("'","'");
|
||
}
|
||
|
||
function setSelectPlaceholder(sel, label){
|
||
sel.innerHTML = "";
|
||
const opt = document.createElement("option");
|
||
opt.value = "";
|
||
opt.textContent = label;
|
||
sel.appendChild(opt);
|
||
}
|
||
|
||
function buildAssignedSets(assignOut){
|
||
const assignedBeaconIDs = new Set();
|
||
const assignedServicePointIDs = new Set();
|
||
|
||
(assignOut.ASSIGNMENTS || []).forEach(a => {
|
||
if (a && a.BeaconID != null) assignedBeaconIDs.add(String(a.BeaconID));
|
||
if (a && a.ServicePointID != null) assignedServicePointIDs.add(String(a.ServicePointID));
|
||
});
|
||
|
||
return { assignedBeaconIDs, assignedServicePointIDs };
|
||
}
|
||
|
||
function getSelVal(id){
|
||
const el = document.getElementById(id);
|
||
return el ? String(el.value || "") : "";
|
||
}
|
||
|
||
function setSelValIfExists(id, value){
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
|
||
const opt = Array.from(el.options).find(o => String(o.value) === String(value));
|
||
if (opt) el.value = String(value);
|
||
}
|
||
|
||
async function refreshAssignments(){
|
||
const out = await api("/assignments/list.cfm", {});
|
||
const tbody = document.getElementById("rows");
|
||
tbody.innerHTML = "";
|
||
|
||
(out.ASSIGNMENTS || []).forEach(a => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td>${a.lt_Beacon_Businesses_ServicePointID}</td>
|
||
<td>${escapeHtml((a.BeaconName || "") + " (ID " + a.BeaconID + ")")}</td>
|
||
<td>${escapeHtml((a.ServicePointName || "") + " (ID " + a.ServicePointID + ")")}</td>
|
||
<td>${escapeHtml(a.lt_Beacon_Businesses_ServicePointNotes || "")}</td>
|
||
<td>${escapeHtml(a.CreatedAt || "")}</td>
|
||
`;
|
||
tr.style.cursor = "pointer";
|
||
tr.onclick = () => document.getElementById("RelID").value = a.lt_Beacon_Businesses_ServicePointID;
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
return out;
|
||
}
|
||
|
||
async function refreshBeacons(assignedBeaconIDs, keepSelectedBeaconID){
|
||
const out = await api("/beacons/list.cfm", {});
|
||
const sel = document.getElementById("BeaconSelect");
|
||
setSelectPlaceholder(sel, "-- Select Beacon --");
|
||
|
||
(out.BEACONS || []).forEach(b => {
|
||
const isAssigned = assignedBeaconIDs.has(String(b.BeaconID));
|
||
if (HIDE_ASSIGNED_BEACONS && isAssigned) return;
|
||
|
||
const opt = document.createElement("option");
|
||
opt.value = b.BeaconID;
|
||
opt.textContent = String(b.BeaconID) + " - " + (b.BeaconName || "");
|
||
sel.appendChild(opt);
|
||
});
|
||
|
||
if (keepSelectedBeaconID) setSelValIfExists("BeaconSelect", keepSelectedBeaconID);
|
||
|
||
return out;
|
||
}
|
||
|
||
async function refreshServicePoints(assignedServicePointIDs, keepSelectedServicePointID){
|
||
const out = await api("/servicepoints/list.cfm", {});
|
||
const sel = document.getElementById("ServicePointSelect");
|
||
setSelectPlaceholder(sel, "-- Select ServicePoint --");
|
||
|
||
(out.SERVICEPOINTS || []).forEach(sp => {
|
||
const isAssigned = assignedServicePointIDs.has(String(sp.ServicePointID));
|
||
if (HIDE_ASSIGNED_SERVICEPOINTS && isAssigned) return;
|
||
|
||
const opt = document.createElement("option");
|
||
opt.value = sp.ServicePointID;
|
||
opt.textContent = String(sp.ServicePointID) + " - " + (sp.ServicePointName || "");
|
||
sel.appendChild(opt);
|
||
});
|
||
|
||
if (keepSelectedServicePointID) setSelValIfExists("ServicePointSelect", keepSelectedServicePointID);
|
||
|
||
return out;
|
||
}
|
||
|
||
async function refreshAll(){
|
||
// preserve selection during refresh
|
||
const keepBeacon = getSelVal("BeaconSelect");
|
||
const keepSP = getSelVal("ServicePointSelect");
|
||
|
||
// assignments first (for filtering)
|
||
const assignOut = await refreshAssignments();
|
||
const sets = buildAssignedSets(assignOut);
|
||
|
||
const beaconOut = await refreshBeacons(sets.assignedBeaconIDs, keepBeacon);
|
||
const spOut = await refreshServicePoints(sets.assignedServicePointIDs, keepSP);
|
||
|
||
const beaconCount = (beaconOut.BEACONS || []).length;
|
||
const spCount = (spOut.SERVICEPOINTS || []).length;
|
||
const assignCount = (assignOut.ASSIGNMENTS || []).length;
|
||
|
||
document.getElementById("counts").textContent =
|
||
"Beacons: " + beaconCount + " | ServicePoints: " + spCount + " | Assignments: " + assignCount;
|
||
|
||
showSnapshot({
|
||
AssignedBeaconIDs: Array.from(sets.assignedBeaconIDs),
|
||
AssignedServicePointIDs: Array.from(sets.assignedServicePointIDs),
|
||
BeaconsResponse: beaconOut,
|
||
ServicePointsResponse: spOut,
|
||
AssignmentsResponse: assignOut
|
||
});
|
||
}
|
||
|
||
async function saveRel(){
|
||
const rawBeacon = getSelVal("BeaconSelect");
|
||
const rawSP = getSelVal("ServicePointSelect");
|
||
|
||
if (!rawBeacon || !rawSP){
|
||
showAction({
|
||
OK:false,
|
||
ERROR: (!rawBeacon ? "missing_BeaconID" : "missing_ServicePointID"),
|
||
BeaconSelectValue: rawBeacon,
|
||
ServicePointSelectValue: rawSP
|
||
});
|
||
return;
|
||
}
|
||
|
||
const BeaconID = parseInt(rawBeacon, 10);
|
||
const ServicePointID = parseInt(rawSP, 10);
|
||
const Notes = (document.getElementById("Notes").value || "").trim();
|
||
|
||
if (!BeaconID){
|
||
showAction({ OK:false, ERROR:"missing_BeaconID", BeaconSelectValue: rawBeacon });
|
||
return;
|
||
}
|
||
if (!ServicePointID){
|
||
showAction({ OK:false, ERROR:"missing_ServicePointID", ServicePointSelectValue: rawSP });
|
||
return;
|
||
}
|
||
|
||
// IMPORTANT: show the save response and DO NOT overwrite it with refresh output
|
||
const out = await api("/assignments/save.cfm", { BeaconID, ServicePointID, Notes });
|
||
showAction(out);
|
||
|
||
// refresh snapshot + table silently (snapshot still updates, but action box stays)
|
||
await refreshAll();
|
||
}
|
||
|
||
async function deleteRel(){
|
||
const idStr = (document.getElementById("RelID").value || "").trim();
|
||
const id = parseInt(idStr, 10);
|
||
if (!id){
|
||
showAction({OK:false,ERROR:"missing_lt_Beacon_Businesses_ServicePointID"});
|
||
return;
|
||
}
|
||
|
||
const out = await api("/assignments/delete.cfm", { lt_Beacon_Businesses_ServicePointID: id });
|
||
showAction(out);
|
||
await refreshAll();
|
||
}
|
||
|
||
// initial load
|
||
showAction({ OK:true, MESSAGE:"Ready. Select Beacon + ServicePoint then click Link." });
|
||
refreshAll();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
<a href="/pads.payfrit.com" target="_blank">pads.payfrit.com</a> | <a href="/payfr.it" target="_blank">payfr.it</a> | <a href="/work.payfrit.com" target="_blank">work.payfr.it</a> | <a href="https://help.payfrit.com" target="_blank">support ticket</a><br>Copyright 2025 <a href="mailto:admin@payfrit.com">Payfrit</a>
|