payfrit-works/admin/beacon_servicepoint.cfm

302 lines
10 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 cant 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("&","&amp;")
.replaceAll("<","&lt;")
.replaceAll(">","&gt;")
.replaceAll('"',"&quot;")
.replaceAll("'","&#039;");
}
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>&nbsp;|&nbsp;<a href="/payfr.it" target="_blank">payfr.it</a>&nbsp;|&nbsp;<a href="/work.payfrit.com" target="_blank">work.payfr.it</a>&nbsp;|&nbsp;<a href="https://help.payfrit.com" target="_blank">support ticket</a><br>Copyright 2025 <a href="mailto:admin@payfrit.com">Payfrit</a>