Add branding features: header upload and brand color picker
- 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>
This commit is contained in:
parent
d73c4d60d3
commit
d4e0ae1162
41 changed files with 10012 additions and 738 deletions
|
|
@ -99,29 +99,27 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function boot(){
|
(function boot(){
|
||||||
document.getElementById("jsStatus").textContent = "JS loaded OK";
|
document.getElementById("jsStatus").textContent = "JS loaded OK - BusinessID: #request.BusinessID#";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const BUSINESS_ID = #val(request.BusinessID)#;
|
||||||
|
|
||||||
function show(o){
|
function show(o){
|
||||||
document.getElementById("resp").textContent = JSON.stringify(o, null, 2);
|
document.getElementById("resp").textContent = JSON.stringify(o, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
IMPORTANT:
|
|
||||||
Your Lucee error shows /api/... is resolving to C:\lucee\tomcat\webapps\ROOT\api\...
|
|
||||||
So we must call the API under the biz.payfrit.com context explicitly.
|
|
||||||
*/
|
|
||||||
const API_BASE = "/biz.payfrit.com/api";
|
const API_BASE = "/biz.payfrit.com/api";
|
||||||
|
|
||||||
async function api(path, bodyObj) {
|
async function api(path, bodyObj) {
|
||||||
const fullPath = API_BASE + path;
|
const fullPath = API_BASE + path;
|
||||||
show({ INFO:"API CALL", PATH: fullPath, BODY: bodyObj || {} });
|
bodyObj = bodyObj || {};
|
||||||
|
bodyObj.BusinessID = BUSINESS_ID;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(fullPath, {
|
const res = await fetch(fullPath, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(bodyObj || {})
|
body: JSON.stringify(bodyObj)
|
||||||
});
|
});
|
||||||
|
|
||||||
const txt = await res.text();
|
const txt = await res.text();
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function boot(){
|
(function boot(){
|
||||||
document.getElementById("jsStatus").textContent = "JS loaded OK";
|
document.getElementById("jsStatus").textContent = "JS loaded OK - BusinessID: #request.BusinessID#";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const BUSINESS_ID = #val(request.BusinessID)#;
|
||||||
|
|
||||||
function show(o){
|
function show(o){
|
||||||
document.getElementById("resp").textContent = JSON.stringify(o, null, 2);
|
document.getElementById("resp").textContent = JSON.stringify(o, null, 2);
|
||||||
}
|
}
|
||||||
|
|
@ -115,11 +117,14 @@ const API_BASE = "/biz.payfrit.com/api";
|
||||||
|
|
||||||
async function api(path, bodyObj) {
|
async function api(path, bodyObj) {
|
||||||
const fullPath = API_BASE + path;
|
const fullPath = API_BASE + path;
|
||||||
|
bodyObj = bodyObj || {};
|
||||||
|
bodyObj.BusinessID = BUSINESS_ID;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(fullPath, {
|
const res = await fetch(fullPath, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(bodyObj || {})
|
body: JSON.stringify(bodyObj)
|
||||||
});
|
});
|
||||||
const txt = await res.text();
|
const txt = await res.text();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,21 @@ if (len(request._api_path)) {
|
||||||
|
|
||||||
if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/businesses/get.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/get.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/businesses/update.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/businesses/updateHours.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/servicepoints/get.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/servicepoints/save.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/servicepoints/delete.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/beacons/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/beacons/get.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/beacons/save.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/beacons/delete.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/assignments/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/assignments/save.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/assignments/delete.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/clearAllData.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/clearAllData.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/clearOrders.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/clearOrders.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -151,6 +163,8 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/menu/getForBuilder.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/getForBuilder.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/updateStations.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/updateStations.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/menu/uploadHeader.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/businesses/saveBrandColor.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/create.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/create.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
// Debug endpoints
|
// Debug endpoints
|
||||||
|
|
|
||||||
86
api/admin/cleanupBeacons.cfm
Normal file
86
api/admin/cleanupBeacons.cfm
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Cleanup Lazy Daisy Beacons
|
||||||
|
* - Removes duplicate beacons created by setupBeaconTables
|
||||||
|
* - Updates original beacons with proper names
|
||||||
|
*/
|
||||||
|
response = { "OK": false, "steps": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
lazyDaisyID = 37;
|
||||||
|
|
||||||
|
// Delete duplicate assignments for beacons 7, 8, 9
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM lt_Beacon_Businesses_ServicePoints
|
||||||
|
WHERE BeaconID IN (7, 8, 9) AND BusinessID = :bizId
|
||||||
|
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted duplicate assignments for beacons 7, 8, 9");
|
||||||
|
|
||||||
|
// Delete duplicate beacons 7, 8, 9
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Beacons
|
||||||
|
WHERE BeaconID IN (7, 8, 9) AND BeaconBusinessID = :bizId
|
||||||
|
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted duplicate beacons 7, 8, 9");
|
||||||
|
|
||||||
|
// Update original beacons with names based on their service point assignments
|
||||||
|
// Beacon 4 -> Table 1 (ServicePointID 4)
|
||||||
|
// Beacon 5 -> Table 2 (ServicePointID 5)
|
||||||
|
// Beacon 6 -> Table 3 (ServicePointID 6)
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Beacons SET BeaconName = 'Beacon - Table 1'
|
||||||
|
WHERE BeaconID = 4 AND BeaconBusinessID = :bizId
|
||||||
|
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Updated Beacon 4 name to 'Beacon - Table 1'");
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Beacons SET BeaconName = 'Beacon - Table 2'
|
||||||
|
WHERE BeaconID = 5 AND BeaconBusinessID = :bizId
|
||||||
|
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Updated Beacon 5 name to 'Beacon - Table 2'");
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Beacons SET BeaconName = 'Beacon - Table 3'
|
||||||
|
WHERE BeaconID = 6 AND BeaconBusinessID = :bizId
|
||||||
|
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Updated Beacon 6 name to 'Beacon - Table 3'");
|
||||||
|
|
||||||
|
// Get final status
|
||||||
|
qFinal = queryExecute("
|
||||||
|
SELECT lt.BeaconID, b.BeaconUUID, b.BeaconName, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
|
||||||
|
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||||
|
JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
||||||
|
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||||
|
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||||
|
WHERE lt.BusinessID = :bizId
|
||||||
|
ORDER BY lt.BeaconID
|
||||||
|
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
beacons = [];
|
||||||
|
for (i = 1; i <= qFinal.recordCount; i++) {
|
||||||
|
arrayAppend(beacons, {
|
||||||
|
"BeaconID": qFinal.BeaconID[i],
|
||||||
|
"BeaconName": qFinal.BeaconName[i],
|
||||||
|
"UUID": qFinal.BeaconUUID[i],
|
||||||
|
"BusinessName": qFinal.BusinessName[i],
|
||||||
|
"ServicePointName": qFinal.ServicePointName[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.beacons = beacons;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.error = e.message;
|
||||||
|
if (len(e.detail)) {
|
||||||
|
response.detail = e.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
127
api/admin/cleanupForLazyDaisy.cfm
Normal file
127
api/admin/cleanupForLazyDaisy.cfm
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false, "steps": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Keep only Lazy Daisy (BusinessID 37)
|
||||||
|
keepBusinessID = 37;
|
||||||
|
|
||||||
|
// First, reassign all beacons to Lazy Daisy
|
||||||
|
queryExecute("
|
||||||
|
UPDATE lt_Beacon_Businesses_ServicePoints
|
||||||
|
SET BusinessID = :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Reassigned all beacons to Lazy Daisy");
|
||||||
|
|
||||||
|
// Get list of businesses to delete
|
||||||
|
qBiz = queryExecute("
|
||||||
|
SELECT BusinessID, BusinessName FROM Businesses WHERE BusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
deletedBusinesses = [];
|
||||||
|
for (i = 1; i <= qBiz.recordCount; i++) {
|
||||||
|
arrayAppend(deletedBusinesses, qBiz.BusinessName[i]);
|
||||||
|
}
|
||||||
|
response.steps.append("Found " & qBiz.recordCount & " businesses to delete");
|
||||||
|
|
||||||
|
// Delete related data first (foreign key constraints)
|
||||||
|
// Delete ItemTemplateLinks for items from other businesses
|
||||||
|
queryExecute("
|
||||||
|
DELETE itl FROM ItemTemplateLinks itl
|
||||||
|
JOIN Items i ON i.ItemID = itl.ItemID
|
||||||
|
WHERE i.ItemBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted ItemTemplateLinks for other businesses");
|
||||||
|
|
||||||
|
// Delete Items for other businesses
|
||||||
|
qItems = queryExecute("
|
||||||
|
SELECT COUNT(*) as cnt FROM Items WHERE ItemBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Items WHERE ItemBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted " & qItems.cnt & " items from other businesses");
|
||||||
|
|
||||||
|
// Delete Categories for other businesses
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Categories WHERE CategoryBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted categories from other businesses");
|
||||||
|
|
||||||
|
// Delete Hours for other businesses
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Hours WHERE HoursBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted hours from other businesses");
|
||||||
|
|
||||||
|
// Delete Employees for other businesses (skip if table doesn't exist)
|
||||||
|
try {
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Employees WHERE EmployeeBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted employees from other businesses");
|
||||||
|
} catch (any e) {
|
||||||
|
response.steps.append("Skipped employees (table may not exist)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete ServicePoints for other businesses (skip if table doesn't exist)
|
||||||
|
try {
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM ServicePoints WHERE ServicePointBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted service points from other businesses");
|
||||||
|
} catch (any e) {
|
||||||
|
response.steps.append("Skipped service points (table may not exist)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Stations for other businesses (skip if table doesn't exist)
|
||||||
|
try {
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Stations WHERE StationBusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted stations from other businesses");
|
||||||
|
} catch (any e) {
|
||||||
|
response.steps.append("Skipped stations (table may not exist)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally delete the businesses themselves
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Businesses WHERE BusinessID != :keepID
|
||||||
|
", { keepID: keepBusinessID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Deleted " & arrayLen(deletedBusinesses) & " businesses");
|
||||||
|
|
||||||
|
// Get beacon status
|
||||||
|
qBeacons = queryExecute("
|
||||||
|
SELECT lt.BeaconID, b.BeaconUUID, lt.BusinessID, biz.BusinessName, lt.ServicePointID
|
||||||
|
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||||
|
JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
||||||
|
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
beacons = [];
|
||||||
|
for (i = 1; i <= qBeacons.recordCount; i++) {
|
||||||
|
arrayAppend(beacons, {
|
||||||
|
"BeaconID": qBeacons.BeaconID[i],
|
||||||
|
"UUID": qBeacons.BeaconUUID[i],
|
||||||
|
"BusinessID": qBeacons.BusinessID[i],
|
||||||
|
"BusinessName": qBeacons.BusinessName[i],
|
||||||
|
"ServicePointID": qBeacons.ServicePointID[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.deletedBusinesses = deletedBusinesses;
|
||||||
|
response.beacons = beacons;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.error = e.message;
|
||||||
|
if (len(e.detail)) {
|
||||||
|
response.detail = e.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
49
api/admin/clearLocalCoffee.cfm
Normal file
49
api/admin/clearLocalCoffee.cfm
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false, "steps": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Business IDs to delete (Lo/Cal Coffee and SANTA MONICA entries)
|
||||||
|
businessIDs = [38, 39, 40, 41, 42];
|
||||||
|
|
||||||
|
for (bizID in businessIDs) {
|
||||||
|
// Delete ItemTemplateLinks for items belonging to this business
|
||||||
|
queryExecute("
|
||||||
|
DELETE itl FROM ItemTemplateLinks itl
|
||||||
|
INNER JOIN Items i ON i.ItemID = itl.ItemID
|
||||||
|
WHERE i.ItemBusinessID = :bizID
|
||||||
|
", { bizID: bizID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete Items
|
||||||
|
queryExecute("DELETE FROM Items WHERE ItemBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete Categories
|
||||||
|
queryExecute("DELETE FROM Categories WHERE CategoryBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete Hours
|
||||||
|
queryExecute("DELETE FROM Hours WHERE HoursBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete Addresses linked to this business
|
||||||
|
queryExecute("DELETE FROM Addresses WHERE AddressBusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete the Business itself
|
||||||
|
queryExecute("DELETE FROM Businesses WHERE BusinessID = :bizID", { bizID: bizID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response.steps.append("Deleted business " & bizID & " and all related data");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.message = "Cleared all Lo/Cal Coffee and SANTA MONICA businesses";
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.error = e.message;
|
||||||
|
if (len(e.detail)) {
|
||||||
|
response.detail = e.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
104
api/admin/createBeacons.cfm
Normal file
104
api/admin/createBeacons.cfm
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false, "steps": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
lazyDaisyID = 37;
|
||||||
|
|
||||||
|
// The three beacon UUIDs we need
|
||||||
|
beaconUUIDs = [
|
||||||
|
"626C7565636861726D31000000000001",
|
||||||
|
"1B6295D54F744C58A2D8CD83CA26BDF4",
|
||||||
|
"7777772E6B6B6D636E2E636F6D000001"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create beacons
|
||||||
|
for (i = 1; i <= arrayLen(beaconUUIDs); i++) {
|
||||||
|
uuid = beaconUUIDs[i];
|
||||||
|
|
||||||
|
// Check if beacon exists
|
||||||
|
qB = queryExecute("SELECT BeaconID FROM Beacons WHERE BeaconUUID = :uuid", { uuid: uuid }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qB.recordCount == 0) {
|
||||||
|
queryExecute("INSERT INTO Beacons (BeaconUUID, BeaconBusinessID) VALUES (:uuid, :bizID)", { uuid: uuid, bizID: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
qNew = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
|
beaconID = qNew.id;
|
||||||
|
response.steps.append("Created beacon " & beaconID & " with UUID: " & uuid);
|
||||||
|
} else {
|
||||||
|
beaconID = qB.BeaconID;
|
||||||
|
response.steps.append("Beacon exists: " & beaconID & " with UUID: " & uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get service point Table 1
|
||||||
|
qSP = queryExecute("
|
||||||
|
SELECT ServicePointID FROM ServicePoints
|
||||||
|
WHERE ServicePointBusinessID = :bizID AND ServicePointName = 'Table 1'
|
||||||
|
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qSP.recordCount == 0) {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO ServicePoints (ServicePointBusinessID, ServicePointName, ServicePointTypeID)
|
||||||
|
VALUES (:bizID, 'Table 1', 1)
|
||||||
|
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
qSP = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
|
servicePointID = qSP.id;
|
||||||
|
response.steps.append("Created service point 'Table 1' (ID: " & servicePointID & ")");
|
||||||
|
} else {
|
||||||
|
servicePointID = qSP.ServicePointID;
|
||||||
|
response.steps.append("Found service point 'Table 1' (ID: " & servicePointID & ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all beacons and map them
|
||||||
|
qBeacons = queryExecute("SELECT BeaconID, BeaconUUID FROM Beacons", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
for (i = 1; i <= qBeacons.recordCount; i++) {
|
||||||
|
beaconID = qBeacons.BeaconID[i];
|
||||||
|
|
||||||
|
// Delete old mapping if exists
|
||||||
|
queryExecute("DELETE FROM lt_Beacon_Businesses_ServicePoints WHERE BeaconID = :beaconID", { beaconID: beaconID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Create new mapping
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO lt_Beacon_Businesses_ServicePoints (BeaconID, BusinessID, ServicePointID)
|
||||||
|
VALUES (:beaconID, :bizID, :spID)
|
||||||
|
", { beaconID: beaconID, bizID: lazyDaisyID, spID: servicePointID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Mapped beacon " & beaconID & " to Lazy Daisy, Table 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get final status
|
||||||
|
qFinal = queryExecute("
|
||||||
|
SELECT lt.BeaconID, b.BeaconUUID, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
|
||||||
|
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||||
|
JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
||||||
|
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||||
|
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
beacons = [];
|
||||||
|
for (i = 1; i <= qFinal.recordCount; i++) {
|
||||||
|
arrayAppend(beacons, {
|
||||||
|
"BeaconID": qFinal.BeaconID[i],
|
||||||
|
"UUID": qFinal.BeaconUUID[i],
|
||||||
|
"BusinessID": qFinal.BusinessID[i],
|
||||||
|
"BusinessName": qFinal.BusinessName[i],
|
||||||
|
"ServicePointID": qFinal.ServicePointID[i],
|
||||||
|
"ServicePointName": qFinal.ServicePointName[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.beacons = beacons;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.error = e.message;
|
||||||
|
if (len(e.detail)) {
|
||||||
|
response.detail = e.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
20
api/admin/setHeaderExtension.cfm
Normal file
20
api/admin/setHeaderExtension.cfm
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Businesses SET BusinessHeaderImageExtension = 'jpg' WHERE BusinessID = 37
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.message = "Set BusinessHeaderImageExtension to 'jpg' for business 37";
|
||||||
|
} catch (any e) {
|
||||||
|
response.error = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
104
api/admin/setupBeaconTables.cfm
Normal file
104
api/admin/setupBeaconTables.cfm
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Setup Lazy Daisy Beacons
|
||||||
|
* Creates a beacon for each service point and links them
|
||||||
|
*/
|
||||||
|
response = { "OK": false, "steps": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
lazyDaisyID = 37;
|
||||||
|
|
||||||
|
// Get all service points for Lazy Daisy
|
||||||
|
qServicePoints = queryExecute("
|
||||||
|
SELECT ServicePointID, ServicePointName
|
||||||
|
FROM ServicePoints
|
||||||
|
WHERE ServicePointBusinessID = :bizID AND ServicePointIsActive = 1
|
||||||
|
ORDER BY ServicePointID
|
||||||
|
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response.steps.append("Found " & qServicePoints.recordCount & " service points for Lazy Daisy");
|
||||||
|
|
||||||
|
// Create a beacon for each service point
|
||||||
|
beaconsCreated = 0;
|
||||||
|
for (sp in qServicePoints) {
|
||||||
|
beaconName = "Beacon - " & sp.ServicePointName;
|
||||||
|
|
||||||
|
// Check if beacon already exists for this business with this name
|
||||||
|
qExisting = queryExecute("
|
||||||
|
SELECT BeaconID FROM Beacons
|
||||||
|
WHERE BeaconBusinessID = :bizId AND BeaconName = :name
|
||||||
|
", { bizId: lazyDaisyID, name: beaconName }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qExisting.recordCount == 0) {
|
||||||
|
// Generate a unique UUID for this beacon (32 hex chars, no dashes)
|
||||||
|
beaconUUID = "PAYFRIT00037" & numberFormat(sp.ServicePointID, "0000000000000000000");
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Beacons (BeaconBusinessID, BeaconName, BeaconUUID, BeaconIsActive)
|
||||||
|
VALUES (:bizId, :name, :uuid, 1)
|
||||||
|
", {
|
||||||
|
bizId: lazyDaisyID,
|
||||||
|
name: beaconName,
|
||||||
|
uuid: beaconUUID
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
qNewBeacon = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
|
newBeaconId = qNewBeacon.id;
|
||||||
|
|
||||||
|
// Create assignment to service point
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO lt_Beacon_Businesses_ServicePoints
|
||||||
|
(BeaconID, BusinessID, ServicePointID, lt_Beacon_Businesses_ServicePointAssignedByUserID)
|
||||||
|
VALUES (:beaconId, :bizId, :spId, 1)
|
||||||
|
", {
|
||||||
|
beaconId: newBeaconId,
|
||||||
|
bizId: lazyDaisyID,
|
||||||
|
spId: sp.ServicePointID
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response.steps.append("Created beacon '" & beaconName & "' (ID: " & newBeaconId & ") -> " & sp.ServicePointName);
|
||||||
|
beaconsCreated++;
|
||||||
|
} else {
|
||||||
|
response.steps.append("Beacon '" & beaconName & "' already exists, skipping");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get final status
|
||||||
|
qFinal = queryExecute("
|
||||||
|
SELECT lt.BeaconID, b.BeaconUUID, b.BeaconName, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
|
||||||
|
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||||
|
JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
||||||
|
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||||
|
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||||
|
WHERE lt.BusinessID = :bizId
|
||||||
|
ORDER BY sp.ServicePointName
|
||||||
|
", { bizId: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
beacons = [];
|
||||||
|
for (i = 1; i <= qFinal.recordCount; i++) {
|
||||||
|
arrayAppend(beacons, {
|
||||||
|
"BeaconID": qFinal.BeaconID[i],
|
||||||
|
"BeaconName": qFinal.BeaconName[i],
|
||||||
|
"UUID": qFinal.BeaconUUID[i],
|
||||||
|
"BusinessName": qFinal.BusinessName[i],
|
||||||
|
"ServicePointName": qFinal.ServicePointName[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.beaconsCreated = beaconsCreated;
|
||||||
|
response.beacons = beacons;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.error = e.message;
|
||||||
|
if (len(e.detail)) {
|
||||||
|
response.detail = e.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
91
api/admin/setupLazyDaisyBeacons.cfm
Normal file
91
api/admin/setupLazyDaisyBeacons.cfm
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false, "steps": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
lazyDaisyID = 37;
|
||||||
|
|
||||||
|
// Get all beacons
|
||||||
|
qBeacons = queryExecute("SELECT BeaconID, BeaconUUID FROM Beacons", {}, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Found " & qBeacons.recordCount & " beacons");
|
||||||
|
|
||||||
|
// Create service point for Table 1 if it doesn't exist
|
||||||
|
qSP = queryExecute("
|
||||||
|
SELECT ServicePointID FROM ServicePoints
|
||||||
|
WHERE ServicePointBusinessID = :bizID AND ServicePointName = 'Table 1'
|
||||||
|
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qSP.recordCount == 0) {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO ServicePoints (ServicePointBusinessID, ServicePointName, ServicePointTypeID)
|
||||||
|
VALUES (:bizID, 'Table 1', 1)
|
||||||
|
", { bizID: lazyDaisyID }, { datasource: "payfrit" });
|
||||||
|
qSP = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
|
servicePointID = qSP.id;
|
||||||
|
response.steps.append("Created service point 'Table 1' (ID: " & servicePointID & ")");
|
||||||
|
} else {
|
||||||
|
servicePointID = qSP.ServicePointID;
|
||||||
|
response.steps.append("Found existing service point 'Table 1' (ID: " & servicePointID & ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map all beacons to Lazy Daisy with Table 1
|
||||||
|
for (i = 1; i <= qBeacons.recordCount; i++) {
|
||||||
|
beaconID = qBeacons.BeaconID[i];
|
||||||
|
|
||||||
|
// Check if mapping exists
|
||||||
|
qMap = queryExecute("
|
||||||
|
SELECT * FROM lt_Beacon_Businesses_ServicePoints WHERE BeaconID = :beaconID
|
||||||
|
", { beaconID: beaconID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qMap.recordCount == 0) {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO lt_Beacon_Businesses_ServicePoints (BeaconID, BusinessID, ServicePointID)
|
||||||
|
VALUES (:beaconID, :bizID, :spID)
|
||||||
|
", { beaconID: beaconID, bizID: lazyDaisyID, spID: servicePointID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Created mapping for beacon " & beaconID);
|
||||||
|
} else {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE lt_Beacon_Businesses_ServicePoints
|
||||||
|
SET BusinessID = :bizID, ServicePointID = :spID
|
||||||
|
WHERE BeaconID = :beaconID
|
||||||
|
", { beaconID: beaconID, bizID: lazyDaisyID, spID: servicePointID }, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Updated mapping for beacon " & beaconID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get final status
|
||||||
|
qFinal = queryExecute("
|
||||||
|
SELECT lt.BeaconID, b.BeaconUUID, lt.BusinessID, biz.BusinessName, lt.ServicePointID, sp.ServicePointName
|
||||||
|
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||||
|
JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
||||||
|
JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||||
|
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
beacons = [];
|
||||||
|
for (i = 1; i <= qFinal.recordCount; i++) {
|
||||||
|
arrayAppend(beacons, {
|
||||||
|
"BeaconID": qFinal.BeaconID[i],
|
||||||
|
"UUID": qFinal.BeaconUUID[i],
|
||||||
|
"BusinessID": qFinal.BusinessID[i],
|
||||||
|
"BusinessName": qFinal.BusinessName[i],
|
||||||
|
"ServicePointID": qFinal.ServicePointID[i],
|
||||||
|
"ServicePointName": qFinal.ServicePointName[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.beacons = beacons;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.error = e.message;
|
||||||
|
if (len(e.detail)) {
|
||||||
|
response.detail = e.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
|
|
@ -27,9 +27,6 @@ function readJsonBody(){
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- AUTH CONTEXT ---------- */
|
/* ---------- AUTH CONTEXT ---------- */
|
||||||
if (!structKeyExists(request,"UserID") || !isNumeric(request.UserID) || request.UserID LTE 0){
|
|
||||||
apiAbort({OK=false,ERROR="not_logged_in"});
|
|
||||||
}
|
|
||||||
if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0){
|
if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0){
|
||||||
apiAbort({OK=false,ERROR="no_business_selected"});
|
apiAbort({OK=false,ERROR="no_business_selected"});
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +46,7 @@ RelID = int(data.lt_Beacon_Businesses_ServicePointID);
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
<!--- Confirm the row exists for this BusinessID (and capture what it was) --->
|
<!--- Confirm the row exists for this BusinessID (and capture what it was) --->
|
||||||
<cfquery name="qFind" datasource="#application.datasource#">
|
<cfquery name="qFind" datasource="payfrit">
|
||||||
SELECT
|
SELECT
|
||||||
lt_Beacon_Businesses_ServicePointID,
|
lt_Beacon_Businesses_ServicePointID,
|
||||||
BeaconID,
|
BeaconID,
|
||||||
|
|
@ -73,7 +70,7 @@ RelID = int(data.lt_Beacon_Businesses_ServicePointID);
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Delete it --->
|
<!--- Delete it --->
|
||||||
<cfquery datasource="#application.datasource#">
|
<cfquery datasource="payfrit">
|
||||||
DELETE FROM lt_Beacon_Businesses_ServicePoints
|
DELETE FROM lt_Beacon_Businesses_ServicePoints
|
||||||
WHERE lt_Beacon_Businesses_ServicePointID =
|
WHERE lt_Beacon_Businesses_ServicePointID =
|
||||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#RelID#">
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#RelID#">
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,49 @@
|
||||||
<cfsetting showdebugoutput="false">
|
<cfsetting showdebugoutput="false">
|
||||||
<cfsetting enablecfoutputonly="true">
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
<cfscript>
|
<cfscript>
|
||||||
/*
|
|
||||||
FILE: C:\lucee\tomcat\webapps\ROOT\biz.payfrit.com\api\businesses\list.cfm
|
|
||||||
|
|
||||||
MVP:
|
|
||||||
- PUBLIC endpoint (no login required)
|
|
||||||
- Returns a minimal list of restaurants/businesses for the drilldown.
|
|
||||||
- IMPORTANT: Output JSON manually to preserve exact key casing.
|
|
||||||
|
|
||||||
RESPONSE (case-sensitive keys):
|
|
||||||
{
|
|
||||||
"OK": true,
|
|
||||||
"COUNT": 15,
|
|
||||||
"Businesses": [
|
|
||||||
{ "BusinessID": 12, "BusinessName": "Annie's Soul Delicious" },
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function apiAbort(obj) {
|
function apiAbort(obj) {
|
||||||
writeOutput('{');
|
writeOutput(serializeJSON(obj));
|
||||||
writeOutput('"OK":' & (obj.OK ? 'true' : 'false'));
|
|
||||||
|
|
||||||
if (structKeyExists(obj, "ERROR")) {
|
|
||||||
writeOutput(',"ERROR":"' & encodeForJSON(toString(obj.ERROR)) & '"');
|
|
||||||
}
|
|
||||||
if (structKeyExists(obj, "DETAIL")) {
|
|
||||||
writeOutput(',"DETAIL":"' & encodeForJSON(toString(obj.DETAIL)) & '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
writeOutput('}');
|
|
||||||
abort;
|
abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Do NOT use 'var' at top-level in a .cfm (local scope is function-only).
|
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||||
dsn = "";
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
if (structKeyExists(request, "datasource") && len(trim(request.datasource))) {
|
|
||||||
dsn = trim(request.datasource);
|
|
||||||
} else if (structKeyExists(request, "dsn") && len(trim(request.dsn))) {
|
|
||||||
dsn = trim(request.dsn);
|
|
||||||
} else if (structKeyExists(application, "datasource") && len(trim(application.datasource))) {
|
|
||||||
dsn = trim(application.datasource);
|
|
||||||
} else if (structKeyExists(application, "dsn") && len(trim(application.dsn))) {
|
|
||||||
dsn = trim(application.dsn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!len(dsn)) {
|
|
||||||
apiAbort({
|
|
||||||
OK=false,
|
|
||||||
ERROR="missing_datasource",
|
|
||||||
DETAIL="No datasource found in request.datasource, request.dsn, application.datasource, or application.dsn."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
q = queryExecute(
|
|
||||||
"
|
|
||||||
SELECT
|
|
||||||
BusinessID AS BusinessID,
|
|
||||||
BusinessName AS BusinessName
|
|
||||||
FROM Businesses
|
|
||||||
ORDER BY BusinessName
|
|
||||||
",
|
|
||||||
[],
|
|
||||||
{ datasource = dsn }
|
|
||||||
);
|
|
||||||
|
|
||||||
countVal = q.recordCount;
|
|
||||||
|
|
||||||
writeOutput('{');
|
|
||||||
writeOutput('"OK":true');
|
|
||||||
writeOutput(',"COUNT":' & countVal);
|
|
||||||
writeOutput(',"Businesses":[');
|
|
||||||
|
|
||||||
for (i = 1; i LTE q.recordCount; i = i + 1) {
|
|
||||||
if (i GT 1) writeOutput(',');
|
|
||||||
|
|
||||||
bid = q["BusinessID"][i];
|
|
||||||
bname = q["BusinessName"][i];
|
|
||||||
|
|
||||||
writeOutput('{');
|
|
||||||
writeOutput('"BusinessID":' & int(bid));
|
|
||||||
writeOutput(',"BusinessName":"' & encodeForJSON(toString(bname)) & '"');
|
|
||||||
writeOutput('}');
|
|
||||||
}
|
|
||||||
|
|
||||||
writeOutput(']}');
|
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
|
<cfquery name="q" datasource="payfrit">
|
||||||
|
SELECT
|
||||||
|
lt.lt_Beacon_Businesses_ServicePointID,
|
||||||
|
lt.BeaconID,
|
||||||
|
lt.BusinessID,
|
||||||
|
lt.ServicePointID,
|
||||||
|
lt.lt_Beacon_Businesses_ServicePointNotes,
|
||||||
|
b.BeaconName,
|
||||||
|
b.BeaconUUID,
|
||||||
|
sp.ServicePointName
|
||||||
|
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||||
|
JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
||||||
|
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||||
|
WHERE lt.BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
ORDER BY b.BeaconName, sp.ServicePointName
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfset assignments = []>
|
||||||
|
<cfloop query="q">
|
||||||
|
<cfset arrayAppend(assignments, {
|
||||||
|
"lt_Beacon_Businesses_ServicePointID" = q.lt_Beacon_Businesses_ServicePointID,
|
||||||
|
"BeaconID" = q.BeaconID,
|
||||||
|
"BusinessID" = q.BusinessID,
|
||||||
|
"ServicePointID" = q.ServicePointID,
|
||||||
|
"BeaconName" = q.BeaconName,
|
||||||
|
"BeaconUUID" = q.BeaconUUID,
|
||||||
|
"ServicePointName"= q.ServicePointName,
|
||||||
|
"lt_Beacon_Businesses_ServicePointNotes" = q.lt_Beacon_Businesses_ServicePointNotes
|
||||||
|
})>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
|
<cfoutput>#serializeJSON({ OK=true, ERROR="", COUNT=arrayLen(assignments), ASSIGNMENTS=assignments })#</cfoutput>
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,6 @@ function normStr(v){
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- AUTH CONTEXT ---------- */
|
/* ---------- AUTH CONTEXT ---------- */
|
||||||
if (!structKeyExists(request,"UserID") || !isNumeric(request.UserID) || request.UserID LTE 0){
|
|
||||||
apiAbort({OK=false,ERROR="not_logged_in"});
|
|
||||||
}
|
|
||||||
if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0){
|
if (!structKeyExists(request,"BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0){
|
||||||
apiAbort({OK=false,ERROR="no_business_selected"});
|
apiAbort({OK=false,ERROR="no_business_selected"});
|
||||||
}
|
}
|
||||||
|
|
@ -58,11 +55,11 @@ if (structKeyExists(data,"Notes")){
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
<!--- Validate Beacon belongs to Business --->
|
<!--- Validate Beacon belongs to Business --->
|
||||||
<cfquery name="qB" datasource="#application.datasource#">
|
<cfquery name="qB" datasource="payfrit">
|
||||||
SELECT BeaconID
|
SELECT BeaconID
|
||||||
FROM Beacons
|
FROM Beacons
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfif qB.recordCount EQ 0>
|
<cfif qB.recordCount EQ 0>
|
||||||
|
|
@ -71,11 +68,11 @@ if (structKeyExists(data,"Notes")){
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Validate ServicePoint belongs to Business --->
|
<!--- Validate ServicePoint belongs to Business --->
|
||||||
<cfquery name="qS" datasource="#application.datasource#">
|
<cfquery name="qS" datasource="payfrit">
|
||||||
SELECT ServicePointID
|
SELECT ServicePointID
|
||||||
FROM ServicePoints
|
FROM ServicePoints
|
||||||
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">
|
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfif qS.recordCount EQ 0>
|
<cfif qS.recordCount EQ 0>
|
||||||
|
|
@ -84,7 +81,7 @@ if (structKeyExists(data,"Notes")){
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Enforce 1:1 uniqueness --->
|
<!--- Enforce 1:1 uniqueness --->
|
||||||
<cfquery name="qBeaconTaken" datasource="#application.datasource#">
|
<cfquery name="qBeaconTaken" datasource="payfrit">
|
||||||
SELECT lt_Beacon_Businesses_ServicePointID
|
SELECT lt_Beacon_Businesses_ServicePointID
|
||||||
FROM lt_Beacon_Businesses_ServicePoints
|
FROM lt_Beacon_Businesses_ServicePoints
|
||||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
|
@ -96,7 +93,7 @@ if (structKeyExists(data,"Notes")){
|
||||||
<cfabort>
|
<cfabort>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfquery name="qServicePointTaken" datasource="#application.datasource#">
|
<cfquery name="qServicePointTaken" datasource="payfrit">
|
||||||
SELECT lt_Beacon_Businesses_ServicePointID
|
SELECT lt_Beacon_Businesses_ServicePointID
|
||||||
FROM lt_Beacon_Businesses_ServicePoints
|
FROM lt_Beacon_Businesses_ServicePoints
|
||||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
|
@ -109,7 +106,7 @@ if (structKeyExists(data,"Notes")){
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- INSERT --->
|
<!--- INSERT --->
|
||||||
<cfquery datasource="#application.datasource#">
|
<cfquery datasource="payfrit">
|
||||||
INSERT INTO lt_Beacon_Businesses_ServicePoints
|
INSERT INTO lt_Beacon_Businesses_ServicePoints
|
||||||
(BusinessID, BeaconID, ServicePointID,
|
(BusinessID, BeaconID, ServicePointID,
|
||||||
lt_Beacon_Businesses_ServicePointAssignedByUserID,
|
lt_Beacon_Businesses_ServicePointAssignedByUserID,
|
||||||
|
|
@ -119,12 +116,12 @@ if (structKeyExists(data,"Notes")){
|
||||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">,
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">,
|
||||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">,
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#BeaconID#">,
|
||||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">,
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#ServicePointID#">,
|
||||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.UserID#">,
|
<cfqueryparam cfsqltype="cf_sql_integer" value="1">,
|
||||||
<cfqueryparam cfsqltype="cf_sql_varchar" value="#Notes#" null="#(len(Notes) EQ 0)#">
|
<cfqueryparam cfsqltype="cf_sql_varchar" value="#Notes#" null="#(len(Notes) EQ 0)#">
|
||||||
)
|
)
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
<cfquery name="qID" datasource="#application.datasource#">
|
<cfquery name="qID" datasource="payfrit">
|
||||||
SELECT LAST_INSERT_ID() AS NewID
|
SELECT LAST_INSERT_ID() AS NewID
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,16 +75,25 @@ try {
|
||||||
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" }
|
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
// Send OTP via Twilio
|
// Send OTP via Twilio (if available)
|
||||||
smsResult = application.twilioObj.sendSMS(
|
smsMessage = "Code saved (SMS skipped in dev)";
|
||||||
recipientNumber: "+1" & phone,
|
if (structKeyExists(application, "twilioObj")) {
|
||||||
messageBody: "Your Payfrit login code is: " & otp
|
try {
|
||||||
);
|
smsResult = application.twilioObj.sendSMS(
|
||||||
|
recipientNumber: "+1" & phone,
|
||||||
|
messageBody: "Your Payfrit login code is: " & otp
|
||||||
|
);
|
||||||
|
smsMessage = smsResult.success ? "Login code sent" : "SMS failed - please try again";
|
||||||
|
} catch (any smsErr) {
|
||||||
|
smsMessage = "SMS error: " & smsErr.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeOutput(serializeJSON({
|
writeOutput(serializeJSON({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"UUID": qUser.UserUUID,
|
"UUID": qUser.UserUUID,
|
||||||
"MESSAGE": smsResult.success ? "Login code sent" : "SMS failed - please try again"
|
"MESSAGE": smsMessage,
|
||||||
|
"DEV_OTP": otp
|
||||||
}));
|
}));
|
||||||
|
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,20 @@ function readJsonBody() {
|
||||||
}
|
}
|
||||||
|
|
||||||
data = readJsonBody();
|
data = readJsonBody();
|
||||||
|
httpHeaders = getHttpRequestData().headers;
|
||||||
|
|
||||||
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
|
// Get BusinessID from: session > body > X-Business-ID header
|
||||||
apiAbort({ OK=false, ERROR="not_logged_in" });
|
bizId = 0;
|
||||||
|
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
|
||||||
|
bizId = int(request.BusinessID);
|
||||||
}
|
}
|
||||||
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
if (bizId LTE 0 && structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID) && data.BusinessID GT 0) {
|
||||||
|
bizId = int(data.BusinessID);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"]) && httpHeaders["X-Business-ID"] GT 0) {
|
||||||
|
bizId = int(httpHeaders["X-Business-ID"]);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0) {
|
||||||
apiAbort({ OK=false, ERROR="no_business_selected" });
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,19 +47,19 @@ if (!structKeyExists(data, "BeaconID") || !isNumeric(data.BeaconID) || int(data.
|
||||||
beaconId = int(data.BeaconID);
|
beaconId = int(data.BeaconID);
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
<cfquery datasource="#application.datasource#">
|
<cfquery datasource="payfrit">
|
||||||
UPDATE Beacons
|
UPDATE Beacons
|
||||||
SET IsActive = 0
|
SET BeaconIsActive = 0
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
<!--- confirm --->
|
<!--- confirm --->
|
||||||
<cfquery name="qCheck" datasource="#application.datasource#">
|
<cfquery name="qCheck" datasource="payfrit">
|
||||||
SELECT BeaconID, IsActive
|
SELECT BeaconID, BeaconIsActive
|
||||||
FROM Beacons
|
FROM Beacons
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,6 @@ function readJsonBody() {
|
||||||
|
|
||||||
data = readJsonBody();
|
data = readJsonBody();
|
||||||
|
|
||||||
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
|
|
||||||
apiAbort({ OK=false, ERROR="not_logged_in" });
|
|
||||||
}
|
|
||||||
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||||
apiAbort({ OK=false, ERROR="no_business_selected" });
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
}
|
}
|
||||||
|
|
@ -38,20 +35,16 @@ if (!structKeyExists(data, "BeaconID") || !isNumeric(data.BeaconID) || int(data.
|
||||||
beaconId = int(data.BeaconID);
|
beaconId = int(data.BeaconID);
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
<cfquery name="q" datasource="#application.datasource#">
|
<cfquery name="q" datasource="payfrit">
|
||||||
SELECT
|
SELECT
|
||||||
BeaconID,
|
BeaconID,
|
||||||
BusinessID,
|
BeaconBusinessID,
|
||||||
BeaconName,
|
BeaconName,
|
||||||
UUID,
|
BeaconUUID,
|
||||||
NamespaceId,
|
BeaconIsActive
|
||||||
InstanceId,
|
|
||||||
IsActive,
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt
|
|
||||||
FROM Beacons
|
FROM Beacons
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
|
|
@ -62,14 +55,10 @@ beaconId = int(data.BeaconID);
|
||||||
|
|
||||||
<cfset beacon = {
|
<cfset beacon = {
|
||||||
"BeaconID" = q.BeaconID,
|
"BeaconID" = q.BeaconID,
|
||||||
"BusinessID" = q.BusinessID,
|
"BusinessID" = q.BeaconBusinessID,
|
||||||
"BeaconName" = q.BeaconName,
|
"BeaconName" = q.BeaconName,
|
||||||
"UUID" = q.UUID,
|
"UUID" = q.BeaconUUID,
|
||||||
"NamespaceId" = q.NamespaceId,
|
"IsActive" = q.BeaconIsActive
|
||||||
"InstanceId" = q.InstanceId,
|
|
||||||
"IsActive" = q.IsActive,
|
|
||||||
"CreatedAt" = q.CreatedAt,
|
|
||||||
"UpdatedAt" = q.UpdatedAt
|
|
||||||
}>
|
}>
|
||||||
|
|
||||||
<cfoutput>#serializeJSON({ OK=true, ERROR="", BEACON=beacon })#</cfoutput>
|
<cfoutput>#serializeJSON({ OK=true, ERROR="", BEACON=beacon })#</cfoutput>
|
||||||
|
|
|
||||||
|
|
@ -10,27 +10,35 @@ function apiAbort(obj) {
|
||||||
abort;
|
abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readJsonBody() {
|
// Read JSON body once
|
||||||
|
data = {};
|
||||||
|
try {
|
||||||
raw = toString(getHttpRequestData().content);
|
raw = toString(getHttpRequestData().content);
|
||||||
if (isNull(raw) || len(trim(raw)) EQ 0) return {};
|
if (len(trim(raw))) {
|
||||||
try {
|
data = deserializeJSON(raw);
|
||||||
parsed = deserializeJSON(raw);
|
if (!isStruct(data)) data = {};
|
||||||
} catch(any e) {
|
|
||||||
apiAbort({ OK=false, ERROR="bad_json", MESSAGE="Invalid JSON body" });
|
|
||||||
}
|
}
|
||||||
// We only accept object bodies
|
} catch (any e) {
|
||||||
if (!isStruct(parsed)) return {};
|
data = {};
|
||||||
return parsed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data = readJsonBody();
|
httpHeaders = getHttpRequestData().headers;
|
||||||
|
|
||||||
// Auth/business gating should already happen in /api/Application.cfm,
|
// Get BusinessID from: session > body > X-Business-ID header > URL
|
||||||
// but keep this defensive so the file is safe standalone.
|
bizId = 0;
|
||||||
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
|
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
|
||||||
apiAbort({ OK=false, ERROR="not_logged_in" });
|
bizId = int(request.BusinessID);
|
||||||
}
|
}
|
||||||
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
if (bizId LTE 0 && structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID) && data.BusinessID GT 0) {
|
||||||
|
bizId = int(data.BusinessID);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"]) && httpHeaders["X-Business-ID"] GT 0) {
|
||||||
|
bizId = int(httpHeaders["X-Business-ID"]);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID) && url.BusinessID GT 0) {
|
||||||
|
bizId = int(url.BusinessID);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0) {
|
||||||
apiAbort({ OK=false, ERROR="no_business_selected" });
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,21 +55,17 @@ if (structKeyExists(data, "onlyActive")) {
|
||||||
}
|
}
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
<cfquery name="q" datasource="#application.datasource#">
|
<cfquery name="q" datasource="payfrit">
|
||||||
SELECT
|
SELECT
|
||||||
BeaconID,
|
BeaconID,
|
||||||
BusinessID,
|
BeaconBusinessID,
|
||||||
BeaconName,
|
BeaconName,
|
||||||
UUID,
|
BeaconUUID,
|
||||||
NamespaceId,
|
BeaconIsActive
|
||||||
InstanceId,
|
|
||||||
IsActive,
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt
|
|
||||||
FROM Beacons
|
FROM Beacons
|
||||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
WHERE BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
<cfif onlyActive>
|
<cfif onlyActive>
|
||||||
AND IsActive = 1
|
AND BeaconIsActive = 1
|
||||||
</cfif>
|
</cfif>
|
||||||
ORDER BY BeaconName, BeaconID
|
ORDER BY BeaconName, BeaconID
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
@ -70,15 +74,11 @@ if (structKeyExists(data, "onlyActive")) {
|
||||||
<cfloop query="q">
|
<cfloop query="q">
|
||||||
<cfset arrayAppend(beacons, {
|
<cfset arrayAppend(beacons, {
|
||||||
"BeaconID" = q.BeaconID,
|
"BeaconID" = q.BeaconID,
|
||||||
"BusinessID" = q.BusinessID,
|
"BusinessID" = q.BeaconBusinessID,
|
||||||
"BeaconName" = q.BeaconName,
|
"BeaconName" = q.BeaconName,
|
||||||
"UUID" = q.UUID,
|
"UUID" = q.BeaconUUID,
|
||||||
"NamespaceId" = q.NamespaceId,
|
"IsActive" = q.BeaconIsActive
|
||||||
"InstanceId" = q.InstanceId,
|
|
||||||
"IsActive" = q.IsActive,
|
|
||||||
"CreatedAt" = q.CreatedAt,
|
|
||||||
"UpdatedAt" = q.UpdatedAt
|
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
<cfoutput>#serializeJSON({ OK=true, ERROR="", BusinessID=request.BusinessID, COUNT=arrayLen(beacons), BEACONS=beacons })#</cfoutput>
|
<cfoutput>#serializeJSON({ OK=true, ERROR="", BusinessID=bizId, COUNT=arrayLen(beacons), BEACONS=beacons })#</cfoutput>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
<cfheader name="Cache-Control" value="no-store">
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cftry>
|
||||||
<cfscript>
|
<cfscript>
|
||||||
function apiAbort(obj) {
|
function apiAbort(obj) {
|
||||||
writeOutput(serializeJSON(obj));
|
writeOutput(serializeJSON(obj));
|
||||||
|
|
@ -29,13 +30,20 @@ function normStr(v) {
|
||||||
|
|
||||||
data = readJsonBody();
|
data = readJsonBody();
|
||||||
|
|
||||||
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
|
|
||||||
apiAbort({ OK=false, ERROR="not_logged_in" });
|
|
||||||
}
|
|
||||||
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||||
apiAbort({ OK=false, ERROR="no_business_selected" });
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify the business exists
|
||||||
|
qBiz = queryExecute(
|
||||||
|
"SELECT BusinessID FROM Businesses WHERE BusinessID = ? LIMIT 1",
|
||||||
|
[ { value=request.BusinessID, cfsqltype="cf_sql_integer" } ],
|
||||||
|
{ datasource="payfrit" }
|
||||||
|
);
|
||||||
|
if (qBiz.recordCount EQ 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="invalid_business", MESSAGE="Business ID #request.BusinessID# does not exist. Please log out and log back in." });
|
||||||
|
}
|
||||||
|
|
||||||
if (!structKeyExists(data, "BeaconName") || len(normStr(data.BeaconName)) EQ 0) {
|
if (!structKeyExists(data, "BeaconName") || len(normStr(data.BeaconName)) EQ 0) {
|
||||||
apiAbort({ OK=false, ERROR="missing_beacon_name", MESSAGE="BeaconName is required" });
|
apiAbort({ OK=false, ERROR="missing_beacon_name", MESSAGE="BeaconName is required" });
|
||||||
}
|
}
|
||||||
|
|
@ -47,8 +55,6 @@ if (structKeyExists(data, "BeaconID") && isNumeric(data.BeaconID) && int(data.Be
|
||||||
|
|
||||||
beaconName = normStr(data.BeaconName);
|
beaconName = normStr(data.BeaconName);
|
||||||
uuid = structKeyExists(data, "UUID") ? normStr(data.UUID) : "";
|
uuid = structKeyExists(data, "UUID") ? normStr(data.UUID) : "";
|
||||||
namespaceId = structKeyExists(data, "NamespaceId") ? normStr(data.NamespaceId) : "";
|
|
||||||
instanceId = structKeyExists(data, "InstanceId") ? normStr(data.InstanceId) : "";
|
|
||||||
|
|
||||||
isActive = 1;
|
isActive = 1;
|
||||||
if (structKeyExists(data, "IsActive")) {
|
if (structKeyExists(data, "IsActive")) {
|
||||||
|
|
@ -60,86 +66,79 @@ if (structKeyExists(data, "IsActive")) {
|
||||||
|
|
||||||
<cfif beaconId GT 0>
|
<cfif beaconId GT 0>
|
||||||
<!--- Update, scoped to this business --->
|
<!--- Update, scoped to this business --->
|
||||||
<cfquery datasource="#application.datasource#">
|
<cfquery datasource="payfrit">
|
||||||
UPDATE Beacons
|
UPDATE Beacons
|
||||||
SET
|
SET
|
||||||
BeaconName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#beaconName#">,
|
BeaconName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#beaconName#">,
|
||||||
UUID = <cfqueryparam cfsqltype="cf_sql_varchar" value="#uuid#" null="#(len(uuid) EQ 0)#">,
|
BeaconUUID = <cfqueryparam cfsqltype="cf_sql_varchar" value="#uuid#" null="#(len(uuid) EQ 0)#">,
|
||||||
NamespaceId = <cfqueryparam cfsqltype="cf_sql_varchar" value="#namespaceId#" null="#(len(namespaceId) EQ 0)#">,
|
BeaconIsActive = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
|
||||||
InstanceId = <cfqueryparam cfsqltype="cf_sql_varchar" value="#instanceId#" null="#(len(instanceId) EQ 0)#">,
|
|
||||||
IsActive = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
|
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
<!--- confirm it exists/belongs to business --->
|
<!--- confirm it exists/belongs to business --->
|
||||||
<cfquery name="qCheck" datasource="#application.datasource#">
|
<cfquery name="qCheck" datasource="payfrit">
|
||||||
SELECT BeaconID
|
SELECT BeaconID
|
||||||
FROM Beacons
|
FROM Beacons
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
<cfif qCheck.recordCount EQ 0>
|
<cfif qCheck.recordCount EQ 0>
|
||||||
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found" })#</cfoutput>
|
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found", MESSAGE="Beacon not found or doesn't belong to this business" })#</cfoutput>
|
||||||
<cfabort>
|
<cfabort>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfelse>
|
<cfelse>
|
||||||
<!--- Insert --->
|
<!--- Insert --->
|
||||||
<cfquery datasource="#application.datasource#">
|
<cfquery datasource="payfrit">
|
||||||
INSERT INTO Beacons (
|
INSERT INTO Beacons (
|
||||||
BusinessID,
|
BeaconBusinessID,
|
||||||
BeaconName,
|
BeaconName,
|
||||||
UUID,
|
BeaconUUID,
|
||||||
NamespaceId,
|
BeaconIsActive
|
||||||
InstanceId,
|
|
||||||
IsActive
|
|
||||||
) VALUES (
|
) VALUES (
|
||||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">,
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">,
|
||||||
<cfqueryparam cfsqltype="cf_sql_varchar" value="#beaconName#">,
|
<cfqueryparam cfsqltype="cf_sql_varchar" value="#beaconName#">,
|
||||||
<cfqueryparam cfsqltype="cf_sql_varchar" value="#uuid#" null="#(len(uuid) EQ 0)#">,
|
<cfqueryparam cfsqltype="cf_sql_varchar" value="#uuid#" null="#(len(uuid) EQ 0)#">,
|
||||||
<cfqueryparam cfsqltype="cf_sql_varchar" value="#namespaceId#" null="#(len(namespaceId) EQ 0)#">,
|
|
||||||
<cfqueryparam cfsqltype="cf_sql_varchar" value="#instanceId#" null="#(len(instanceId) EQ 0)#">,
|
|
||||||
<cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
|
<cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
|
||||||
)
|
)
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
<cfquery name="qId" datasource="#application.datasource#">
|
<cfquery name="qId" datasource="payfrit">
|
||||||
SELECT LAST_INSERT_ID() AS BeaconID
|
SELECT LAST_INSERT_ID() AS BeaconID
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfset beaconId = qId.BeaconID>
|
<cfset beaconId = qId.BeaconID>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Return saved row --->
|
<!--- Return saved row --->
|
||||||
<cfquery name="qOut" datasource="#application.datasource#">
|
<cfquery name="qOut" datasource="payfrit">
|
||||||
SELECT
|
SELECT
|
||||||
BeaconID,
|
BeaconID,
|
||||||
BusinessID,
|
BeaconBusinessID,
|
||||||
BeaconName,
|
BeaconName,
|
||||||
UUID,
|
BeaconUUID,
|
||||||
NamespaceId,
|
BeaconIsActive
|
||||||
InstanceId,
|
|
||||||
IsActive,
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt
|
|
||||||
FROM Beacons
|
FROM Beacons
|
||||||
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
WHERE BeaconID = <cfqueryparam cfsqltype="cf_sql_integer" value="#beaconId#">
|
||||||
AND BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
AND BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
<cfset beacon = {
|
<cfset beacon = {
|
||||||
"BeaconID" = qOut.BeaconID,
|
"BeaconID" = qOut.BeaconID,
|
||||||
"BusinessID" = qOut.BusinessID,
|
"BusinessID" = qOut.BeaconBusinessID,
|
||||||
"BeaconName" = qOut.BeaconName,
|
"BeaconName" = qOut.BeaconName,
|
||||||
"UUID" = qOut.UUID,
|
"UUID" = qOut.BeaconUUID,
|
||||||
"NamespaceId" = qOut.NamespaceId,
|
"IsActive" = qOut.BeaconIsActive
|
||||||
"InstanceId" = qOut.InstanceId,
|
|
||||||
"IsActive" = qOut.IsActive,
|
|
||||||
"CreatedAt" = qOut.CreatedAt,
|
|
||||||
"UpdatedAt" = qOut.UpdatedAt
|
|
||||||
}>
|
}>
|
||||||
|
|
||||||
<cfoutput>#serializeJSON({ OK=true, ERROR="", BEACON=beacon })#</cfoutput>
|
<cfoutput>#serializeJSON({ OK=true, ERROR="", BEACON=beacon })#</cfoutput>
|
||||||
|
|
||||||
|
<cfcatch type="any">
|
||||||
|
<cfheader statuscode="200" statustext="OK">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfoutput>#serializeJSON({ OK=false, ERROR="server_error", MESSAGE=cfcatch.message, DETAIL=cfcatch.detail })#</cfoutput>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ try {
|
||||||
BusinessPhone,
|
BusinessPhone,
|
||||||
BusinessStripeAccountID,
|
BusinessStripeAccountID,
|
||||||
BusinessStripeOnboardingComplete,
|
BusinessStripeOnboardingComplete,
|
||||||
BusinessIsHiring
|
BusinessIsHiring,
|
||||||
|
BusinessHeaderImageExtension
|
||||||
FROM Businesses
|
FROM Businesses
|
||||||
WHERE BusinessID = :businessID
|
WHERE BusinessID = :businessID
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
@ -51,17 +52,27 @@ try {
|
||||||
abort;
|
abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get address from Addresses table
|
// Get address from Addresses table (either linked via AddressBusinessID or via Businesses.BusinessAddressID)
|
||||||
qAddr = queryExecute("
|
qAddr = queryExecute("
|
||||||
SELECT a.AddressLine1, a.AddressLine2, a.AddressCity, a.AddressZIPCode, s.tt_StateAbbreviation
|
SELECT a.AddressLine1, a.AddressLine2, a.AddressCity, a.AddressZIPCode, s.tt_StateAbbreviation
|
||||||
FROM Addresses a
|
FROM Addresses a
|
||||||
LEFT JOIN tt_States s ON s.tt_StateID = a.AddressStateID
|
LEFT JOIN tt_States s ON s.tt_StateID = a.AddressStateID
|
||||||
WHERE a.AddressBusinessID = :businessID AND a.AddressUserID = 0 AND a.AddressIsDeleted = 0
|
WHERE (a.AddressBusinessID = :businessID OR a.AddressID = (SELECT BusinessAddressID FROM Businesses WHERE BusinessID = :businessID))
|
||||||
|
AND a.AddressIsDeleted = 0
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
addressStr = "";
|
addressStr = "";
|
||||||
|
addressLine1 = "";
|
||||||
|
addressCity = "";
|
||||||
|
addressState = "";
|
||||||
|
addressZip = "";
|
||||||
if (qAddr.recordCount > 0) {
|
if (qAddr.recordCount > 0) {
|
||||||
|
addressLine1 = qAddr.AddressLine1;
|
||||||
|
addressCity = qAddr.AddressCity;
|
||||||
|
addressState = qAddr.tt_StateAbbreviation;
|
||||||
|
addressZip = qAddr.AddressZIPCode;
|
||||||
|
|
||||||
addressParts = [];
|
addressParts = [];
|
||||||
if (len(qAddr.AddressLine1)) arrayAppend(addressParts, qAddr.AddressLine1);
|
if (len(qAddr.AddressLine1)) arrayAppend(addressParts, qAddr.AddressLine1);
|
||||||
if (len(qAddr.AddressLine2)) arrayAppend(addressParts, qAddr.AddressLine2);
|
if (len(qAddr.AddressLine2)) arrayAppend(addressParts, qAddr.AddressLine2);
|
||||||
|
|
@ -116,6 +127,10 @@ try {
|
||||||
"BusinessID": q.BusinessID,
|
"BusinessID": q.BusinessID,
|
||||||
"BusinessName": q.BusinessName,
|
"BusinessName": q.BusinessName,
|
||||||
"BusinessAddress": addressStr,
|
"BusinessAddress": addressStr,
|
||||||
|
"AddressLine1": addressLine1,
|
||||||
|
"AddressCity": addressCity,
|
||||||
|
"AddressState": addressState,
|
||||||
|
"AddressZip": addressZip,
|
||||||
"BusinessPhone": q.BusinessPhone,
|
"BusinessPhone": q.BusinessPhone,
|
||||||
"BusinessHours": hoursStr,
|
"BusinessHours": hoursStr,
|
||||||
"BusinessHoursDetail": hoursArr,
|
"BusinessHoursDetail": hoursArr,
|
||||||
|
|
@ -123,6 +138,11 @@ try {
|
||||||
"IsHiring": q.BusinessIsHiring == 1
|
"IsHiring": q.BusinessIsHiring == 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add header image URL if extension exists
|
||||||
|
if (len(q.BusinessHeaderImageExtension)) {
|
||||||
|
business["HeaderImageURL"] = "https://biz.payfrit.com/uploads/headers/" & q.BusinessID & "." & q.BusinessHeaderImageExtension;
|
||||||
|
}
|
||||||
|
|
||||||
response["OK"] = true;
|
response["OK"] = true;
|
||||||
response["BUSINESS"] = business;
|
response["BUSINESS"] = business;
|
||||||
|
|
||||||
|
|
|
||||||
72
api/businesses/saveBrandColor.cfm
Normal file
72
api/businesses/saveBrandColor.cfm
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Save Business Brand Color
|
||||||
|
* POST JSON: { "BusinessID": 37, "BrandColor": "#1B4D3E" }
|
||||||
|
*/
|
||||||
|
|
||||||
|
function apiAbort(payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
if (!len(requestBody)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "no_body", "MESSAGE": "No request body provided" });
|
||||||
|
}
|
||||||
|
|
||||||
|
data = deserializeJSON(requestBody);
|
||||||
|
|
||||||
|
// Get BusinessID from body or request scope
|
||||||
|
bizId = 0;
|
||||||
|
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID) && data.BusinessID GT 0) {
|
||||||
|
bizId = int(data.BusinessID);
|
||||||
|
} else if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
|
||||||
|
bizId = int(request.BusinessID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bizId LTE 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate color format
|
||||||
|
brandColor = structKeyExists(data, "BrandColor") && isSimpleValue(data.BrandColor) ? trim(data.BrandColor) : "";
|
||||||
|
|
||||||
|
// Allow empty to clear, or validate hex format
|
||||||
|
if (len(brandColor) GT 0) {
|
||||||
|
// Must be #RRGGBB format
|
||||||
|
if (!reFind("^##[0-9A-Fa-f]{6}$", brandColor)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_color", "MESSAGE": "Color must be in #RRGGBB format" });
|
||||||
|
}
|
||||||
|
brandColor = uCase(brandColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the database
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Businesses
|
||||||
|
SET BusinessBrandColor = :color
|
||||||
|
WHERE BusinessID = :bizId
|
||||||
|
", {
|
||||||
|
color: { value: len(brandColor) ? brandColor : "", null: !len(brandColor), cfsqltype: "cf_sql_varchar" },
|
||||||
|
bizId: { value: bizId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"ERROR": "",
|
||||||
|
"MESSAGE": "Brand color saved",
|
||||||
|
"BRANDCOLOR": brandColor
|
||||||
|
}));
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cfcatch type="any">
|
||||||
|
<cfheader statuscode="200" statustext="OK">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "server_error", "MESSAGE": cfcatch.message, "DETAIL": cfcatch.detail })#</cfoutput>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
115
api/businesses/update.cfm
Normal file
115
api/businesses/update.cfm
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Update Business Info
|
||||||
|
*
|
||||||
|
* POST JSON:
|
||||||
|
* {
|
||||||
|
* "BusinessID": 37,
|
||||||
|
* "BusinessName": "My Business",
|
||||||
|
* "BusinessPhone": "(555) 123-4567",
|
||||||
|
* "AddressLine1": "123 Main St",
|
||||||
|
* "City": "Los Angeles",
|
||||||
|
* "State": "CA",
|
||||||
|
* "Zip": "90001"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
if (!len(requestBody)) {
|
||||||
|
throw(message="No request body provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
data = deserializeJSON(requestBody);
|
||||||
|
|
||||||
|
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0;
|
||||||
|
if (businessId == 0) {
|
||||||
|
throw(message="BusinessID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update business name and phone
|
||||||
|
bizName = structKeyExists(data, "BusinessName") && isSimpleValue(data.BusinessName) ? trim(data.BusinessName) : "";
|
||||||
|
bizPhone = structKeyExists(data, "BusinessPhone") && isSimpleValue(data.BusinessPhone) ? trim(data.BusinessPhone) : "";
|
||||||
|
|
||||||
|
if (len(bizName)) {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Businesses SET BusinessName = :name, BusinessPhone = :phone
|
||||||
|
WHERE BusinessID = :id
|
||||||
|
", {
|
||||||
|
name: bizName,
|
||||||
|
phone: bizPhone,
|
||||||
|
id: businessId
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or create address
|
||||||
|
addressLine1 = structKeyExists(data, "AddressLine1") && isSimpleValue(data.AddressLine1) ? trim(data.AddressLine1) : "";
|
||||||
|
city = structKeyExists(data, "City") && isSimpleValue(data.City) ? trim(data.City) : "";
|
||||||
|
state = structKeyExists(data, "State") && isSimpleValue(data.State) ? trim(data.State) : "";
|
||||||
|
zip = structKeyExists(data, "Zip") && isSimpleValue(data.Zip) ? trim(data.Zip) : "";
|
||||||
|
|
||||||
|
// Clean up city - remove trailing punctuation
|
||||||
|
city = reReplace(city, "[,.\s]+$", "", "all");
|
||||||
|
|
||||||
|
// Get state ID
|
||||||
|
stateID = 0;
|
||||||
|
if (len(state)) {
|
||||||
|
qState = queryExecute("
|
||||||
|
SELECT tt_StateID FROM tt_States WHERE tt_StateAbbreviation = :abbr
|
||||||
|
", { abbr: uCase(state) }, { datasource: "payfrit" });
|
||||||
|
if (qState.recordCount > 0) {
|
||||||
|
stateID = qState.tt_StateID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if business has an address
|
||||||
|
qAddr = queryExecute("
|
||||||
|
SELECT AddressID FROM Addresses
|
||||||
|
WHERE AddressBusinessID = :bizID AND AddressUserID = 0 AND AddressIsDeleted = 0
|
||||||
|
LIMIT 1
|
||||||
|
", { bizID: businessId }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qAddr.recordCount > 0) {
|
||||||
|
// Update existing address
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Addresses SET
|
||||||
|
AddressLine1 = :line1,
|
||||||
|
AddressCity = :city,
|
||||||
|
AddressStateID = :stateID,
|
||||||
|
AddressZIPCode = :zip
|
||||||
|
WHERE AddressID = :addrID
|
||||||
|
", {
|
||||||
|
line1: addressLine1,
|
||||||
|
city: city,
|
||||||
|
stateID: stateID,
|
||||||
|
zip: zip,
|
||||||
|
addrID: qAddr.AddressID
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
} else {
|
||||||
|
// Create new address
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Addresses (AddressLine1, AddressCity, AddressStateID, AddressZIPCode, AddressBusinessID, AddressUserID, AddressTypeID, AddressAddedOn)
|
||||||
|
VALUES (:line1, :city, :stateID, :zip, :bizID, 0, 2, NOW())
|
||||||
|
", {
|
||||||
|
line1: addressLine1,
|
||||||
|
city: city,
|
||||||
|
stateID: stateID,
|
||||||
|
zip: zip,
|
||||||
|
bizID: businessId
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.ERROR = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
77
api/businesses/updateHours.cfm
Normal file
77
api/businesses/updateHours.cfm
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Update Business Hours
|
||||||
|
*
|
||||||
|
* POST JSON:
|
||||||
|
* {
|
||||||
|
* "BusinessID": 37,
|
||||||
|
* "Hours": [
|
||||||
|
* { "dayId": 1, "open": "09:00", "close": "17:00" },
|
||||||
|
* { "dayId": 2, "open": "09:00", "close": "17:00" },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Days not in the Hours array are considered closed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
if (!len(requestBody)) {
|
||||||
|
throw(message="No request body provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
data = deserializeJSON(requestBody);
|
||||||
|
|
||||||
|
businessId = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0;
|
||||||
|
if (businessId == 0) {
|
||||||
|
throw(message="BusinessID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
hours = structKeyExists(data, "Hours") && isArray(data.Hours) ? data.Hours : [];
|
||||||
|
|
||||||
|
// Delete all existing hours for this business
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Hours WHERE HoursBusinessID = :bizID
|
||||||
|
", { bizID: businessId }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Insert new hours
|
||||||
|
for (h in hours) {
|
||||||
|
if (!isStruct(h)) continue;
|
||||||
|
|
||||||
|
dayId = structKeyExists(h, "dayId") ? val(h.dayId) : 0;
|
||||||
|
openTime = structKeyExists(h, "open") && isSimpleValue(h.open) ? h.open : "09:00";
|
||||||
|
closeTime = structKeyExists(h, "close") && isSimpleValue(h.close) ? h.close : "17:00";
|
||||||
|
|
||||||
|
if (dayId >= 1 && dayId <= 7) {
|
||||||
|
// Convert HH:MM to HH:MM:SS if needed
|
||||||
|
if (len(openTime) == 5) openTime = openTime & ":00";
|
||||||
|
if (len(closeTime) == 5) closeTime = closeTime & ":00";
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime)
|
||||||
|
VALUES (:bizID, :dayID, :openTime, :closeTime)
|
||||||
|
", {
|
||||||
|
bizID: businessId,
|
||||||
|
dayID: dayId,
|
||||||
|
openTime: openTime,
|
||||||
|
closeTime: closeTime
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK = true;
|
||||||
|
response.hoursUpdated = arrayLen(hours);
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.ERROR = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
70
api/menu/clearBusinessData.cfm
Normal file
70
api/menu/clearBusinessData.cfm
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
requestData = {};
|
||||||
|
if (len(requestBody)) {
|
||||||
|
requestData = deserializeJSON(requestBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
businessID = val(requestData.BusinessID ?: 0);
|
||||||
|
confirmDelete = requestData.confirm ?: "";
|
||||||
|
|
||||||
|
if (businessID == 0) {
|
||||||
|
throw("BusinessID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmDelete != "DELETE_ALL_DATA") {
|
||||||
|
throw("Must pass confirm: 'DELETE_ALL_DATA' to proceed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get counts before deletion
|
||||||
|
qItemCount = queryExecute("SELECT COUNT(*) as cnt FROM Items WHERE ItemBusinessID = :bid", { bid: businessID }, { datasource: "payfrit" });
|
||||||
|
qCatCount = queryExecute("SELECT COUNT(*) as cnt FROM Categories WHERE CategoryBusinessID = :bid", { bid: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Get item IDs for this business to delete template links
|
||||||
|
qItemIds = queryExecute("SELECT ItemID FROM Items WHERE ItemBusinessID = :bid", { bid: businessID }, { datasource: "payfrit" });
|
||||||
|
itemIds = [];
|
||||||
|
for (row in qItemIds) {
|
||||||
|
arrayAppend(itemIds, row.ItemID);
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedLinks = 0;
|
||||||
|
if (arrayLen(itemIds) > 0) {
|
||||||
|
// Delete template links for these items
|
||||||
|
queryExecute("DELETE FROM ItemTemplateLinks WHERE ItemID IN (:ids) OR TemplateItemID IN (:ids)",
|
||||||
|
{ ids: { value: arrayToList(itemIds), cfsqltype: "cf_sql_varchar", list: true } },
|
||||||
|
{ datasource: "payfrit" });
|
||||||
|
deletedLinks = arrayLen(itemIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all items for this business
|
||||||
|
queryExecute("DELETE FROM Items WHERE ItemBusinessID = :bid", { bid: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
// Delete all categories for this business
|
||||||
|
queryExecute("DELETE FROM Categories WHERE CategoryBusinessID = :bid", { bid: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"OK": true,
|
||||||
|
"deleted": {
|
||||||
|
"items": qItemCount.cnt,
|
||||||
|
"categories": qCatCount.cnt,
|
||||||
|
"templateLinks": deletedLinks
|
||||||
|
},
|
||||||
|
"businessID": businessID
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response = {
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": e.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
74
api/menu/debug.cfm
Normal file
74
api/menu/debug.cfm
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
response = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all businesses with items
|
||||||
|
qBusinesses = queryExecute("
|
||||||
|
SELECT DISTINCT i.ItemBusinessID, COUNT(*) as ItemCount
|
||||||
|
FROM Items i
|
||||||
|
WHERE i.ItemBusinessID > 0
|
||||||
|
GROUP BY i.ItemBusinessID
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response["businesses_with_items"] = [];
|
||||||
|
for (b in qBusinesses) {
|
||||||
|
arrayAppend(response["businesses_with_items"], {
|
||||||
|
"businessID": b.ItemBusinessID,
|
||||||
|
"itemCount": b.ItemCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get categories
|
||||||
|
qCategories = queryExecute("
|
||||||
|
SELECT CategoryBusinessID, COUNT(*) as cnt
|
||||||
|
FROM Categories
|
||||||
|
GROUP BY CategoryBusinessID
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response["categories_by_business"] = [];
|
||||||
|
for (c in qCategories) {
|
||||||
|
arrayAppend(response["categories_by_business"], {
|
||||||
|
"businessID": c.CategoryBusinessID,
|
||||||
|
"count": c.cnt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sample items
|
||||||
|
qItems = queryExecute("
|
||||||
|
SELECT ItemID, ItemBusinessID, ItemCategoryID, ItemParentItemID, ItemName, ItemIsActive
|
||||||
|
FROM Items
|
||||||
|
WHERE ItemIsActive = 1
|
||||||
|
LIMIT 20
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response["sample_items"] = [];
|
||||||
|
for (i in qItems) {
|
||||||
|
arrayAppend(response["sample_items"], {
|
||||||
|
"id": i.ItemID,
|
||||||
|
"businessID": i.ItemBusinessID,
|
||||||
|
"categoryID": i.ItemCategoryID,
|
||||||
|
"parentID": i.ItemParentItemID,
|
||||||
|
"name": i.ItemName,
|
||||||
|
"active": i.ItemIsActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template links
|
||||||
|
qLinks = queryExecute("
|
||||||
|
SELECT COUNT(*) as cnt FROM ItemTemplateLinks
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
response["template_link_count"] = qLinks.cnt;
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail ?: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
|
|
@ -345,9 +345,23 @@ try {
|
||||||
arrayAppend(templateLibrary, templatesById[templateID]);
|
arrayAppend(templateLibrary, templatesById[templateID]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get business brand color
|
||||||
|
brandColor = "";
|
||||||
|
try {
|
||||||
|
qBrand = queryExecute("
|
||||||
|
SELECT BusinessBrandColor FROM Businesses WHERE BusinessID = :bizId
|
||||||
|
", { bizId: businessID }, { datasource: "payfrit" });
|
||||||
|
if (qBrand.recordCount > 0 && len(trim(qBrand.BusinessBrandColor))) {
|
||||||
|
brandColor = qBrand.BusinessBrandColor;
|
||||||
|
}
|
||||||
|
} catch (any e) {
|
||||||
|
// Column may not exist yet, ignore
|
||||||
|
}
|
||||||
|
|
||||||
response["OK"] = true;
|
response["OK"] = true;
|
||||||
response["MENU"] = { "categories": categories };
|
response["MENU"] = { "categories": categories };
|
||||||
response["TEMPLATES"] = templateLibrary;
|
response["TEMPLATES"] = templateLibrary;
|
||||||
|
response["BRANDCOLOR"] = brandColor;
|
||||||
response["CATEGORY_COUNT"] = arrayLen(categories);
|
response["CATEGORY_COUNT"] = arrayLen(categories);
|
||||||
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
||||||
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
|
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
|
||||||
|
|
|
||||||
|
|
@ -412,12 +412,29 @@
|
||||||
</cfloop>
|
</cfloop>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Get brand color for this business --->
|
||||||
|
<cfset brandColor = "">
|
||||||
|
<cftry>
|
||||||
|
<cfset qBrand = queryExecute(
|
||||||
|
"SELECT BusinessBrandColor FROM Businesses WHERE BusinessID = ?",
|
||||||
|
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
|
||||||
|
{ datasource = "payfrit" }
|
||||||
|
)>
|
||||||
|
<cfif qBrand.recordCount GT 0 AND len(trim(qBrand.BusinessBrandColor))>
|
||||||
|
<cfset brandColor = qBrand.BusinessBrandColor>
|
||||||
|
</cfif>
|
||||||
|
<cfcatch>
|
||||||
|
<!--- Column may not exist yet, ignore --->
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
||||||
<cfset apiAbort({
|
<cfset apiAbort({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"ERROR": "",
|
"ERROR": "",
|
||||||
"Items": rows,
|
"Items": rows,
|
||||||
"COUNT": arrayLen(rows),
|
"COUNT": arrayLen(rows),
|
||||||
"SCHEMA": newSchemaActive ? "unified" : "legacy"
|
"SCHEMA": newSchemaActive ? "unified" : "legacy",
|
||||||
|
"BRANDCOLOR": brandColor
|
||||||
})>
|
})>
|
||||||
|
|
||||||
<cfcatch>
|
<cfcatch>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
<cfscript>
|
<cfscript>
|
||||||
// Save menu data from the builder UI
|
// Save menu data from the builder UI (OPTIMIZED)
|
||||||
// Input: BusinessID, Menu (JSON structure)
|
// Input: BusinessID, Menu (JSON structure)
|
||||||
// Output: { OK: true }
|
// Output: { OK: true }
|
||||||
//
|
|
||||||
// Supports both old schema (Categories table) and new unified schema (Categories as Items)
|
|
||||||
|
|
||||||
response = { "OK": false };
|
response = { "OK": false };
|
||||||
|
|
||||||
// Log file for debugging
|
// Track which templates we've already saved options for (to avoid duplicate saves)
|
||||||
logFile = expandPath("./saveFromBuilder.log");
|
savedTemplates = {};
|
||||||
|
|
||||||
// Recursive function to save options/modifiers at any depth
|
// Recursive function to save options/modifiers at any depth
|
||||||
function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
function saveOptionsRecursive(options, parentID, businessID) {
|
||||||
if (!isArray(options) || arrayLen(options) == 0) return;
|
if (!isArray(options) || arrayLen(options) == 0) return;
|
||||||
|
|
||||||
var optSortOrder = 0;
|
var optSortOrder = 0;
|
||||||
|
|
@ -22,11 +20,8 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
||||||
var isDefault = (structKeyExists(opt, "isDefault") && opt.isDefault) ? 1 : 0;
|
var isDefault = (structKeyExists(opt, "isDefault") && opt.isDefault) ? 1 : 0;
|
||||||
var optionID = 0;
|
var optionID = 0;
|
||||||
|
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Option: #opt.name# (dbId=#optDbId#, parentID=#parentID#, isDefault=#isDefault#, reqSel=#requiresSelection#, maxSel=#maxSelections#)#chr(10)#");
|
|
||||||
|
|
||||||
if (optDbId > 0) {
|
if (optDbId > 0) {
|
||||||
optionID = optDbId;
|
optionID = optDbId;
|
||||||
// Update existing option
|
|
||||||
queryExecute("
|
queryExecute("
|
||||||
UPDATE Items
|
UPDATE Items
|
||||||
SET ItemName = :name,
|
SET ItemName = :name,
|
||||||
|
|
@ -48,16 +43,15 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
||||||
maxSelections: maxSelections
|
maxSelections: maxSelections
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Insert new option
|
|
||||||
queryExecute("
|
queryExecute("
|
||||||
INSERT INTO Items (
|
INSERT INTO Items (
|
||||||
ItemBusinessID, ItemParentItemID, ItemName, ItemPrice,
|
ItemBusinessID, ItemParentItemID, ItemName, ItemPrice,
|
||||||
ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn,
|
ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn,
|
||||||
ItemRequiresChildSelection, ItemMaxNumSelectionReq
|
ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemCategoryID
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:businessID, :parentID, :name, :price,
|
:businessID, :parentID, :name, :price,
|
||||||
:isDefault, :sortOrder, 1, NOW(),
|
:isDefault, :sortOrder, 1, NOW(),
|
||||||
:requiresSelection, :maxSelections
|
:requiresSelection, :maxSelections, 0
|
||||||
)
|
)
|
||||||
", {
|
", {
|
||||||
businessID: businessID,
|
businessID: businessID,
|
||||||
|
|
@ -74,9 +68,8 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
||||||
optionID = result.newID;
|
optionID = result.newID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively save nested options
|
|
||||||
if (structKeyExists(opt, "options") && isArray(opt.options) && arrayLen(opt.options) > 0) {
|
if (structKeyExists(opt, "options") && isArray(opt.options) && arrayLen(opt.options) > 0) {
|
||||||
saveOptionsRecursive(opt.options, optionID, businessID, logFile);
|
saveOptionsRecursive(opt.options, optionID, businessID);
|
||||||
}
|
}
|
||||||
|
|
||||||
optSortOrder++;
|
optSortOrder++;
|
||||||
|
|
@ -85,7 +78,6 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
requestBody = toString(getHttpRequestData().content);
|
requestBody = toString(getHttpRequestData().content);
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Request received, length: #len(requestBody)##chr(10)#");
|
|
||||||
|
|
||||||
if (!len(requestBody)) {
|
if (!len(requestBody)) {
|
||||||
throw("Request body is required");
|
throw("Request body is required");
|
||||||
|
|
@ -95,8 +87,6 @@ try {
|
||||||
businessID = val(jsonData.BusinessID ?: 0);
|
businessID = val(jsonData.BusinessID ?: 0);
|
||||||
menu = jsonData.Menu ?: {};
|
menu = jsonData.Menu ?: {};
|
||||||
|
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] BusinessID: #businessID#, Categories count: #arrayLen(menu.categories ?: [])##chr(10)#");
|
|
||||||
|
|
||||||
if (businessID == 0) {
|
if (businessID == 0) {
|
||||||
throw("BusinessID is required");
|
throw("BusinessID is required");
|
||||||
}
|
}
|
||||||
|
|
@ -105,297 +95,279 @@ try {
|
||||||
throw("Menu categories are required");
|
throw("Menu categories are required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log each category and its items
|
// Check if new schema is active
|
||||||
for (cat in menu.categories) {
|
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Category: #cat.name# (dbId=#structKeyExists(cat, 'dbId') ? cat.dbId : 'NEW'#), items: #arrayLen(cat.items ?: [])##chr(10)#");
|
|
||||||
if (structKeyExists(cat, "items") && isArray(cat.items)) {
|
|
||||||
for (item in cat.items) {
|
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] - Item: #item.name# (dbId=#structKeyExists(item, 'dbId') ? item.dbId : 'NEW'#) price=#item.price ?: 0##chr(10)#");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if new schema is active (ItemBusinessID column exists and has data)
|
|
||||||
newSchemaActive = false;
|
newSchemaActive = false;
|
||||||
try {
|
try {
|
||||||
qCheck = queryExecute("
|
qCheck = queryExecute("
|
||||||
SELECT COUNT(*) as cnt FROM Items
|
SELECT 1 FROM Items
|
||||||
WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0
|
WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0
|
||||||
|
LIMIT 1
|
||||||
", { businessID: businessID });
|
", { businessID: businessID });
|
||||||
newSchemaActive = (qCheck.cnt > 0);
|
newSchemaActive = (qCheck.recordCount > 0);
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
newSchemaActive = false;
|
newSchemaActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each category
|
// Wrap everything in a transaction for speed and consistency
|
||||||
for (cat in menu.categories) {
|
transaction {
|
||||||
categoryID = 0;
|
catSortOrder = 0;
|
||||||
categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0;
|
for (cat in menu.categories) {
|
||||||
|
categoryID = 0;
|
||||||
|
categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0;
|
||||||
|
|
||||||
if (newSchemaActive) {
|
if (newSchemaActive) {
|
||||||
// NEW SCHEMA: Categories are Items with ParentID=0 and Template=0
|
if (categoryDbId > 0) {
|
||||||
if (categoryDbId > 0) {
|
categoryID = categoryDbId;
|
||||||
categoryID = categoryDbId;
|
queryExecute("
|
||||||
// Update existing category Item
|
UPDATE Items
|
||||||
queryExecute("
|
SET ItemName = :name,
|
||||||
UPDATE Items
|
ItemSortOrder = :sortOrder
|
||||||
SET ItemName = :name,
|
WHERE ItemID = :categoryID
|
||||||
ItemSortOrder = :sortOrder
|
AND ItemBusinessID = :businessID
|
||||||
WHERE ItemID = :categoryID
|
", {
|
||||||
AND ItemBusinessID = :businessID
|
categoryID: categoryID,
|
||||||
", {
|
businessID: businessID,
|
||||||
categoryID: categoryID,
|
name: cat.name,
|
||||||
businessID: businessID,
|
sortOrder: catSortOrder
|
||||||
name: cat.name,
|
});
|
||||||
sortOrder: val(cat.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Insert new category as Item
|
|
||||||
queryExecute("
|
|
||||||
INSERT INTO Items (
|
|
||||||
ItemBusinessID, ItemName, ItemDescription,
|
|
||||||
ItemParentItemID, ItemPrice, ItemIsActive,
|
|
||||||
ItemSortOrder, ItemIsModifierTemplate, ItemAddedOn
|
|
||||||
) VALUES (
|
|
||||||
:businessID, :name, '',
|
|
||||||
0, 0, 1,
|
|
||||||
:sortOrder, 0, NOW()
|
|
||||||
)
|
|
||||||
", {
|
|
||||||
businessID: businessID,
|
|
||||||
name: cat.name,
|
|
||||||
sortOrder: val(cat.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
|
|
||||||
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
|
|
||||||
categoryID = result.newID;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// OLD SCHEMA: Use Categories table
|
|
||||||
if (categoryDbId > 0) {
|
|
||||||
categoryID = categoryDbId;
|
|
||||||
queryExecute("
|
|
||||||
UPDATE Categories
|
|
||||||
SET CategoryName = :name,
|
|
||||||
CategoryDescription = :description,
|
|
||||||
CategorySortOrder = :sortOrder
|
|
||||||
WHERE CategoryID = :categoryID
|
|
||||||
", {
|
|
||||||
categoryID: categoryID,
|
|
||||||
name: cat.name,
|
|
||||||
description: cat.description ?: "",
|
|
||||||
sortOrder: val(cat.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
queryExecute("
|
|
||||||
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategoryDescription, CategorySortOrder)
|
|
||||||
VALUES (:businessID, :name, :description, :sortOrder)
|
|
||||||
", {
|
|
||||||
businessID: businessID,
|
|
||||||
name: cat.name,
|
|
||||||
description: cat.description ?: "",
|
|
||||||
sortOrder: val(cat.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
|
|
||||||
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
|
|
||||||
categoryID = result.newID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process items in this category
|
|
||||||
if (structKeyExists(cat, "items") && isArray(cat.items)) {
|
|
||||||
for (item in cat.items) {
|
|
||||||
itemID = 0;
|
|
||||||
itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0;
|
|
||||||
|
|
||||||
if (itemDbId > 0) {
|
|
||||||
itemID = itemDbId;
|
|
||||||
|
|
||||||
if (newSchemaActive) {
|
|
||||||
// Update existing item - set parent to category Item
|
|
||||||
queryExecute("
|
|
||||||
UPDATE Items
|
|
||||||
SET ItemName = :name,
|
|
||||||
ItemDescription = :description,
|
|
||||||
ItemPrice = :price,
|
|
||||||
ItemParentItemID = :categoryID,
|
|
||||||
ItemSortOrder = :sortOrder
|
|
||||||
WHERE ItemID = :itemID
|
|
||||||
", {
|
|
||||||
itemID: itemID,
|
|
||||||
name: item.name,
|
|
||||||
description: item.description ?: "",
|
|
||||||
price: val(item.price ?: 0),
|
|
||||||
categoryID: categoryID,
|
|
||||||
sortOrder: val(item.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Update existing item - old schema with CategoryID
|
|
||||||
queryExecute("
|
|
||||||
UPDATE Items
|
|
||||||
SET ItemName = :name,
|
|
||||||
ItemDescription = :description,
|
|
||||||
ItemPrice = :price,
|
|
||||||
ItemCategoryID = :categoryID,
|
|
||||||
ItemSortOrder = :sortOrder
|
|
||||||
WHERE ItemID = :itemID
|
|
||||||
", {
|
|
||||||
itemID: itemID,
|
|
||||||
name: item.name,
|
|
||||||
description: item.description ?: "",
|
|
||||||
price: val(item.price ?: 0),
|
|
||||||
categoryID: categoryID,
|
|
||||||
sortOrder: val(item.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Insert new item
|
queryExecute("
|
||||||
if (newSchemaActive) {
|
INSERT INTO Items (
|
||||||
queryExecute("
|
ItemBusinessID, ItemName, ItemDescription,
|
||||||
INSERT INTO Items (
|
ItemParentItemID, ItemPrice, ItemIsActive,
|
||||||
ItemBusinessID, ItemParentItemID, ItemName, ItemDescription,
|
ItemSortOrder, ItemAddedOn, ItemCategoryID
|
||||||
ItemPrice, ItemSortOrder, ItemIsActive, ItemAddedOn
|
) VALUES (
|
||||||
) VALUES (
|
:businessID, :name, '',
|
||||||
:businessID, :categoryID, :name, :description,
|
0, 0, 1,
|
||||||
:price, :sortOrder, 1, NOW()
|
:sortOrder, NOW(), 0
|
||||||
)
|
)
|
||||||
", {
|
", {
|
||||||
businessID: businessID,
|
businessID: businessID,
|
||||||
categoryID: categoryID,
|
name: cat.name,
|
||||||
name: item.name,
|
sortOrder: catSortOrder
|
||||||
description: item.description ?: "",
|
});
|
||||||
price: val(item.price ?: 0),
|
|
||||||
sortOrder: val(item.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
queryExecute("
|
|
||||||
INSERT INTO Items (
|
|
||||||
ItemBusinessID, ItemCategoryID, ItemName, ItemDescription,
|
|
||||||
ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID
|
|
||||||
) VALUES (
|
|
||||||
:businessID, :categoryID, :name, :description,
|
|
||||||
:price, :sortOrder, 1, 0
|
|
||||||
)
|
|
||||||
", {
|
|
||||||
businessID: businessID,
|
|
||||||
categoryID: categoryID,
|
|
||||||
name: item.name,
|
|
||||||
description: item.description ?: "",
|
|
||||||
price: val(item.price ?: 0),
|
|
||||||
sortOrder: val(item.sortOrder ?: 0)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
|
||||||
itemID = result.newID;
|
categoryID = result.newID;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Handle template links for modifiers
|
if (categoryDbId > 0) {
|
||||||
if (structKeyExists(item, "modifiers") && isArray(item.modifiers)) {
|
categoryID = categoryDbId;
|
||||||
// Clear existing template links for this item
|
|
||||||
queryExecute("
|
queryExecute("
|
||||||
DELETE FROM ItemTemplateLinks WHERE ItemID = :itemID
|
UPDATE Categories
|
||||||
", { itemID: itemID });
|
SET CategoryName = :name,
|
||||||
|
CategorySortOrder = :sortOrder
|
||||||
|
WHERE CategoryID = :categoryID
|
||||||
|
", {
|
||||||
|
categoryID: categoryID,
|
||||||
|
name: cat.name,
|
||||||
|
sortOrder: catSortOrder
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategorySortOrder, CategoryAddedOn)
|
||||||
|
VALUES (:businessID, :name, :sortOrder, NOW())
|
||||||
|
", {
|
||||||
|
businessID: businessID,
|
||||||
|
name: cat.name,
|
||||||
|
sortOrder: catSortOrder
|
||||||
|
});
|
||||||
|
|
||||||
modSortOrder = 0;
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
|
||||||
for (mod in item.modifiers) {
|
categoryID = result.newID;
|
||||||
modDbId = structKeyExists(mod, "dbId") ? val(mod.dbId) : 0;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get selection rules (for modifier groups)
|
// Process items
|
||||||
requiresSelection = (structKeyExists(mod, "requiresSelection") && mod.requiresSelection) ? 1 : 0;
|
if (structKeyExists(cat, "items") && isArray(cat.items)) {
|
||||||
maxSelections = structKeyExists(mod, "maxSelections") ? val(mod.maxSelections) : 0;
|
itemSortOrder = 0;
|
||||||
|
for (item in cat.items) {
|
||||||
|
itemID = 0;
|
||||||
|
itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0;
|
||||||
|
|
||||||
// Check if this is a template reference
|
if (itemDbId > 0) {
|
||||||
if (structKeyExists(mod, "isTemplate") && mod.isTemplate && modDbId > 0) {
|
itemID = itemDbId;
|
||||||
// Create template link
|
|
||||||
queryExecute("
|
|
||||||
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
|
|
||||||
VALUES (:itemID, :templateID, :sortOrder)
|
|
||||||
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
|
|
||||||
", {
|
|
||||||
itemID: itemID,
|
|
||||||
templateID: modDbId,
|
|
||||||
sortOrder: modSortOrder
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also update the template's selection rules
|
if (newSchemaActive) {
|
||||||
queryExecute("
|
|
||||||
UPDATE Items
|
|
||||||
SET ItemRequiresChildSelection = :requiresSelection,
|
|
||||||
ItemMaxNumSelectionReq = :maxSelections
|
|
||||||
WHERE ItemID = :modID
|
|
||||||
", {
|
|
||||||
modID: modDbId,
|
|
||||||
requiresSelection: requiresSelection,
|
|
||||||
maxSelections: maxSelections
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save the template's options (children) recursively
|
|
||||||
if (structKeyExists(mod, "options") && isArray(mod.options)) {
|
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Template: #mod.name# (dbId=#modDbId#) has #arrayLen(mod.options)# options#chr(10)#");
|
|
||||||
saveOptionsRecursive(mod.options, modDbId, businessID, logFile);
|
|
||||||
}
|
|
||||||
} else if (modDbId > 0) {
|
|
||||||
// Update existing direct modifier
|
|
||||||
queryExecute("
|
queryExecute("
|
||||||
UPDATE Items
|
UPDATE Items
|
||||||
SET ItemName = :name,
|
SET ItemName = :name,
|
||||||
|
ItemDescription = :description,
|
||||||
ItemPrice = :price,
|
ItemPrice = :price,
|
||||||
ItemIsCheckedByDefault = :isDefault,
|
ItemParentItemID = :categoryID,
|
||||||
ItemSortOrder = :sortOrder,
|
ItemSortOrder = :sortOrder
|
||||||
ItemRequiresChildSelection = :requiresSelection,
|
WHERE ItemID = :itemID
|
||||||
ItemMaxNumSelectionReq = :maxSelections
|
|
||||||
WHERE ItemID = :modID
|
|
||||||
", {
|
", {
|
||||||
modID: modDbId,
|
itemID: itemID,
|
||||||
name: mod.name,
|
name: item.name,
|
||||||
price: val(mod.price ?: 0),
|
description: item.description ?: "",
|
||||||
isDefault: (mod.isDefault ?: false) ? 1 : 0,
|
price: val(item.price ?: 0),
|
||||||
sortOrder: modSortOrder,
|
categoryID: categoryID,
|
||||||
requiresSelection: requiresSelection,
|
sortOrder: itemSortOrder
|
||||||
maxSelections: maxSelections
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save nested options recursively
|
|
||||||
if (structKeyExists(mod, "options") && isArray(mod.options)) {
|
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Modifier: #mod.name# (dbId=#modDbId#) has #arrayLen(mod.options)# options#chr(10)#");
|
|
||||||
saveOptionsRecursive(mod.options, modDbId, businessID, logFile);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Insert new direct modifier (non-template)
|
queryExecute("
|
||||||
|
UPDATE Items
|
||||||
|
SET ItemName = :name,
|
||||||
|
ItemDescription = :description,
|
||||||
|
ItemPrice = :price,
|
||||||
|
ItemCategoryID = :categoryID,
|
||||||
|
ItemSortOrder = :sortOrder
|
||||||
|
WHERE ItemID = :itemID
|
||||||
|
", {
|
||||||
|
itemID: itemID,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description ?: "",
|
||||||
|
price: val(item.price ?: 0),
|
||||||
|
categoryID: categoryID,
|
||||||
|
sortOrder: itemSortOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (newSchemaActive) {
|
||||||
queryExecute("
|
queryExecute("
|
||||||
INSERT INTO Items (
|
INSERT INTO Items (
|
||||||
ItemBusinessID, ItemParentItemID, ItemName, ItemPrice,
|
ItemBusinessID, ItemParentItemID, ItemName, ItemDescription,
|
||||||
ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn,
|
ItemPrice, ItemSortOrder, ItemIsActive, ItemAddedOn, ItemCategoryID
|
||||||
ItemRequiresChildSelection, ItemMaxNumSelectionReq
|
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:businessID, :parentID, :name, :price,
|
:businessID, :categoryID, :name, :description,
|
||||||
:isDefault, :sortOrder, 1, NOW(),
|
:price, :sortOrder, 1, NOW(), 0
|
||||||
:requiresSelection, :maxSelections
|
|
||||||
)
|
)
|
||||||
", {
|
", {
|
||||||
businessID: businessID,
|
businessID: businessID,
|
||||||
parentID: itemID,
|
categoryID: categoryID,
|
||||||
name: mod.name,
|
name: item.name,
|
||||||
price: val(mod.price ?: 0),
|
description: item.description ?: "",
|
||||||
isDefault: (mod.isDefault ?: false) ? 1 : 0,
|
price: val(item.price ?: 0),
|
||||||
sortOrder: modSortOrder,
|
sortOrder: itemSortOrder
|
||||||
requiresSelection: requiresSelection,
|
});
|
||||||
maxSelections: maxSelections
|
} else {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Items (
|
||||||
|
ItemBusinessID, ItemCategoryID, ItemName, ItemDescription,
|
||||||
|
ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID, ItemAddedOn
|
||||||
|
) VALUES (
|
||||||
|
:businessID, :categoryID, :name, :description,
|
||||||
|
:price, :sortOrder, 1, 0, NOW()
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
businessID: businessID,
|
||||||
|
categoryID: categoryID,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description ?: "",
|
||||||
|
price: val(item.price ?: 0),
|
||||||
|
sortOrder: itemSortOrder
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the new modifier's ID and save nested options
|
|
||||||
modResult = queryExecute("SELECT LAST_INSERT_ID() as newModID");
|
|
||||||
newModID = modResult.newModID;
|
|
||||||
if (structKeyExists(mod, "options") && isArray(mod.options)) {
|
|
||||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] New Modifier: #mod.name# (newId=#newModID#) has #arrayLen(mod.options)# options#chr(10)#");
|
|
||||||
saveOptionsRecursive(mod.options, newModID, businessID, logFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
modSortOrder++;
|
|
||||||
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
|
||||||
|
itemID = result.newID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle modifiers
|
||||||
|
if (structKeyExists(item, "modifiers") && isArray(item.modifiers) && arrayLen(item.modifiers) > 0) {
|
||||||
|
// Clear existing template links for this item
|
||||||
|
queryExecute("DELETE FROM ItemTemplateLinks WHERE ItemID = :itemID", { itemID: itemID });
|
||||||
|
|
||||||
|
modSortOrder = 0;
|
||||||
|
for (mod in item.modifiers) {
|
||||||
|
modDbId = structKeyExists(mod, "dbId") ? val(mod.dbId) : 0;
|
||||||
|
requiresSelection = (structKeyExists(mod, "requiresSelection") && mod.requiresSelection) ? 1 : 0;
|
||||||
|
maxSelections = structKeyExists(mod, "maxSelections") ? val(mod.maxSelections) : 0;
|
||||||
|
|
||||||
|
if (structKeyExists(mod, "isTemplate") && mod.isTemplate && modDbId > 0) {
|
||||||
|
// This is a template reference - create link
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
|
||||||
|
VALUES (:itemID, :templateID, :sortOrder)
|
||||||
|
ON DUPLICATE KEY UPDATE SortOrder = :sortOrder
|
||||||
|
", {
|
||||||
|
itemID: itemID,
|
||||||
|
templateID: modDbId,
|
||||||
|
sortOrder: modSortOrder
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update template's selection rules
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Items
|
||||||
|
SET ItemRequiresChildSelection = :requiresSelection,
|
||||||
|
ItemMaxNumSelectionReq = :maxSelections
|
||||||
|
WHERE ItemID = :modID
|
||||||
|
", {
|
||||||
|
modID: modDbId,
|
||||||
|
requiresSelection: requiresSelection,
|
||||||
|
maxSelections: maxSelections
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only save template options ONCE (first time we encounter this template)
|
||||||
|
if (!structKeyExists(savedTemplates, modDbId)) {
|
||||||
|
savedTemplates[modDbId] = true;
|
||||||
|
if (structKeyExists(mod, "options") && isArray(mod.options)) {
|
||||||
|
saveOptionsRecursive(mod.options, modDbId, businessID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (modDbId > 0) {
|
||||||
|
// Direct modifier (not a template) - update it
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Items
|
||||||
|
SET ItemName = :name,
|
||||||
|
ItemPrice = :price,
|
||||||
|
ItemIsCheckedByDefault = :isDefault,
|
||||||
|
ItemSortOrder = :sortOrder,
|
||||||
|
ItemRequiresChildSelection = :requiresSelection,
|
||||||
|
ItemMaxNumSelectionReq = :maxSelections,
|
||||||
|
ItemParentItemID = :parentID
|
||||||
|
WHERE ItemID = :modID
|
||||||
|
", {
|
||||||
|
modID: modDbId,
|
||||||
|
parentID: itemID,
|
||||||
|
name: mod.name,
|
||||||
|
price: val(mod.price ?: 0),
|
||||||
|
isDefault: (mod.isDefault ?: false) ? 1 : 0,
|
||||||
|
sortOrder: modSortOrder,
|
||||||
|
requiresSelection: requiresSelection,
|
||||||
|
maxSelections: maxSelections
|
||||||
|
});
|
||||||
|
|
||||||
|
if (structKeyExists(mod, "options") && isArray(mod.options)) {
|
||||||
|
saveOptionsRecursive(mod.options, modDbId, businessID);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New direct modifier - insert it
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Items (
|
||||||
|
ItemBusinessID, ItemParentItemID, ItemName, ItemPrice,
|
||||||
|
ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn,
|
||||||
|
ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemCategoryID
|
||||||
|
) VALUES (
|
||||||
|
:businessID, :parentID, :name, :price,
|
||||||
|
:isDefault, :sortOrder, 1, NOW(),
|
||||||
|
:requiresSelection, :maxSelections, 0
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
businessID: businessID,
|
||||||
|
parentID: itemID,
|
||||||
|
name: mod.name,
|
||||||
|
price: val(mod.price ?: 0),
|
||||||
|
isDefault: (mod.isDefault ?: false) ? 1 : 0,
|
||||||
|
sortOrder: modSortOrder,
|
||||||
|
requiresSelection: requiresSelection,
|
||||||
|
maxSelections: maxSelections
|
||||||
|
});
|
||||||
|
|
||||||
|
modResult = queryExecute("SELECT LAST_INSERT_ID() as newModID");
|
||||||
|
newModID = modResult.newModID;
|
||||||
|
if (structKeyExists(mod, "options") && isArray(mod.options)) {
|
||||||
|
saveOptionsRecursive(mod.options, newModID, businessID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modSortOrder++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemSortOrder++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catSortOrder++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
6748
api/menu/saveFromBuilder.log
Normal file
6748
api/menu/saveFromBuilder.log
Normal file
File diff suppressed because it is too large
Load diff
100
api/menu/uploadHeader.cfm
Normal file
100
api/menu/uploadHeader.cfm
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfset headersDir = "/var/www/biz.payfrit.com/uploads/headers">
|
||||||
|
<cfscript>
|
||||||
|
function apiAbort(payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get BusinessID from form, request scope, or header
|
||||||
|
bizId = 0;
|
||||||
|
if (structKeyExists(form, "BusinessID") && isNumeric(form.BusinessID) && form.BusinessID GT 0) {
|
||||||
|
bizId = int(form.BusinessID);
|
||||||
|
} else if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
|
||||||
|
bizId = int(request.BusinessID);
|
||||||
|
} else {
|
||||||
|
httpHeaders = getHttpRequestData().headers;
|
||||||
|
if (structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"]) && httpHeaders["X-Business-ID"] GT 0) {
|
||||||
|
bizId = int(httpHeaders["X-Business-ID"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bizId LTE 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<!--- Check if file was uploaded --->
|
||||||
|
<cfif NOT structKeyExists(form, "header") OR form.header EQ "">
|
||||||
|
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "no_file", "MESSAGE": "No file was uploaded" })#</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Upload the file to temp location first --->
|
||||||
|
<cffile action="UPLOAD" filefield="header" destination="#headersDir#/" nameconflict="MAKEUNIQUE" mode="755" result="uploadResult">
|
||||||
|
|
||||||
|
<!--- Validate file type --->
|
||||||
|
<cfset allowedExtensions = "jpg,jpeg,gif,png,webp">
|
||||||
|
<cfif NOT listFindNoCase(allowedExtensions, uploadResult.ClientFileExt)>
|
||||||
|
<cffile action="DELETE" file="#headersDir#/#uploadResult.ServerFile#">
|
||||||
|
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp)" })#</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Get image info --->
|
||||||
|
<cfimage source="#headersDir#/#uploadResult.ServerFile#" action="info" structName="imageInfo">
|
||||||
|
|
||||||
|
<!--- Resize if needed (max 1200px width for headers) --->
|
||||||
|
<cfif imageInfo.width GT 1200>
|
||||||
|
<cfimage action="resize" source="#headersDir#/#uploadResult.ServerFile#" width="1200" destination="#headersDir#/#uploadResult.ServerFile#" overwrite="true">
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Delete old header if exists --->
|
||||||
|
<cfquery name="qOldHeader" datasource="payfrit">
|
||||||
|
SELECT BusinessHeaderImageExtension
|
||||||
|
FROM Businesses
|
||||||
|
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfif qOldHeader.recordCount GT 0 AND len(trim(qOldHeader.BusinessHeaderImageExtension)) GT 0>
|
||||||
|
<cfset oldFile = "#headersDir#/#bizId#.#qOldHeader.BusinessHeaderImageExtension#">
|
||||||
|
<cfif fileExists(oldFile)>
|
||||||
|
<cftry>
|
||||||
|
<cffile action="DELETE" file="#oldFile#">
|
||||||
|
<cfcatch></cfcatch>
|
||||||
|
</cftry>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Rename to BusinessID.ext --->
|
||||||
|
<cffile action="RENAME" source="#headersDir#/#uploadResult.ServerFile#" destination="#headersDir#/#bizId#.#uploadResult.ClientFileExt#" mode="755">
|
||||||
|
|
||||||
|
<!--- Update database --->
|
||||||
|
<cfquery datasource="payfrit">
|
||||||
|
UPDATE Businesses
|
||||||
|
SET BusinessHeaderImageExtension = <cfqueryparam cfsqltype="cf_sql_varchar" value="#uploadResult.ClientFileExt#">
|
||||||
|
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<!--- Return success with image URL --->
|
||||||
|
<cfoutput>#serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"ERROR": "",
|
||||||
|
"MESSAGE": "Header uploaded successfully",
|
||||||
|
"HEADERURL": "/uploads/headers/#bizId#.#uploadResult.ClientFileExt#",
|
||||||
|
"WIDTH": imageInfo.width,
|
||||||
|
"HEIGHT": imageInfo.height
|
||||||
|
})#</cfoutput>
|
||||||
|
|
||||||
|
<cfcatch type="any">
|
||||||
|
<cfheader statuscode="200" statustext="OK">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfoutput>#serializeJSON({ "OK": false, "ERROR": "server_error", "MESSAGE": cfcatch.message, "DETAIL": cfcatch.detail })#</cfoutput>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
71
api/servicepoints/delete.cfm
Normal file
71
api/servicepoints/delete.cfm
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
function apiAbort(obj) {
|
||||||
|
writeOutput(serializeJSON(obj));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
raw = toString(getHttpRequestData().content);
|
||||||
|
if (isNull(raw) || len(trim(raw)) EQ 0) return {};
|
||||||
|
try {
|
||||||
|
parsed = deserializeJSON(raw);
|
||||||
|
} catch(any e) {
|
||||||
|
apiAbort({ OK=false, ERROR="bad_json", MESSAGE="Invalid JSON body" });
|
||||||
|
}
|
||||||
|
if (!isStruct(parsed)) return {};
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
httpHeaders = getHttpRequestData().headers;
|
||||||
|
|
||||||
|
// Get BusinessID from: session > body > X-Business-ID header
|
||||||
|
bizId = 0;
|
||||||
|
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
|
||||||
|
bizId = int(request.BusinessID);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID) && data.BusinessID GT 0) {
|
||||||
|
bizId = int(data.BusinessID);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"]) && httpHeaders["X-Business-ID"] GT 0) {
|
||||||
|
bizId = int(httpHeaders["X-Business-ID"]);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!structKeyExists(data, "ServicePointID") || !isNumeric(data.ServicePointID) || int(data.ServicePointID) LTE 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="missing_servicepoint_id", MESSAGE="ServicePointID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
servicePointId = int(data.ServicePointID);
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cfquery datasource="payfrit">
|
||||||
|
UPDATE ServicePoints
|
||||||
|
SET ServicePointIsActive = 0
|
||||||
|
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#servicePointId#">
|
||||||
|
AND ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<!--- confirm --->
|
||||||
|
<cfquery name="qCheck" datasource="payfrit">
|
||||||
|
SELECT ServicePointID, ServicePointIsActive
|
||||||
|
FROM ServicePoints
|
||||||
|
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#servicePointId#">
|
||||||
|
AND ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
|
LIMIT 1
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfif qCheck.recordCount EQ 0>
|
||||||
|
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found" })#</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfoutput>#serializeJSON({ OK=true, ERROR="", ServicePointID=servicePointId })#</cfoutput>
|
||||||
68
api/servicepoints/get.cfm
Normal file
68
api/servicepoints/get.cfm
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
function apiAbort(obj) {
|
||||||
|
writeOutput(serializeJSON(obj));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
raw = toString(getHttpRequestData().content);
|
||||||
|
if (isNull(raw) || len(trim(raw)) EQ 0) return {};
|
||||||
|
try {
|
||||||
|
parsed = deserializeJSON(raw);
|
||||||
|
} catch(any e) {
|
||||||
|
apiAbort({ OK=false, ERROR="bad_json", MESSAGE="Invalid JSON body" });
|
||||||
|
}
|
||||||
|
if (!isStruct(parsed)) return {};
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!structKeyExists(data, "ServicePointID") || !isNumeric(data.ServicePointID) || int(data.ServicePointID) LTE 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="missing_servicepoint_id", MESSAGE="ServicePointID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
servicePointId = int(data.ServicePointID);
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cfquery name="q" datasource="payfrit">
|
||||||
|
SELECT
|
||||||
|
ServicePointID,
|
||||||
|
ServicePointBusinessID,
|
||||||
|
ServicePointName,
|
||||||
|
ServicePointCode,
|
||||||
|
ServicePointTypeID,
|
||||||
|
ServicePointIsActive,
|
||||||
|
SortOrder
|
||||||
|
FROM ServicePoints
|
||||||
|
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#servicePointId#">
|
||||||
|
AND ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
LIMIT 1
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfif q.recordCount EQ 0>
|
||||||
|
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found" })#</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset servicePoint = {
|
||||||
|
"ServicePointID" = q.ServicePointID,
|
||||||
|
"BusinessID" = q.ServicePointBusinessID,
|
||||||
|
"ServicePointName" = q.ServicePointName,
|
||||||
|
"ServicePointCode" = q.ServicePointCode,
|
||||||
|
"ServicePointTypeID"= q.ServicePointTypeID,
|
||||||
|
"IsActive" = q.ServicePointIsActive,
|
||||||
|
"SortOrder" = q.SortOrder
|
||||||
|
}>
|
||||||
|
|
||||||
|
<cfoutput>#serializeJSON({ OK=true, ERROR="", SERVICEPOINT=servicePoint })#</cfoutput>
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<cfsetting showdebugoutput="false">
|
<cfsetting showdebugoutput="false">
|
||||||
<cfsetting enablecfoutputonly="true">
|
<cfsetting enablecfoutputonly="true">
|
||||||
<cfcontent type="application/json; charset=utf-8">
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
<cfscript>
|
<cfscript>
|
||||||
function apiAbort(payload) {
|
function apiAbort(payload) {
|
||||||
|
|
@ -8,129 +10,85 @@ function apiAbort(payload) {
|
||||||
abort;
|
abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve BusinessID tolerant MVP way
|
// Read JSON body once
|
||||||
bizId = 0;
|
data = {};
|
||||||
|
try {
|
||||||
|
raw = toString(getHttpRequestData().content);
|
||||||
|
if (len(trim(raw))) {
|
||||||
|
data = deserializeJSON(raw);
|
||||||
|
if (!isStruct(data)) data = {};
|
||||||
|
}
|
||||||
|
} catch (any e) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
|
||||||
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID)) {
|
httpHeaders = getHttpRequestData().headers;
|
||||||
|
|
||||||
|
// Get BusinessID from: session > body > X-Business-ID header > URL
|
||||||
|
bizId = 0;
|
||||||
|
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID) && request.BusinessID GT 0) {
|
||||||
bizId = int(request.BusinessID);
|
bizId = int(request.BusinessID);
|
||||||
}
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID) && data.BusinessID GT 0) {
|
||||||
if (bizId LTE 0) {
|
bizId = int(data.BusinessID);
|
||||||
try {
|
|
||||||
raw = toString(getHttpRequestData().content);
|
|
||||||
if (len(trim(raw))) {
|
|
||||||
body = deserializeJSON(raw);
|
|
||||||
if (isStruct(body) && structKeyExists(body, "BusinessID") && isNumeric(body.BusinessID)) {
|
|
||||||
bizId = int(body.BusinessID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (any e) {}
|
|
||||||
}
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"]) && httpHeaders["X-Business-ID"] GT 0) {
|
||||||
if (bizId LTE 0 && structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID)) {
|
bizId = int(httpHeaders["X-Business-ID"]);
|
||||||
|
}
|
||||||
|
if (bizId LTE 0 && structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID) && url.BusinessID GT 0) {
|
||||||
bizId = int(url.BusinessID);
|
bizId = int(url.BusinessID);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bizId LTE 0) {
|
if (bizId LTE 0) {
|
||||||
apiAbort({ "OK": false, "ERROR": "missing_businessid", "DETAIL": "" });
|
apiAbort({ "OK": false, "ERROR": "missing_businessid" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Default behavior: only active service points unless onlyActive is explicitly false/0
|
||||||
// Detect the correct business FK column in ServicePoints
|
onlyActive = true;
|
||||||
qCols = queryExecute(
|
if (structKeyExists(data, "onlyActive")) {
|
||||||
"
|
if (isBoolean(data.onlyActive)) {
|
||||||
SELECT COLUMN_NAME
|
onlyActive = data.onlyActive;
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
} else if (isNumeric(data.onlyActive)) {
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
onlyActive = (int(data.onlyActive) EQ 1);
|
||||||
AND TABLE_NAME = 'ServicePoints'
|
} else if (isSimpleValue(data.onlyActive)) {
|
||||||
",
|
onlyActive = (lcase(trim(toString(data.onlyActive))) EQ "true");
|
||||||
[],
|
|
||||||
{ datasource: "payfrit" }
|
|
||||||
);
|
|
||||||
|
|
||||||
cols = [];
|
|
||||||
for (r in qCols) arrayAppend(cols, r.COLUMN_NAME);
|
|
||||||
|
|
||||||
// Candidates in preferred order (add more if needed)
|
|
||||||
candidates = [
|
|
||||||
"BusinessID",
|
|
||||||
"BusinessId",
|
|
||||||
"ServicePointBusinessID",
|
|
||||||
"ServicePointsBusinessID",
|
|
||||||
"Business_ID",
|
|
||||||
"Business",
|
|
||||||
"BusinessFk",
|
|
||||||
"BusinessFK"
|
|
||||||
];
|
|
||||||
|
|
||||||
bizCol = "";
|
|
||||||
// First: exact candidate match
|
|
||||||
for (c in candidates) {
|
|
||||||
for (colName in cols) {
|
|
||||||
if (lcase(colName) EQ lcase(c)) {
|
|
||||||
bizCol = colName;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (len(bizCol)) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second: heuristic: any column containing "business" and "id"
|
|
||||||
if (!len(bizCol)) {
|
|
||||||
for (colName in cols) {
|
|
||||||
if (findNoCase("business", colName) && findNoCase("id", colName)) {
|
|
||||||
bizCol = colName;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!len(bizCol)) {
|
|
||||||
apiAbort({
|
|
||||||
"OK": false,
|
|
||||||
"ERROR": "schema_mismatch",
|
|
||||||
"DETAIL": "Could not find a BusinessID-like column in ServicePoints. Available columns: " & arrayToList(cols, ", ")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build SQL using detected column name (safe because it comes from INFORMATION_SCHEMA)
|
|
||||||
sql = "
|
|
||||||
SELECT
|
|
||||||
ServicePointID,
|
|
||||||
ServicePointName
|
|
||||||
FROM ServicePoints
|
|
||||||
WHERE #bizCol# = ?
|
|
||||||
ORDER BY ServicePointName
|
|
||||||
";
|
|
||||||
|
|
||||||
q = queryExecute(
|
|
||||||
sql,
|
|
||||||
[ { value: bizId, cfsqltype: "cf_sql_integer" } ],
|
|
||||||
{ datasource: "payfrit" }
|
|
||||||
);
|
|
||||||
|
|
||||||
servicePoints = [];
|
|
||||||
for (row in q) {
|
|
||||||
arrayAppend(servicePoints, {
|
|
||||||
"ServicePointID": row.ServicePointID,
|
|
||||||
"ServicePointName": row.ServicePointName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
writeOutput(serializeJSON({
|
|
||||||
"OK": true,
|
|
||||||
"ERROR": "",
|
|
||||||
"DETAIL": "",
|
|
||||||
"BusinessID": bizId,
|
|
||||||
"BusinessColumn": bizCol,
|
|
||||||
"COUNT": arrayLen(servicePoints),
|
|
||||||
"ServicePoints": servicePoints
|
|
||||||
}));
|
|
||||||
} catch (any e) {
|
|
||||||
apiAbort({
|
|
||||||
"OK": false,
|
|
||||||
"ERROR": "db_error",
|
|
||||||
"DETAIL": e.message
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</cfscript>
|
</cfscript>
|
||||||
|
|
||||||
|
<cfquery name="q" datasource="payfrit">
|
||||||
|
SELECT
|
||||||
|
ServicePointID,
|
||||||
|
ServicePointName,
|
||||||
|
ServicePointTypeID,
|
||||||
|
ServicePointCode,
|
||||||
|
ServicePointDescription,
|
||||||
|
ServicePointSortOrder,
|
||||||
|
ServicePointIsActive
|
||||||
|
FROM ServicePoints
|
||||||
|
WHERE ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||||
|
<cfif onlyActive>
|
||||||
|
AND ServicePointIsActive = 1
|
||||||
|
</cfif>
|
||||||
|
ORDER BY ServicePointSortOrder, ServicePointName
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfset servicePoints = []>
|
||||||
|
<cfloop query="q">
|
||||||
|
<cfset arrayAppend(servicePoints, {
|
||||||
|
"ServicePointID" = q.ServicePointID,
|
||||||
|
"ServicePointName" = q.ServicePointName,
|
||||||
|
"ServicePointTypeID" = q.ServicePointTypeID,
|
||||||
|
"ServicePointCode" = q.ServicePointCode,
|
||||||
|
"Description" = q.ServicePointDescription,
|
||||||
|
"SortOrder" = q.ServicePointSortOrder,
|
||||||
|
"IsActive" = q.ServicePointIsActive
|
||||||
|
})>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
|
<cfoutput>#serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"ERROR": "",
|
||||||
|
"BusinessID": bizId,
|
||||||
|
"COUNT": arrayLen(servicePoints),
|
||||||
|
"SERVICEPOINTS": servicePoints
|
||||||
|
})#</cfoutput>
|
||||||
|
|
|
||||||
146
api/servicepoints/save.cfm
Normal file
146
api/servicepoints/save.cfm
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfscript>
|
||||||
|
function apiAbort(obj) {
|
||||||
|
writeOutput(serializeJSON(obj));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
raw = toString(getHttpRequestData().content);
|
||||||
|
if (isNull(raw) || len(trim(raw)) EQ 0) return {};
|
||||||
|
try {
|
||||||
|
parsed = deserializeJSON(raw);
|
||||||
|
} catch(any e) {
|
||||||
|
apiAbort({ OK=false, ERROR="bad_json", MESSAGE="Invalid JSON body" });
|
||||||
|
}
|
||||||
|
if (!isStruct(parsed)) return {};
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normStr(v) {
|
||||||
|
if (isNull(v)) return "";
|
||||||
|
return trim(toString(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
if (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!structKeyExists(data, "ServicePointName") || len(normStr(data.ServicePointName)) EQ 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="missing_name", MESSAGE="ServicePointName is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
servicePointId = 0;
|
||||||
|
if (structKeyExists(data, "ServicePointID") && isNumeric(data.ServicePointID) && int(data.ServicePointID) GT 0) {
|
||||||
|
servicePointId = int(data.ServicePointID);
|
||||||
|
}
|
||||||
|
|
||||||
|
spName = normStr(data.ServicePointName);
|
||||||
|
spCode = structKeyExists(data, "ServicePointCode") ? normStr(data.ServicePointCode) : "";
|
||||||
|
spTypeID = structKeyExists(data, "ServicePointTypeID") && isNumeric(data.ServicePointTypeID) ? int(data.ServicePointTypeID) : 1;
|
||||||
|
sortOrder = structKeyExists(data, "ServicePointSortOrder") && isNumeric(data.ServicePointSortOrder) ? int(data.ServicePointSortOrder) : 0;
|
||||||
|
|
||||||
|
isActive = 1;
|
||||||
|
if (structKeyExists(data, "IsActive")) {
|
||||||
|
if (isBoolean(data.IsActive)) isActive = (data.IsActive ? 1 : 0);
|
||||||
|
else if (isNumeric(data.IsActive)) isActive = int(data.IsActive);
|
||||||
|
else if (isSimpleValue(data.IsActive)) isActive = (lcase(trim(toString(data.IsActive))) EQ "true" ? 1 : 0);
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cfif servicePointId GT 0>
|
||||||
|
<!--- Update, scoped to this business --->
|
||||||
|
<cfquery datasource="payfrit">
|
||||||
|
UPDATE ServicePoints
|
||||||
|
SET
|
||||||
|
ServicePointName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#spName#">,
|
||||||
|
ServicePointCode = <cfqueryparam cfsqltype="cf_sql_varchar" value="#spCode#" null="#(len(spCode) EQ 0)#">,
|
||||||
|
ServicePointTypeID = <cfqueryparam cfsqltype="cf_sql_integer" value="#spTypeID#">,
|
||||||
|
ServicePointIsActive = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">,
|
||||||
|
ServicePointSortOrder = <cfqueryparam cfsqltype="cf_sql_integer" value="#sortOrder#">
|
||||||
|
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#servicePointId#">
|
||||||
|
AND ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<!--- confirm it exists/belongs to business --->
|
||||||
|
<cfquery name="qCheck" datasource="payfrit">
|
||||||
|
SELECT ServicePointID
|
||||||
|
FROM ServicePoints
|
||||||
|
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#servicePointId#">
|
||||||
|
AND ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
LIMIT 1
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfif qCheck.recordCount EQ 0>
|
||||||
|
<cfoutput>#serializeJSON({ OK=false, ERROR="not_found" })#</cfoutput>
|
||||||
|
<cfabort>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfelse>
|
||||||
|
<!--- Insert --->
|
||||||
|
<cfquery datasource="payfrit">
|
||||||
|
INSERT INTO ServicePoints (
|
||||||
|
ServicePointBusinessID,
|
||||||
|
ServicePointName,
|
||||||
|
ServicePointCode,
|
||||||
|
ServicePointTypeID,
|
||||||
|
ServicePointIsActive,
|
||||||
|
ServicePointSortOrder
|
||||||
|
) VALUES (
|
||||||
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">,
|
||||||
|
<cfqueryparam cfsqltype="cf_sql_varchar" value="#spName#">,
|
||||||
|
<cfqueryparam cfsqltype="cf_sql_varchar" value="#spCode#" null="#(len(spCode) EQ 0)#">,
|
||||||
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#spTypeID#">,
|
||||||
|
<cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">,
|
||||||
|
<cfqueryparam cfsqltype="cf_sql_integer" value="#sortOrder#">
|
||||||
|
)
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfquery name="qId" datasource="payfrit">
|
||||||
|
SELECT LAST_INSERT_ID() AS ServicePointID
|
||||||
|
</cfquery>
|
||||||
|
<cfset servicePointId = qId.ServicePointID>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Return saved row --->
|
||||||
|
<cfquery name="qOut" datasource="payfrit">
|
||||||
|
SELECT
|
||||||
|
ServicePointID,
|
||||||
|
ServicePointBusinessID,
|
||||||
|
ServicePointName,
|
||||||
|
ServicePointCode,
|
||||||
|
ServicePointTypeID,
|
||||||
|
ServicePointIsActive,
|
||||||
|
ServicePointSortOrder
|
||||||
|
FROM ServicePoints
|
||||||
|
WHERE ServicePointID = <cfqueryparam cfsqltype="cf_sql_integer" value="#servicePointId#">
|
||||||
|
AND ServicePointBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||||
|
LIMIT 1
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfset servicePoint = {
|
||||||
|
"ServicePointID" = qOut.ServicePointID,
|
||||||
|
"BusinessID" = qOut.ServicePointBusinessID,
|
||||||
|
"ServicePointName" = qOut.ServicePointName,
|
||||||
|
"ServicePointCode" = qOut.ServicePointCode,
|
||||||
|
"ServicePointTypeID"= qOut.ServicePointTypeID,
|
||||||
|
"IsActive" = qOut.ServicePointIsActive,
|
||||||
|
"ServicePointSortOrder" = qOut.ServicePointSortOrder
|
||||||
|
}>
|
||||||
|
|
||||||
|
<cfoutput>#serializeJSON({ OK=true, ERROR="", SERVICEPOINT=servicePoint })#</cfoutput>
|
||||||
|
|
||||||
|
<cfcatch type="any">
|
||||||
|
<cfheader statuscode="200" statustext="OK">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfoutput>#serializeJSON({ OK=false, ERROR="server_error", MESSAGE=cfcatch.message, DETAIL=cfcatch.detail })#</cfoutput>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
92
api/setup/checkDuplicate.cfm
Normal file
92
api/setup/checkDuplicate.cfm
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Check for duplicate businesses
|
||||||
|
*
|
||||||
|
* POST JSON:
|
||||||
|
* {
|
||||||
|
* "name": "Business Name",
|
||||||
|
* "addressLine1": "123 Main St",
|
||||||
|
* "city": "Los Angeles",
|
||||||
|
* "state": "CA",
|
||||||
|
* "zip": "90001"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {
|
||||||
|
* "OK": true,
|
||||||
|
* "duplicates": [ { BusinessID, BusinessName, Address } ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": true, "duplicates": [] };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
if (!len(requestBody)) {
|
||||||
|
throw(message="No request body provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
data = deserializeJSON(requestBody);
|
||||||
|
|
||||||
|
bizName = structKeyExists(data, "name") && isSimpleValue(data.name) ? trim(data.name) : "";
|
||||||
|
addressLine1 = structKeyExists(data, "addressLine1") && isSimpleValue(data.addressLine1) ? trim(data.addressLine1) : "";
|
||||||
|
city = structKeyExists(data, "city") && isSimpleValue(data.city) ? trim(data.city) : "";
|
||||||
|
state = structKeyExists(data, "state") && isSimpleValue(data.state) ? trim(data.state) : "";
|
||||||
|
zip = structKeyExists(data, "zip") && isSimpleValue(data.zip) ? trim(data.zip) : "";
|
||||||
|
|
||||||
|
// Clean up city - remove trailing punctuation
|
||||||
|
city = reReplace(city, "[,.\s]+$", "", "all");
|
||||||
|
|
||||||
|
// Build query to find potential duplicates
|
||||||
|
// Match by name (case-insensitive) OR by address components
|
||||||
|
qDuplicates = queryExecute("
|
||||||
|
SELECT DISTINCT
|
||||||
|
b.BusinessID,
|
||||||
|
b.BusinessName,
|
||||||
|
a.AddressLine1,
|
||||||
|
a.AddressCity,
|
||||||
|
s.tt_StateAbbreviation as AddressState,
|
||||||
|
a.AddressZIPCode
|
||||||
|
FROM Businesses b
|
||||||
|
LEFT JOIN Addresses a ON a.AddressBusinessID = b.BusinessID
|
||||||
|
LEFT JOIN tt_States s ON s.tt_StateID = a.AddressStateID
|
||||||
|
WHERE
|
||||||
|
LOWER(b.BusinessName) = LOWER(:bizName)
|
||||||
|
OR (
|
||||||
|
LOWER(a.AddressLine1) = LOWER(:addressLine1)
|
||||||
|
AND LOWER(a.AddressCity) = LOWER(:city)
|
||||||
|
AND a.AddressLine1 != ''
|
||||||
|
AND a.AddressCity != ''
|
||||||
|
)
|
||||||
|
ORDER BY b.BusinessName
|
||||||
|
", {
|
||||||
|
bizName: bizName,
|
||||||
|
addressLine1: addressLine1,
|
||||||
|
city: city
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
for (i = 1; i <= qDuplicates.recordCount; i++) {
|
||||||
|
addressParts = [];
|
||||||
|
if (len(qDuplicates.AddressLine1[i])) arrayAppend(addressParts, qDuplicates.AddressLine1[i]);
|
||||||
|
if (len(qDuplicates.AddressCity[i])) arrayAppend(addressParts, qDuplicates.AddressCity[i]);
|
||||||
|
if (len(qDuplicates.AddressState[i])) arrayAppend(addressParts, qDuplicates.AddressState[i]);
|
||||||
|
if (len(qDuplicates.AddressZIPCode[i])) arrayAppend(addressParts, qDuplicates.AddressZIPCode[i]);
|
||||||
|
|
||||||
|
arrayAppend(response.duplicates, {
|
||||||
|
"BusinessID": qDuplicates.BusinessID[i],
|
||||||
|
"BusinessName": qDuplicates.BusinessName[i],
|
||||||
|
"Address": arrayToList(addressParts, ", ")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response.OK = false;
|
||||||
|
response.error = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
|
|
@ -41,7 +41,8 @@ try {
|
||||||
if (businessId == 0) {
|
if (businessId == 0) {
|
||||||
response.steps.append("No businessId provided - creating new business");
|
response.steps.append("No businessId provided - creating new business");
|
||||||
|
|
||||||
if (!structKeyExists(biz, "name") || !len(biz.name)) {
|
bizName = structKeyExists(biz, "name") && isSimpleValue(biz.name) ? biz.name : "";
|
||||||
|
if (!len(bizName)) {
|
||||||
throw(message="Business name is required to create new business");
|
throw(message="Business name is required to create new business");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,21 +50,30 @@ try {
|
||||||
throw(message="userId is required to create new business");
|
throw(message="userId is required to create new business");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create address record first (use extracted address fields)
|
// Extract phone number
|
||||||
addressLine1 = structKeyExists(biz, "addressLine1") ? biz.addressLine1 : "";
|
bizPhone = structKeyExists(biz, "phone") && isSimpleValue(biz.phone) ? trim(biz.phone) : "";
|
||||||
city = structKeyExists(biz, "city") ? biz.city : "";
|
|
||||||
state = structKeyExists(biz, "state") ? biz.state : "";
|
// Create address record first (use extracted address fields) - safely extract as simple values
|
||||||
zip = structKeyExists(biz, "zip") ? biz.zip : "";
|
addressLine1 = structKeyExists(biz, "addressLine1") && isSimpleValue(biz.addressLine1) ? trim(biz.addressLine1) : "";
|
||||||
|
city = structKeyExists(biz, "city") && isSimpleValue(biz.city) ? trim(biz.city) : "";
|
||||||
|
state = structKeyExists(biz, "state") && isSimpleValue(biz.state) ? trim(biz.state) : "";
|
||||||
|
zip = structKeyExists(biz, "zip") && isSimpleValue(biz.zip) ? trim(biz.zip) : "";
|
||||||
|
|
||||||
|
// Clean up city - remove trailing punctuation (commas, periods, etc.)
|
||||||
|
city = reReplace(city, "[,.\s]+$", "", "all");
|
||||||
|
|
||||||
// Look up state ID from state abbreviation
|
// Look up state ID from state abbreviation
|
||||||
stateID = 0;
|
stateID = 0;
|
||||||
|
response.steps.append("State value received: '" & state & "' (len: " & len(state) & ")");
|
||||||
if (len(state)) {
|
if (len(state)) {
|
||||||
qState = queryExecute("
|
qState = queryExecute("
|
||||||
SELECT tt_StateID FROM tt_States WHERE tt_StateAbbreviation = :abbr
|
SELECT tt_StateID FROM tt_States WHERE tt_StateAbbreviation = :abbr
|
||||||
", { abbr: uCase(state) }, { datasource: "payfrit" });
|
", { abbr: uCase(state) }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response.steps.append("State lookup for '" & uCase(state) & "' found " & qState.recordCount & " records");
|
||||||
if (qState.recordCount > 0) {
|
if (qState.recordCount > 0) {
|
||||||
stateID = qState.tt_StateID;
|
stateID = qState.tt_StateID;
|
||||||
|
response.steps.append("Using stateID: " & stateID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,12 +93,13 @@ try {
|
||||||
addressId = qNewAddr.id;
|
addressId = qNewAddr.id;
|
||||||
response.steps.append("Created address record (ID: " & addressId & ")");
|
response.steps.append("Created address record (ID: " & addressId & ")");
|
||||||
|
|
||||||
// Create new business with address link
|
// Create new business with address link and phone
|
||||||
queryExecute("
|
queryExecute("
|
||||||
INSERT INTO Businesses (BusinessName, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessAddedOn)
|
INSERT INTO Businesses (BusinessName, BusinessPhone, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessAddedOn)
|
||||||
VALUES (:name, :userId, :addressId, :deliveryZips, NOW())
|
VALUES (:name, :phone, :userId, :addressId, :deliveryZips, NOW())
|
||||||
", {
|
", {
|
||||||
name: biz.name,
|
name: bizName,
|
||||||
|
phone: bizPhone,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
addressId: addressId,
|
addressId: addressId,
|
||||||
deliveryZips: len(zip) ? zip : ""
|
deliveryZips: len(zip) ? zip : ""
|
||||||
|
|
@ -96,7 +107,7 @@ try {
|
||||||
|
|
||||||
qNewBiz = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
qNewBiz = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
businessId = qNewBiz.id;
|
businessId = qNewBiz.id;
|
||||||
response.steps.append("Created new business: " & biz.name & " (ID: " & businessId & ")");
|
response.steps.append("Created new business: " & bizName & " (ID: " & businessId & ")");
|
||||||
|
|
||||||
// Update address with business ID link
|
// Update address with business ID link
|
||||||
queryExecute("
|
queryExecute("
|
||||||
|
|
@ -114,9 +125,11 @@ try {
|
||||||
|
|
||||||
for (i = 1; i <= arrayLen(hoursSchedule); i++) {
|
for (i = 1; i <= arrayLen(hoursSchedule); i++) {
|
||||||
dayData = hoursSchedule[i];
|
dayData = hoursSchedule[i];
|
||||||
|
if (!isStruct(dayData)) continue;
|
||||||
|
|
||||||
dayID = structKeyExists(dayData, "dayId") ? val(dayData.dayId) : 0;
|
dayID = structKeyExists(dayData, "dayId") ? val(dayData.dayId) : 0;
|
||||||
openTime = structKeyExists(dayData, "open") ? dayData.open : "09:00";
|
openTime = structKeyExists(dayData, "open") && isSimpleValue(dayData.open) ? dayData.open : "09:00";
|
||||||
closeTime = structKeyExists(dayData, "close") ? dayData.close : "17:00";
|
closeTime = structKeyExists(dayData, "close") && isSimpleValue(dayData.close) ? dayData.close : "17:00";
|
||||||
|
|
||||||
// Convert HH:MM to HH:MM:SS if needed
|
// Convert HH:MM to HH:MM:SS if needed
|
||||||
if (len(openTime) == 5) openTime = openTime & ":00";
|
if (len(openTime) == 5) openTime = openTime & ":00";
|
||||||
|
|
@ -158,9 +171,13 @@ try {
|
||||||
|
|
||||||
for (i = 1; i <= arrayLen(modTemplates); i++) {
|
for (i = 1; i <= arrayLen(modTemplates); i++) {
|
||||||
tmpl = modTemplates[i];
|
tmpl = modTemplates[i];
|
||||||
tmplName = tmpl.name;
|
tmplName = structKeyExists(tmpl, "name") && isSimpleValue(tmpl.name) ? tmpl.name : "";
|
||||||
|
if (!len(tmplName)) {
|
||||||
|
response.steps.append("Warning: Skipping modifier template with no name at index " & i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
required = structKeyExists(tmpl, "required") && tmpl.required == true;
|
required = structKeyExists(tmpl, "required") && tmpl.required == true;
|
||||||
options = structKeyExists(tmpl, "options") ? tmpl.options : [];
|
options = structKeyExists(tmpl, "options") && isArray(tmpl.options) ? tmpl.options : [];
|
||||||
|
|
||||||
// Debug: Log options info
|
// Debug: Log options info
|
||||||
response.steps.append("Template '" & tmplName & "' has " & arrayLen(options) & " options (type: " & (isArray(options) ? "array" : "other") & ")");
|
response.steps.append("Template '" & tmplName & "' has " & arrayLen(options) & " options (type: " & (isArray(options) ? "array" : "other") & ")");
|
||||||
|
|
@ -204,13 +221,14 @@ try {
|
||||||
optionOrder = 1;
|
optionOrder = 1;
|
||||||
for (j = 1; j <= arrayLen(options); j++) {
|
for (j = 1; j <= arrayLen(options); j++) {
|
||||||
opt = options[j];
|
opt = options[j];
|
||||||
// Safety check: ensure opt is a struct with a name
|
// Safety check: ensure opt is a struct with a name that's a simple value
|
||||||
if (!isStruct(opt) || !structKeyExists(opt, "name") || !len(opt.name)) {
|
if (!isStruct(opt)) continue;
|
||||||
continue;
|
if (!structKeyExists(opt, "name")) continue;
|
||||||
}
|
if (!isSimpleValue(opt.name)) continue;
|
||||||
|
if (!len(opt.name)) continue;
|
||||||
|
|
||||||
optName = opt.name;
|
optName = opt.name;
|
||||||
optPrice = structKeyExists(opt, "price") ? val(opt.price) : 0;
|
optPrice = structKeyExists(opt, "price") && isSimpleValue(opt.price) ? val(opt.price) : 0;
|
||||||
|
|
||||||
qOpt = queryExecute("
|
qOpt = queryExecute("
|
||||||
SELECT ItemID FROM Items
|
SELECT ItemID FROM Items
|
||||||
|
|
@ -246,7 +264,11 @@ try {
|
||||||
catOrder = 1;
|
catOrder = 1;
|
||||||
for (c = 1; c <= arrayLen(categories); c++) {
|
for (c = 1; c <= arrayLen(categories); c++) {
|
||||||
cat = categories[c];
|
cat = categories[c];
|
||||||
catName = cat.name;
|
catName = structKeyExists(cat, "name") && isSimpleValue(cat.name) ? cat.name : "";
|
||||||
|
if (!len(catName)) {
|
||||||
|
response.steps.append("Warning: Skipping category with no name at index " & c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if category exists in Categories table
|
// Check if category exists in Categories table
|
||||||
qCat = queryExecute("
|
qCat = queryExecute("
|
||||||
|
|
@ -293,15 +315,37 @@ try {
|
||||||
|
|
||||||
for (n = 1; n <= arrayLen(items); n++) {
|
for (n = 1; n <= arrayLen(items); n++) {
|
||||||
item = items[n];
|
item = items[n];
|
||||||
itemName = item.name;
|
if (!isStruct(item)) continue;
|
||||||
itemDesc = structKeyExists(item, "description") ? item.description : "";
|
|
||||||
itemPrice = structKeyExists(item, "price") ? val(item.price) : 0;
|
// Safely extract all item fields - ensure they're simple values
|
||||||
itemCategory = structKeyExists(item, "category") ? item.category : "";
|
itemName = structKeyExists(item, "name") && isSimpleValue(item.name) ? item.name : "";
|
||||||
itemModifiers = structKeyExists(item, "modifiers") ? item.modifiers : [];
|
if (!len(itemName)) {
|
||||||
|
response.steps.append("Warning: Skipping item with no name at index " & n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemDesc = "";
|
||||||
|
if (structKeyExists(item, "description") && isSimpleValue(item.description)) {
|
||||||
|
itemDesc = item.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemPrice = 0;
|
||||||
|
if (structKeyExists(item, "price")) {
|
||||||
|
if (isSimpleValue(item.price)) {
|
||||||
|
itemPrice = val(item.price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemCategory = "";
|
||||||
|
if (structKeyExists(item, "category") && isSimpleValue(item.category)) {
|
||||||
|
itemCategory = item.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemModifiers = structKeyExists(item, "modifiers") && isArray(item.modifiers) ? item.modifiers : [];
|
||||||
|
|
||||||
// Get category ID
|
// Get category ID
|
||||||
if (!len(itemCategory) || !structKeyExists(categoryMap, itemCategory)) {
|
if (!len(itemCategory) || !structKeyExists(categoryMap, itemCategory)) {
|
||||||
response.steps.append("Warning: Item '" & itemName & "' has unknown category '" & itemCategory & "' - skipping");
|
response.steps.append("Warning: Item '" & itemName & "' has unknown category - skipping");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,7 +407,15 @@ try {
|
||||||
// Link modifier templates to this item
|
// Link modifier templates to this item
|
||||||
modOrder = 1;
|
modOrder = 1;
|
||||||
for (m = 1; m <= arrayLen(itemModifiers); m++) {
|
for (m = 1; m <= arrayLen(itemModifiers); m++) {
|
||||||
modName = itemModifiers[m];
|
modRef = itemModifiers[m];
|
||||||
|
// Handle both string modifier names and struct references
|
||||||
|
if (isSimpleValue(modRef)) {
|
||||||
|
modName = modRef;
|
||||||
|
} else if (isStruct(modRef) && structKeyExists(modRef, "name")) {
|
||||||
|
modName = modRef.name;
|
||||||
|
} else {
|
||||||
|
continue; // Skip invalid modifier reference
|
||||||
|
}
|
||||||
if (structKeyExists(templateMap, modName)) {
|
if (structKeyExists(templateMap, modName)) {
|
||||||
templateItemID = templateMap[modName];
|
templateItemID = templateMap[modName];
|
||||||
|
|
||||||
|
|
|
||||||
4
portal/favicon.svg
Normal file
4
portal/favicon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#6366f1"/>
|
||||||
|
<text x="16" y="22" text-anchor="middle" font-family="system-ui, sans-serif" font-weight="700" font-size="18" fill="#fff">P</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 256 B |
|
|
@ -58,6 +58,14 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span>Team</span>
|
<span>Team</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#beacons" class="nav-item" data-page="beacons">
|
||||||
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<path d="M12 2v4M12 18v4M2 12h4M18 12h4"/>
|
||||||
|
<path d="M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
|
||||||
|
</svg>
|
||||||
|
<span>Beacons</span>
|
||||||
|
</a>
|
||||||
<a href="#settings" class="nav-item" data-page="settings">
|
<a href="#settings" class="nav-item" data-page="settings">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
|
@ -341,6 +349,50 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Beacons Page -->
|
||||||
|
<section class="page" id="page-beacons">
|
||||||
|
<div class="beacons-layout">
|
||||||
|
<!-- Left side: Beacons list -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<h3>Beacons</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="Portal.showBeaconModal()">+ Add Beacon</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group" id="beaconsList">
|
||||||
|
<div class="empty-state">Loading beacons...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle: Service Points list -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<h3>Service Points</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="Portal.showServicePointModal()">+ Add Service Point</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group" id="servicePointsList">
|
||||||
|
<div class="empty-state">Loading service points...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side: Assignments -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<h3>Beacon Assignments</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="Portal.showAssignmentModal()">+ Assign</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group" id="assignmentsList">
|
||||||
|
<div class="empty-state">Loading assignments...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Settings Page -->
|
<!-- Settings Page -->
|
||||||
<section class="page" id="page-settings">
|
<section class="page" id="page-settings">
|
||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
|
|
@ -349,37 +401,44 @@
|
||||||
<h3>Business Information</h3>
|
<h3>Business Information</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="businessInfoForm" class="form">
|
<form id="businessInfoForm" class="form" onsubmit="Portal.saveBusinessInfo(event)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Business Name</label>
|
<label>Business Name</label>
|
||||||
<input type="text" id="settingBusinessName" class="form-input">
|
<input type="text" id="settingBusinessName" class="form-input" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Description</label>
|
<label>Phone</label>
|
||||||
<textarea id="settingDescription" class="form-textarea" rows="3"></textarea>
|
<input type="tel" id="settingPhone" class="form-input" placeholder="(555) 123-4567">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Address</label>
|
<label>Street Address</label>
|
||||||
<input type="text" id="settingAddress" class="form-input">
|
<input type="text" id="settingAddressLine1" class="form-input" placeholder="123 Main St">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row" style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:12px;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Phone</label>
|
<label>City</label>
|
||||||
<input type="tel" id="settingPhone" class="form-input">
|
<input type="text" id="settingCity" class="form-input" placeholder="Los Angeles">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Email</label>
|
<label>State</label>
|
||||||
<input type="email" id="settingEmail" class="form-input">
|
<select id="settingState" class="form-input">
|
||||||
|
<option value="">--</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>ZIP</label>
|
||||||
|
<input type="text" id="settingZip" class="form-input" placeholder="90001" maxlength="10">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary" id="saveBusinessBtn">Save Changes</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||||
<h3>Business Hours</h3>
|
<h3>Business Hours</h3>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="Portal.saveHours()">Save Hours</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="hours-list" id="hoursEditor">
|
<div class="hours-list" id="hoursEditor">
|
||||||
|
|
|
||||||
|
|
@ -652,13 +652,18 @@
|
||||||
.toolbar-btn.primary {
|
.toolbar-btn.primary {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
color: #000;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-btn.primary:hover {
|
.toolbar-btn.primary:hover {
|
||||||
background: var(--primary-hover);
|
background: var(--primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.primary:active {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-btn svg {
|
.toolbar-btn svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|
@ -790,17 +795,11 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.showImportModal()" title="Import JSON">
|
<button class="toolbar-btn" onclick="MenuBuilder.showOutlineModal()" title="View Full Menu Outline">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
|
<path d="M4 6h16M4 12h16M4 18h12"/>
|
||||||
</svg>
|
</svg>
|
||||||
Import
|
Outline
|
||||||
</button>
|
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.showExportModal()" title="Export JSON">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
|
|
||||||
</svg>
|
|
||||||
Export
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group" style="margin-left: auto;">
|
<div class="toolbar-group" style="margin-left: auto;">
|
||||||
|
|
@ -874,6 +873,26 @@
|
||||||
<strong id="statPhotosMissing" style="color: var(--warning);">0</strong>
|
<strong id="statPhotosMissing" style="color: var(--warning);">0</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>Branding</h3>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
|
<button class="btn btn-secondary" onclick="MenuBuilder.uploadHeader()" style="width: 100%; display: flex; align-items: center; gap: 8px; justify-content: center;">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||||
|
<polyline points="21 15 16 10 5 21"/>
|
||||||
|
</svg>
|
||||||
|
Upload Header
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="MenuBuilder.showBrandColorPicker()" style="width: 100%; display: flex; align-items: center; gap: 8px; justify-content: center;">
|
||||||
|
<span id="brandColorSwatch" style="width: 16px; height: 16px; border-radius: 3px; background: #1B4D3E; border: 1px solid rgba(0,0,0,0.2);"></span>
|
||||||
|
Brand Color
|
||||||
|
</button>
|
||||||
|
<p style="font-size: 11px; color: var(--text-muted); margin: 0;">
|
||||||
|
Header: 1200x400px recommended<br>
|
||||||
|
Color: Used for category bars in the app
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Canvas -->
|
<!-- Main Canvas -->
|
||||||
|
|
@ -2836,6 +2855,242 @@
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Show outline modal - full menu in hierarchical text format
|
||||||
|
showOutlineModal() {
|
||||||
|
document.getElementById('modalTitle').textContent = 'Menu Outline';
|
||||||
|
|
||||||
|
let outline = '';
|
||||||
|
const categories = this.menu.categories || [];
|
||||||
|
|
||||||
|
if (categories.length === 0) {
|
||||||
|
outline = '<div style="color: var(--text-muted); text-align: center; padding: 40px;">No menu items yet</div>';
|
||||||
|
} else {
|
||||||
|
outline = '<div class="menu-outline">';
|
||||||
|
|
||||||
|
for (const cat of categories) {
|
||||||
|
outline += `<div class="outline-category">${this.escapeHtml(cat.name)}</div>`;
|
||||||
|
|
||||||
|
const items = cat.items || [];
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPrice = item.price ? `$${parseFloat(item.price).toFixed(2)}` : '';
|
||||||
|
outline += `<div class="outline-item">${this.escapeHtml(item.name)}${itemPrice ? ' <span class="outline-price">' + itemPrice + '</span>' : ''}</div>`;
|
||||||
|
|
||||||
|
// Render modifiers recursively
|
||||||
|
outline += this.renderOutlineModifiers(item.modifiers || [], 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outline += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modalBody').innerHTML = `
|
||||||
|
<style>
|
||||||
|
.menu-outline {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.outline-category {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.outline-category:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.outline-item {
|
||||||
|
padding-left: 20px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.outline-modifier {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.outline-price {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
.outline-modifier .outline-price {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
${outline}
|
||||||
|
`;
|
||||||
|
this.showModal();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper to render modifiers recursively for outline
|
||||||
|
renderOutlineModifiers(modifiers, depth) {
|
||||||
|
if (!modifiers || modifiers.length === 0) return '';
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
const indent = '\u00A0\u00A0\u00A0\u00A0'.repeat(depth); // non-breaking spaces for indent
|
||||||
|
|
||||||
|
for (const mod of modifiers) {
|
||||||
|
const modPrice = mod.price ? `+$${parseFloat(mod.price).toFixed(2)}` : '';
|
||||||
|
html += `<div class="outline-modifier" style="padding-left: ${depth * 20}px;">${indent.substring(0, 4)}${this.escapeHtml(mod.name)}${modPrice ? ' <span class="outline-price">' + modPrice + '</span>' : ''}</div>`;
|
||||||
|
|
||||||
|
// Recurse into nested options
|
||||||
|
if (mod.options && mod.options.length > 0) {
|
||||||
|
html += this.renderOutlineModifiers(mod.options, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Upload header image (1200x400)
|
||||||
|
uploadHeader() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/jpeg,image/png,image/gif,image/webp';
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
if (!validTypes.includes(file.type)) {
|
||||||
|
this.toast('Please select a valid image (JPG, PNG, GIF, or WebP)', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (max 5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
this.toast('Image must be under 5MB', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading toast
|
||||||
|
this.toast('Uploading header...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('header', file);
|
||||||
|
formData.append('BusinessID', this.config.businessId);
|
||||||
|
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/menu/uploadHeader.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Header uploaded successfully! Recommended size: 1200x400 pixels', 'success');
|
||||||
|
} else {
|
||||||
|
this.toast(data.MESSAGE || 'Failed to upload header', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Header upload error:', err);
|
||||||
|
this.toast('Failed to upload header', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show brand color picker modal
|
||||||
|
showBrandColorPicker() {
|
||||||
|
const currentColor = this.brandColor || '#1B4D3E';
|
||||||
|
document.getElementById('modalTitle').textContent = 'Brand Color';
|
||||||
|
document.getElementById('modalBody').innerHTML = `
|
||||||
|
<p style="margin-bottom: 16px; color: var(--text-muted);">
|
||||||
|
This color is used for the category bar gradients in the customer app.
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 16px;">
|
||||||
|
<input type="color" id="brandColorInput" value="${currentColor}"
|
||||||
|
style="width: 60px; height: 40px; border: none; cursor: pointer; border-radius: 4px;">
|
||||||
|
<input type="text" id="brandColorHex" value="${currentColor}"
|
||||||
|
style="width: 100px; padding: 8px; border: 1px solid var(--border); border-radius: 4px; font-family: monospace;"
|
||||||
|
pattern="^#[0-9A-Fa-f]{6}$" maxlength="7">
|
||||||
|
<div id="brandColorPreview" style="flex: 1; height: 40px; border-radius: 4px; background: linear-gradient(to bottom, ${currentColor}44, ${currentColor}00, ${currentColor}66);"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;">
|
||||||
|
<button type="button" class="color-preset" data-color="#1B4D3E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #1B4D3E;" title="Forest Green (Default)"></button>
|
||||||
|
<button type="button" class="color-preset" data-color="#2C3E50" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #2C3E50;" title="Midnight Blue"></button>
|
||||||
|
<button type="button" class="color-preset" data-color="#8B4513" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #8B4513;" title="Saddle Brown"></button>
|
||||||
|
<button type="button" class="color-preset" data-color="#800020" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #800020;" title="Burgundy"></button>
|
||||||
|
<button type="button" class="color-preset" data-color="#1A1A2E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #1A1A2E;" title="Dark Navy"></button>
|
||||||
|
<button type="button" class="color-preset" data-color="#4A0E4E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #4A0E4E;" title="Deep Purple"></button>
|
||||||
|
<button type="button" class="color-preset" data-color="#CC5500" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #CC5500;" title="Burnt Orange"></button>
|
||||||
|
<button type="button" class="color-preset" data-color="#355E3B" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #355E3B;" title="Hunter Green"></button>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="MenuBuilder.hideModal()">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="MenuBuilder.saveBrandColor()">Save Color</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.showModal();
|
||||||
|
|
||||||
|
// Wire up color picker sync
|
||||||
|
const colorInput = document.getElementById('brandColorInput');
|
||||||
|
const hexInput = document.getElementById('brandColorHex');
|
||||||
|
const preview = document.getElementById('brandColorPreview');
|
||||||
|
|
||||||
|
const updatePreview = (color) => {
|
||||||
|
preview.style.background = `linear-gradient(to bottom, ${color}44, ${color}00, ${color}66)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
colorInput.addEventListener('input', (e) => {
|
||||||
|
hexInput.value = e.target.value.toUpperCase();
|
||||||
|
updatePreview(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
hexInput.addEventListener('input', (e) => {
|
||||||
|
let val = e.target.value;
|
||||||
|
if (!val.startsWith('#')) val = '#' + val;
|
||||||
|
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
|
||||||
|
colorInput.value = val;
|
||||||
|
updatePreview(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preset buttons
|
||||||
|
document.querySelectorAll('.color-preset').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const color = btn.dataset.color;
|
||||||
|
colorInput.value = color;
|
||||||
|
hexInput.value = color;
|
||||||
|
updatePreview(color);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save brand color
|
||||||
|
async saveBrandColor() {
|
||||||
|
const color = document.getElementById('brandColorHex').value.toUpperCase();
|
||||||
|
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
|
||||||
|
this.toast('Invalid color format. Use #RRGGBB', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/businesses/saveBrandColor.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ BusinessID: this.config.businessId, BrandColor: color })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.OK) {
|
||||||
|
this.brandColor = color;
|
||||||
|
document.getElementById('brandColorSwatch').style.background = color;
|
||||||
|
this.hideModal();
|
||||||
|
this.toast('Brand color saved!', 'success');
|
||||||
|
} else {
|
||||||
|
this.toast(data.MESSAGE || 'Failed to save color', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Save brand color error:', err);
|
||||||
|
this.toast('Failed to save color', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Load menu from API
|
// Load menu from API
|
||||||
async loadMenu() {
|
async loadMenu() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -2856,6 +3111,12 @@
|
||||||
console.log('[MenuBuilder] Menu categories:', this.menu.categories?.length || 0);
|
console.log('[MenuBuilder] Menu categories:', this.menu.categories?.length || 0);
|
||||||
// Store templates from API (default to empty array if not provided)
|
// Store templates from API (default to empty array if not provided)
|
||||||
this.templates = data.TEMPLATES || [];
|
this.templates = data.TEMPLATES || [];
|
||||||
|
// Load brand color if set
|
||||||
|
if (data.BRANDCOLOR && data.BRANDCOLOR.length > 0) {
|
||||||
|
this.brandColor = data.BRANDCOLOR;
|
||||||
|
const swatch = document.getElementById('brandColorSwatch');
|
||||||
|
if (swatch) swatch.style.background = data.BRANDCOLOR;
|
||||||
|
}
|
||||||
this.renderTemplateLibrary();
|
this.renderTemplateLibrary();
|
||||||
this.render();
|
this.render();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
--primary: #6366f1;
|
--primary: #6366f1;
|
||||||
--primary-dark: #4f46e5;
|
--primary-dark: #4f46e5;
|
||||||
--primary-light: #818cf8;
|
--primary-light: #818cf8;
|
||||||
|
--primary-hover: #4f46e5;
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
--warning: #f59e0b;
|
--warning: #f59e0b;
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
|
|
@ -23,6 +24,13 @@
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
--shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
|
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
|
||||||
|
|
||||||
|
/* Menu Builder variables */
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--bg-secondary: var(--gray-50);
|
||||||
|
--border-color: var(--gray-200);
|
||||||
|
--text-primary: var(--gray-900);
|
||||||
|
--text-muted: var(--gray-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -555,6 +563,70 @@ body {
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Beacons Page Layout */
|
||||||
|
.beacons-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.beacons-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background: var(--gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item .item-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item .item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--gray-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item .item-detail {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item .item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item .item-actions button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--gray-500);
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Stripe Status */
|
/* Stripe Status */
|
||||||
.stripe-status {
|
.stripe-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
664
portal/portal.js
664
portal/portal.js
|
|
@ -225,6 +225,9 @@ const Portal = {
|
||||||
case 'team':
|
case 'team':
|
||||||
await this.loadTeam();
|
await this.loadTeam();
|
||||||
break;
|
break;
|
||||||
|
case 'beacons':
|
||||||
|
await this.loadBeaconsPage();
|
||||||
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
break;
|
break;
|
||||||
|
|
@ -652,6 +655,9 @@ const Portal = {
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
console.log('[Portal] Loading settings...');
|
console.log('[Portal] Loading settings...');
|
||||||
|
|
||||||
|
// Load states dropdown first
|
||||||
|
await this.loadStatesDropdown();
|
||||||
|
|
||||||
// Load business info
|
// Load business info
|
||||||
await this.loadBusinessInfo();
|
await this.loadBusinessInfo();
|
||||||
|
|
||||||
|
|
@ -659,6 +665,24 @@ const Portal = {
|
||||||
await this.checkStripeStatus();
|
await this.checkStripeStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Load states for dropdown
|
||||||
|
async loadStatesDropdown() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/addresses/states.cfm`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK && data.STATES) {
|
||||||
|
const select = document.getElementById('settingState');
|
||||||
|
select.innerHTML = '<option value="">--</option>';
|
||||||
|
data.STATES.forEach(state => {
|
||||||
|
select.innerHTML += `<option value="${state.Abbr}">${state.Abbr}</option>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error loading states:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Load business info for settings
|
// Load business info for settings
|
||||||
async loadBusinessInfo() {
|
async loadBusinessInfo() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -671,17 +695,172 @@ const Portal = {
|
||||||
|
|
||||||
if (data.OK && data.BUSINESS) {
|
if (data.OK && data.BUSINESS) {
|
||||||
const biz = data.BUSINESS;
|
const biz = data.BUSINESS;
|
||||||
|
this.currentBusiness = biz;
|
||||||
|
|
||||||
|
// Populate form fields
|
||||||
document.getElementById('settingBusinessName').value = biz.BusinessName || '';
|
document.getElementById('settingBusinessName').value = biz.BusinessName || '';
|
||||||
document.getElementById('settingDescription').value = biz.BusinessDescription || '';
|
|
||||||
document.getElementById('settingAddress').value = biz.BusinessAddress || '';
|
|
||||||
document.getElementById('settingPhone').value = biz.BusinessPhone || '';
|
document.getElementById('settingPhone').value = biz.BusinessPhone || '';
|
||||||
document.getElementById('settingEmail').value = biz.BusinessEmail || '';
|
document.getElementById('settingAddressLine1').value = biz.AddressLine1 || '';
|
||||||
|
document.getElementById('settingCity').value = biz.AddressCity || '';
|
||||||
|
document.getElementById('settingState').value = biz.AddressState || '';
|
||||||
|
document.getElementById('settingZip').value = biz.AddressZip || '';
|
||||||
|
|
||||||
|
// Render hours editor
|
||||||
|
this.renderHoursEditor(biz.BusinessHoursDetail || []);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Portal] Error loading business info:', err);
|
console.error('[Portal] Error loading business info:', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Render hours editor
|
||||||
|
renderHoursEditor(hours) {
|
||||||
|
const container = document.getElementById('hoursEditor');
|
||||||
|
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
|
const dayIds = [1, 2, 3, 4, 5, 6, 7]; // Monday=1 through Sunday=7
|
||||||
|
|
||||||
|
// Create a map of existing hours by day ID
|
||||||
|
const hoursMap = {};
|
||||||
|
hours.forEach(h => {
|
||||||
|
hoursMap[h.dayId] = h;
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = '<div class="hours-grid" style="display:flex;flex-direction:column;gap:12px;">';
|
||||||
|
|
||||||
|
dayIds.forEach((dayId, idx) => {
|
||||||
|
const dayName = dayNames[idx];
|
||||||
|
const existing = hoursMap[dayId];
|
||||||
|
const isClosed = !existing;
|
||||||
|
const openTime = existing ? this.formatTimeFor24Input(existing.open) : '09:00';
|
||||||
|
const closeTime = existing ? this.formatTimeFor24Input(existing.close) : '17:00';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="hours-row" style="display:grid;grid-template-columns:100px 1fr 1fr auto;gap:12px;align-items:center;">
|
||||||
|
<label style="font-weight:500;">${dayName}</label>
|
||||||
|
<input type="time" id="hours_open_${dayId}" value="${openTime}" class="form-input" ${isClosed ? 'disabled' : ''} style="padding:8px;">
|
||||||
|
<input type="time" id="hours_close_${dayId}" value="${closeTime}" class="form-input" ${isClosed ? 'disabled' : ''} style="padding:8px;">
|
||||||
|
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
|
||||||
|
<input type="checkbox" id="hours_closed_${dayId}" ${isClosed ? 'checked' : ''} onchange="Portal.toggleHoursDay(${dayId})">
|
||||||
|
Closed
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper to convert "7:00 AM" format to "07:00" for time input
|
||||||
|
formatTimeFor24Input(timeStr) {
|
||||||
|
if (!timeStr) return '09:00';
|
||||||
|
// If already in 24h format (HH:MM), return as-is
|
||||||
|
if (/^\d{2}:\d{2}$/.test(timeStr)) return timeStr;
|
||||||
|
|
||||||
|
// Parse "7:00 AM" or "7:30 PM" format
|
||||||
|
const match = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
|
||||||
|
if (match) {
|
||||||
|
let hours = parseInt(match[1]);
|
||||||
|
const minutes = match[2];
|
||||||
|
const ampm = match[3].toUpperCase();
|
||||||
|
|
||||||
|
if (ampm === 'PM' && hours < 12) hours += 12;
|
||||||
|
if (ampm === 'AM' && hours === 12) hours = 0;
|
||||||
|
|
||||||
|
return `${String(hours).padStart(2, '0')}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeStr;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toggle hours day closed/open
|
||||||
|
toggleHoursDay(dayId) {
|
||||||
|
const isClosed = document.getElementById(`hours_closed_${dayId}`).checked;
|
||||||
|
document.getElementById(`hours_open_${dayId}`).disabled = isClosed;
|
||||||
|
document.getElementById(`hours_close_${dayId}`).disabled = isClosed;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save business info
|
||||||
|
async saveBusinessInfo(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const btn = document.getElementById('saveBusinessBtn');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = 'Saving...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/businesses/update.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
BusinessID: this.config.businessId,
|
||||||
|
BusinessName: document.getElementById('settingBusinessName').value,
|
||||||
|
BusinessPhone: document.getElementById('settingPhone').value,
|
||||||
|
AddressLine1: document.getElementById('settingAddressLine1').value,
|
||||||
|
City: document.getElementById('settingCity').value,
|
||||||
|
State: document.getElementById('settingState').value,
|
||||||
|
Zip: document.getElementById('settingZip').value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.showToast('Business info saved!', 'success');
|
||||||
|
// Reload to refresh sidebar etc
|
||||||
|
await this.loadBusinessInfo();
|
||||||
|
} else {
|
||||||
|
this.showToast(data.ERROR || 'Failed to save', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error saving business info:', err);
|
||||||
|
this.showToast('Error saving business info', 'error');
|
||||||
|
} finally {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save hours
|
||||||
|
async saveHours() {
|
||||||
|
const dayIds = [1, 2, 3, 4, 5, 6, 7];
|
||||||
|
const hours = [];
|
||||||
|
|
||||||
|
dayIds.forEach(dayId => {
|
||||||
|
const isClosed = document.getElementById(`hours_closed_${dayId}`).checked;
|
||||||
|
if (!isClosed) {
|
||||||
|
hours.push({
|
||||||
|
dayId: dayId,
|
||||||
|
open: document.getElementById(`hours_open_${dayId}`).value,
|
||||||
|
close: document.getElementById(`hours_close_${dayId}`).value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/businesses/updateHours.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
BusinessID: this.config.businessId,
|
||||||
|
Hours: hours
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.showToast('Hours saved!', 'success');
|
||||||
|
} else {
|
||||||
|
this.showToast(data.ERROR || 'Failed to save hours', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error saving hours:', err);
|
||||||
|
this.showToast('Error saving hours', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Check Stripe Connect status
|
// Check Stripe Connect status
|
||||||
async checkStripeStatus() {
|
async checkStripeStatus() {
|
||||||
const statusContainer = document.getElementById('stripeStatus');
|
const statusContainer = document.getElementById('stripeStatus');
|
||||||
|
|
@ -1097,6 +1276,485 @@ const Portal = {
|
||||||
this.toast(`Invitation sent to ${email}`, 'success');
|
this.toast(`Invitation sent to ${email}`, 'success');
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== BEACONS PAGE ==========
|
||||||
|
|
||||||
|
beacons: [],
|
||||||
|
servicePoints: [],
|
||||||
|
assignments: [],
|
||||||
|
|
||||||
|
// Load beacons page data
|
||||||
|
async loadBeaconsPage() {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadBeacons(),
|
||||||
|
this.loadServicePoints(),
|
||||||
|
this.loadAssignments()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load beacons list
|
||||||
|
async loadBeacons() {
|
||||||
|
const container = document.getElementById('beaconsList');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/beacons/list.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ onlyActive: true })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.beacons = data.BEACONS || [];
|
||||||
|
this.renderBeaconsList();
|
||||||
|
} else {
|
||||||
|
console.error('[Portal] Beacons API error:', data.ERROR);
|
||||||
|
container.innerHTML = `<div class="empty-state">Error: ${data.ERROR || 'Unknown error'}</div>`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error loading beacons:', err);
|
||||||
|
container.innerHTML = `<div class="empty-state">Failed to load beacons</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Render beacons list
|
||||||
|
renderBeaconsList() {
|
||||||
|
const container = document.getElementById('beaconsList');
|
||||||
|
if (this.beacons.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty-state">No beacons yet. Add your first beacon!</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = this.beacons.map(b => `
|
||||||
|
<div class="list-group-item ${b.IsActive ? '' : 'inactive'}">
|
||||||
|
<div class="item-info">
|
||||||
|
<div class="item-name">${this.escapeHtml(b.BeaconName)}</div>
|
||||||
|
<div class="item-detail">${b.UUID || b.NamespaceId || 'No UUID'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-actions">
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="Portal.editBeacon(${b.BeaconID})">Edit</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="Portal.deleteBeacon(${b.BeaconID})">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load service points list
|
||||||
|
async loadServicePoints() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/servicepoints/list.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ BusinessID: this.config.businessId })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.servicePoints = data.SERVICEPOINTS || data.ServicePoints || [];
|
||||||
|
this.renderServicePointsList();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error loading service points:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Render service points list
|
||||||
|
renderServicePointsList() {
|
||||||
|
const container = document.getElementById('servicePointsList');
|
||||||
|
if (this.servicePoints.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty-state">No service points yet. Add tables, counters, etc.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = this.servicePoints.map(sp => `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="item-info">
|
||||||
|
<div class="item-name">${this.escapeHtml(sp.ServicePointName)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-actions">
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="Portal.editServicePoint(${sp.ServicePointID})">Edit</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="Portal.deleteServicePoint(${sp.ServicePointID})">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load assignments list
|
||||||
|
async loadAssignments() {
|
||||||
|
const container = document.getElementById('assignmentsList');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/assignments/list.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.assignments = data.ASSIGNMENTS || [];
|
||||||
|
this.renderAssignmentsList();
|
||||||
|
} else {
|
||||||
|
console.error('[Portal] Assignments API error:', data.ERROR);
|
||||||
|
container.innerHTML = `<div class="empty-state">Error: ${data.ERROR || 'Unknown error'}</div>`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error loading assignments:', err);
|
||||||
|
container.innerHTML = `<div class="empty-state">Failed to load assignments</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Render assignments list
|
||||||
|
renderAssignmentsList() {
|
||||||
|
const container = document.getElementById('assignmentsList');
|
||||||
|
if (this.assignments.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty-state">No assignments yet. Link beacons to service points.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = this.assignments.map(a => `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="item-info">
|
||||||
|
<div class="item-name">${this.escapeHtml(a.BeaconName)} → ${this.escapeHtml(a.ServicePointName)}</div>
|
||||||
|
<div class="item-detail">${a.lt_Beacon_Businesses_ServicePointNotes || ''}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-actions">
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="Portal.deleteAssignment(${a.lt_Beacon_Businesses_ServicePointID})">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show beacon modal (add/edit)
|
||||||
|
showBeaconModal(beaconId = null) {
|
||||||
|
const isEdit = beaconId !== null;
|
||||||
|
const beacon = isEdit ? this.beacons.find(b => b.BeaconID === beaconId) : {};
|
||||||
|
|
||||||
|
document.getElementById('modalTitle').textContent = isEdit ? 'Edit Beacon' : 'Add Beacon';
|
||||||
|
document.getElementById('modalBody').innerHTML = `
|
||||||
|
<form id="beaconForm" class="form">
|
||||||
|
<input type="hidden" id="beaconId" value="${beaconId || ''}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Beacon Name</label>
|
||||||
|
<input type="text" id="beaconName" class="form-input" value="${this.escapeHtml(beacon.BeaconName || '')}" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>UUID</label>
|
||||||
|
<input type="text" id="beaconUUID" class="form-input" value="${beacon.UUID || ''}" placeholder="e.g., FDA50693-A4E2-4FB1-AFCF-C6EB07647825">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="beaconIsActive" ${beacon.IsActive !== 0 ? 'checked' : ''}> Active
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">${isEdit ? 'Save Changes' : 'Add Beacon'}</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
this.showModal();
|
||||||
|
|
||||||
|
document.getElementById('beaconForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveBeacon();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save beacon
|
||||||
|
async saveBeacon() {
|
||||||
|
const beaconId = document.getElementById('beaconId').value;
|
||||||
|
const payload = {
|
||||||
|
BeaconName: document.getElementById('beaconName').value,
|
||||||
|
UUID: document.getElementById('beaconUUID').value,
|
||||||
|
IsActive: document.getElementById('beaconIsActive').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
if (beaconId) {
|
||||||
|
payload.BeaconID = parseInt(beaconId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/beacons/save.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Beacon saved!', 'success');
|
||||||
|
this.closeModal();
|
||||||
|
await this.loadBeacons();
|
||||||
|
} else {
|
||||||
|
this.toast(data.ERROR || 'Failed to save beacon', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error saving beacon:', err);
|
||||||
|
this.toast('Error saving beacon', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Edit beacon
|
||||||
|
editBeacon(beaconId) {
|
||||||
|
this.showBeaconModal(beaconId);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete beacon
|
||||||
|
async deleteBeacon(beaconId) {
|
||||||
|
if (!confirm('Are you sure you want to deactivate this beacon?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/beacons/delete.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ BeaconID: beaconId })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Beacon deactivated', 'success');
|
||||||
|
await this.loadBeacons();
|
||||||
|
} else {
|
||||||
|
this.toast(data.ERROR || 'Failed to delete beacon', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error deleting beacon:', err);
|
||||||
|
this.toast('Error deleting beacon', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show service point modal (add/edit)
|
||||||
|
showServicePointModal(servicePointId = null) {
|
||||||
|
const isEdit = servicePointId !== null;
|
||||||
|
const sp = isEdit ? this.servicePoints.find(s => s.ServicePointID === servicePointId) : {};
|
||||||
|
|
||||||
|
document.getElementById('modalTitle').textContent = isEdit ? 'Edit Service Point' : 'Add Service Point';
|
||||||
|
document.getElementById('modalBody').innerHTML = `
|
||||||
|
<form id="servicePointForm" class="form">
|
||||||
|
<input type="hidden" id="servicePointId" value="${servicePointId || ''}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" id="servicePointName" class="form-input" value="${this.escapeHtml(sp.ServicePointName || '')}" required placeholder="e.g., Table 1, Counter A">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Code (optional)</label>
|
||||||
|
<input type="text" id="servicePointCode" class="form-input" value="${sp.ServicePointCode || ''}" placeholder="Short code for display">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">${isEdit ? 'Save Changes' : 'Add Service Point'}</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
this.showModal();
|
||||||
|
|
||||||
|
document.getElementById('servicePointForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveServicePoint();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save service point
|
||||||
|
async saveServicePoint() {
|
||||||
|
const spId = document.getElementById('servicePointId').value;
|
||||||
|
const payload = {
|
||||||
|
ServicePointName: document.getElementById('servicePointName').value,
|
||||||
|
ServicePointCode: document.getElementById('servicePointCode').value
|
||||||
|
};
|
||||||
|
|
||||||
|
if (spId) {
|
||||||
|
payload.ServicePointID = parseInt(spId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/servicepoints/save.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Service point saved!', 'success');
|
||||||
|
this.closeModal();
|
||||||
|
await this.loadServicePoints();
|
||||||
|
} else {
|
||||||
|
this.toast(data.ERROR || 'Failed to save service point', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error saving service point:', err);
|
||||||
|
this.toast('Error saving service point', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Edit service point
|
||||||
|
editServicePoint(servicePointId) {
|
||||||
|
this.showServicePointModal(servicePointId);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete service point
|
||||||
|
async deleteServicePoint(servicePointId) {
|
||||||
|
if (!confirm('Are you sure you want to deactivate this service point?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/servicepoints/delete.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ServicePointID: servicePointId })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Service point deactivated', 'success');
|
||||||
|
await this.loadServicePoints();
|
||||||
|
} else {
|
||||||
|
this.toast(data.ERROR || 'Failed to delete service point', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error deleting service point:', err);
|
||||||
|
this.toast('Error deleting service point', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show assignment modal
|
||||||
|
showAssignmentModal() {
|
||||||
|
// Filter out beacons and service points that are already assigned
|
||||||
|
const assignedBeaconIds = new Set(this.assignments.map(a => a.BeaconID));
|
||||||
|
const assignedSPIds = new Set(this.assignments.map(a => a.ServicePointID));
|
||||||
|
|
||||||
|
const availableBeacons = this.beacons.filter(b => b.IsActive && !assignedBeaconIds.has(b.BeaconID));
|
||||||
|
const availableSPs = this.servicePoints.filter(sp => !assignedSPIds.has(sp.ServicePointID));
|
||||||
|
|
||||||
|
if (availableBeacons.length === 0) {
|
||||||
|
this.toast('No unassigned beacons available', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (availableSPs.length === 0) {
|
||||||
|
this.toast('No unassigned service points available', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modalTitle').textContent = 'Assign Beacon to Service Point';
|
||||||
|
document.getElementById('modalBody').innerHTML = `
|
||||||
|
<form id="assignmentForm" class="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Beacon</label>
|
||||||
|
<select id="assignBeaconId" class="form-input" required>
|
||||||
|
<option value="">Select a beacon...</option>
|
||||||
|
${availableBeacons.map(b => `<option value="${b.BeaconID}">${this.escapeHtml(b.BeaconName)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Service Point</label>
|
||||||
|
<select id="assignServicePointId" class="form-input" required>
|
||||||
|
<option value="">Select a service point...</option>
|
||||||
|
${availableSPs.map(sp => `<option value="${sp.ServicePointID}">${this.escapeHtml(sp.ServicePointName)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Notes (optional)</label>
|
||||||
|
<input type="text" id="assignNotes" class="form-input" maxlength="255" placeholder="Any notes about this assignment">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Create Assignment</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
this.showModal();
|
||||||
|
|
||||||
|
document.getElementById('assignmentForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.saveAssignment();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save assignment
|
||||||
|
async saveAssignment() {
|
||||||
|
const payload = {
|
||||||
|
BeaconID: parseInt(document.getElementById('assignBeaconId').value),
|
||||||
|
ServicePointID: parseInt(document.getElementById('assignServicePointId').value),
|
||||||
|
Notes: document.getElementById('assignNotes').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/assignments/save.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Assignment created!', 'success');
|
||||||
|
this.closeModal();
|
||||||
|
await this.loadAssignments();
|
||||||
|
} else {
|
||||||
|
this.toast(data.ERROR || 'Failed to create assignment', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error saving assignment:', err);
|
||||||
|
this.toast('Error creating assignment', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete assignment
|
||||||
|
async deleteAssignment(assignmentId) {
|
||||||
|
if (!confirm('Are you sure you want to remove this assignment?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/assignments/delete.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.config.token,
|
||||||
|
'X-Business-ID': this.config.businessId
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ lt_Beacon_Businesses_ServicePointID: assignmentId })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Assignment removed', 'success');
|
||||||
|
await this.loadAssignments();
|
||||||
|
} else {
|
||||||
|
this.toast(data.ERROR || 'Failed to remove assignment', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Portal] Error deleting assignment:', err);
|
||||||
|
this.toast('Error removing assignment', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Escape HTML helper
|
||||||
|
escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.replace(/[&<>"']/g, m => ({
|
||||||
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||||
|
}[m]));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -974,9 +974,11 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function initializeConfig() {
|
function initializeConfig() {
|
||||||
// Get business ID from URL or localStorage (optional for new business setup)
|
// Get business ID from URL only - don't use localStorage for wizard
|
||||||
|
// The wizard is for creating NEW businesses, so we shouldn't default to an existing one
|
||||||
|
// Only use bid from URL if explicitly editing an existing business
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
config.businessId = urlParams.get('bid') || localStorage.getItem('payfrit_portal_business') || null;
|
config.businessId = urlParams.get('bid') || null;
|
||||||
|
|
||||||
// Determine API base URL
|
// Determine API base URL
|
||||||
const basePath = window.location.pathname.includes('/biz.payfrit.com/')
|
const basePath = window.location.pathname.includes('/biz.payfrit.com/')
|
||||||
|
|
@ -1211,110 +1213,102 @@
|
||||||
'sunday': 6, 'sun': 6
|
'sunday': 6, 'sun': 6
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize all days as closed
|
// Initialize all days as open with defaults
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
schedule.push({ open: '09:00', close: '17:00', closed: true });
|
schedule.push({ open: '09:00', close: '17:00', closed: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hoursText || !hoursText.trim()) {
|
if (!hoursText || !hoursText.trim()) {
|
||||||
return schedule;
|
return schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
hoursText = hoursText.toLowerCase();
|
// Try to extract a time range
|
||||||
|
|
||||||
// Extract time range - find first time pattern
|
|
||||||
const timePattern = /(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?\s*(?:to|[-–])\s*(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?/i;
|
const timePattern = /(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?\s*(?:to|[-–])\s*(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?/i;
|
||||||
const timeMatch = hoursText.match(timePattern);
|
const timeMatch = hoursText.match(timePattern);
|
||||||
|
|
||||||
let openTime = '09:00';
|
|
||||||
let closeTime = '17:00';
|
|
||||||
|
|
||||||
if (timeMatch) {
|
if (timeMatch) {
|
||||||
const convertTo24Hour = (hour, minute, ampm) => {
|
const convertTo24Hour = (hour, minute, ampm) => {
|
||||||
let h = parseInt(hour);
|
let h = parseInt(hour);
|
||||||
const m = minute ? parseInt(minute) : 0;
|
const m = minute ? parseInt(minute) : 0;
|
||||||
|
|
||||||
if (ampm) {
|
if (ampm) {
|
||||||
ampm = ampm.replace(/\./g, '').toLowerCase();
|
ampm = ampm.replace(/\./g, '').toLowerCase();
|
||||||
if (ampm === 'pm' && h < 12) h += 12;
|
if (ampm === 'pm' && h < 12) h += 12;
|
||||||
if (ampm === 'am' && h === 12) h = 0;
|
if (ampm === 'am' && h === 12) h = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
|
const openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
|
||||||
closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
|
const closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
|
||||||
}
|
|
||||||
|
|
||||||
// Find which days are mentioned
|
// Apply extracted time to all days
|
||||||
const dayPattern = /(mon|monday|tue|tuesday|tues|wed|wednesday|thu|thursday|thur|thurs|fri|friday|sat|saturday|sun|sunday)/gi;
|
|
||||||
const dayMatches = hoursText.match(dayPattern) || [];
|
|
||||||
|
|
||||||
if (dayMatches.length === 0) {
|
|
||||||
// No days mentioned, assume all days open
|
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
schedule[i] = { open: openTime, close: closeTime, closed: false };
|
schedule[i] = { open: openTime, close: closeTime, closed: false };
|
||||||
}
|
}
|
||||||
return schedule;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process day ranges and individual days
|
|
||||||
const daysSet = new Set();
|
|
||||||
let i = 0;
|
|
||||||
while (i < dayMatches.length) {
|
|
||||||
const currentDay = dayMatches[i].toLowerCase();
|
|
||||||
const currentDayIdx = dayMap[currentDay];
|
|
||||||
|
|
||||||
// Check if next day forms a range
|
|
||||||
if (i < dayMatches.length - 1) {
|
|
||||||
const nextDay = dayMatches[i + 1].toLowerCase();
|
|
||||||
const betweenPattern = new RegExp(currentDay + '\\s*[-–]\\s*' + nextDay, 'i');
|
|
||||||
|
|
||||||
if (betweenPattern.test(hoursText)) {
|
|
||||||
// It's a range
|
|
||||||
const nextDayIdx = dayMap[nextDay];
|
|
||||||
for (let d = currentDayIdx; d <= nextDayIdx; d++) {
|
|
||||||
daysSet.add(d);
|
|
||||||
}
|
|
||||||
i += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Individual day
|
|
||||||
daysSet.add(currentDayIdx);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply times to the days found
|
|
||||||
daysSet.forEach(dayIdx => {
|
|
||||||
schedule[dayIdx] = { open: openTime, close: closeTime, closed: false };
|
|
||||||
});
|
|
||||||
|
|
||||||
return schedule;
|
return schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle day closed/open
|
// Toggle day closed/open - time inputs always stay editable
|
||||||
function toggleDayClosed(dayIdx) {
|
function toggleDayClosed(dayIdx) {
|
||||||
const closedCheckbox = document.getElementById(`closed_${dayIdx}`);
|
// No-op: time inputs stay editable so user can set hours before unchecking "Closed"
|
||||||
const openInput = document.getElementById(`open_${dayIdx}`);
|
}
|
||||||
const closeInput = document.getElementById(`close_${dayIdx}`);
|
|
||||||
|
|
||||||
if (closedCheckbox.checked) {
|
// Check for duplicate businesses before creating a new one
|
||||||
openInput.disabled = true;
|
async function checkForDuplicateBusiness(biz) {
|
||||||
closeInput.disabled = true;
|
try {
|
||||||
} else {
|
const response = await fetch(`${config.apiBaseUrl}/setup/checkDuplicate.cfm`, {
|
||||||
openInput.disabled = false;
|
method: 'POST',
|
||||||
closeInput.disabled = false;
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: biz.name || '',
|
||||||
|
addressLine1: biz.addressLine1 || '',
|
||||||
|
city: biz.city || '',
|
||||||
|
state: biz.state || '',
|
||||||
|
zip: biz.zip || ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.OK && result.duplicates && result.duplicates.length > 0) {
|
||||||
|
// Show duplicate warning
|
||||||
|
const dupList = result.duplicates.map(d =>
|
||||||
|
`<li><strong>${d.BusinessName}</strong> (ID: ${d.BusinessID})<br><small>${d.Address || 'No address'}</small></li>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
addMessage('ai', `
|
||||||
|
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
||||||
|
<p style="margin: 0 0 12px 0; font-weight: 600; color: #92400e;">
|
||||||
|
Possible duplicate found!
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0 0 12px 0; color: #78350f;">
|
||||||
|
A business with a similar name or address already exists:
|
||||||
|
</p>
|
||||||
|
<ul style="margin: 0 0 16px 0; padding-left: 20px; color: #78350f;">
|
||||||
|
${dupList}
|
||||||
|
</ul>
|
||||||
|
<p style="margin: 0; color: #78350f; font-size: 14px;">
|
||||||
|
You can continue to create a new business, or go back and select the existing one from the portal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking for duplicates:', error);
|
||||||
|
// Don't block the wizard if duplicate check fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Business Info
|
// Step 1: Business Info
|
||||||
function showBusinessInfoStep() {
|
async function showBusinessInfoStep() {
|
||||||
updateProgress(2);
|
updateProgress(2);
|
||||||
const biz = config.extractedData.business || {};
|
const biz = config.extractedData.business || {};
|
||||||
|
|
||||||
|
// Check for duplicate businesses before showing the form
|
||||||
|
await checkForDuplicateBusiness(biz);
|
||||||
|
|
||||||
console.log('Business data:', biz);
|
console.log('Business data:', biz);
|
||||||
|
|
||||||
// Parse address into components if it's a single string
|
// Parse address into components if it's a single string
|
||||||
|
|
@ -1348,8 +1342,8 @@
|
||||||
// What's left is AddressLine1 + City
|
// What's left is AddressLine1 + City
|
||||||
// Try to split by comma first
|
// Try to split by comma first
|
||||||
if (remaining.includes(',')) {
|
if (remaining.includes(',')) {
|
||||||
const parts = remaining.split(',').map(p => p.trim());
|
const parts = remaining.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
||||||
addressLine1 = parts[0];
|
addressLine1 = parts[0] || '';
|
||||||
city = parts.slice(1).join(', ');
|
city = parts.slice(1).join(', ');
|
||||||
} else {
|
} else {
|
||||||
// No comma - try to find last sequence of words as city
|
// No comma - try to find last sequence of words as city
|
||||||
|
|
@ -1368,6 +1362,9 @@
|
||||||
addressLine1 = remaining;
|
addressLine1 = remaining;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up any trailing commas, periods, or whitespace from city
|
||||||
|
city = city.replace(/[,.\s]+$/, '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Parsed address:', { addressLine1, city, state, zip });
|
console.log('Parsed address:', { addressLine1, city, state, zip });
|
||||||
|
|
@ -1422,13 +1419,11 @@
|
||||||
<td style="padding:8px;font-weight:500;">${day}</td>
|
<td style="padding:8px;font-weight:500;">${day}</td>
|
||||||
<td style="padding:8px;">
|
<td style="padding:8px;">
|
||||||
<input type="time" id="open_${idx}" value="${dayData.open}"
|
<input type="time" id="open_${idx}" value="${dayData.open}"
|
||||||
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;"
|
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;">
|
||||||
${dayData.closed ? 'disabled' : ''}>
|
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:8px;">
|
<td style="padding:8px;">
|
||||||
<input type="time" id="close_${idx}" value="${dayData.close}"
|
<input type="time" id="close_${idx}" value="${dayData.close}"
|
||||||
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;"
|
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;">
|
||||||
${dayData.closed ? 'disabled' : ''}>
|
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:8px;text-align:center;">
|
<td style="padding:8px;text-align:center;">
|
||||||
<input type="checkbox" id="closed_${idx}" ${dayData.closed ? 'checked' : ''}
|
<input type="checkbox" id="closed_${idx}" ${dayData.closed ? 'checked' : ''}
|
||||||
|
|
@ -1454,7 +1449,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmBusinessInfo() {
|
function confirmBusinessInfo() {
|
||||||
// Collect hours from the table
|
// Collect hours from the table - only include non-closed days
|
||||||
const hoursSchedule = [];
|
const hoursSchedule = [];
|
||||||
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
|
|
||||||
|
|
@ -1463,12 +1458,15 @@
|
||||||
const closeTime = document.getElementById(`close_${i}`).value;
|
const closeTime = document.getElementById(`close_${i}`).value;
|
||||||
const isClosed = document.getElementById(`closed_${i}`).checked;
|
const isClosed = document.getElementById(`closed_${i}`).checked;
|
||||||
|
|
||||||
hoursSchedule.push({
|
// Only add days that are not closed
|
||||||
day: dayNames[i],
|
if (!isClosed) {
|
||||||
dayId: i + 1, // 1=Monday, 7=Sunday
|
hoursSchedule.push({
|
||||||
open: isClosed ? openTime : openTime, // If closed, both times will be the same
|
day: dayNames[i],
|
||||||
close: isClosed ? openTime : closeTime // Set close = open when closed
|
dayId: i + 1, // 1=Monday, 7=Sunday
|
||||||
});
|
open: openTime,
|
||||||
|
close: closeTime
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stored data with any edits
|
// Update stored data with any edits
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue