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>
|
||||
(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){
|
||||
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";
|
||||
|
||||
async function api(path, bodyObj) {
|
||||
const fullPath = API_BASE + path;
|
||||
show({ INFO:"API CALL", PATH: fullPath, BODY: bodyObj || {} });
|
||||
bodyObj = bodyObj || {};
|
||||
bodyObj.BusinessID = BUSINESS_ID;
|
||||
|
||||
try {
|
||||
const res = await fetch(fullPath, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(bodyObj || {})
|
||||
body: JSON.stringify(bodyObj)
|
||||
});
|
||||
|
||||
const txt = await res.text();
|
||||
|
|
|
|||
|
|
@ -104,9 +104,11 @@
|
|||
|
||||
<script>
|
||||
(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){
|
||||
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) {
|
||||
const fullPath = API_BASE + path;
|
||||
bodyObj = bodyObj || {};
|
||||
bodyObj.BusinessID = BUSINESS_ID;
|
||||
|
||||
try {
|
||||
const res = await fetch(fullPath, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(bodyObj || {})
|
||||
body: JSON.stringify(bodyObj)
|
||||
});
|
||||
const txt = await res.text();
|
||||
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/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/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/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/clearAllData.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/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/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;
|
||||
|
||||
// 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 ---------- */
|
||||
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){
|
||||
apiAbort({OK=false,ERROR="no_business_selected"});
|
||||
}
|
||||
|
|
@ -49,7 +46,7 @@ RelID = int(data.lt_Beacon_Businesses_ServicePointID);
|
|||
</cfscript>
|
||||
|
||||
<!--- Confirm the row exists for this BusinessID (and capture what it was) --->
|
||||
<cfquery name="qFind" datasource="#application.datasource#">
|
||||
<cfquery name="qFind" datasource="payfrit">
|
||||
SELECT
|
||||
lt_Beacon_Businesses_ServicePointID,
|
||||
BeaconID,
|
||||
|
|
@ -73,7 +70,7 @@ RelID = int(data.lt_Beacon_Businesses_ServicePointID);
|
|||
</cfif>
|
||||
|
||||
<!--- Delete it --->
|
||||
<cfquery datasource="#application.datasource#">
|
||||
<cfquery datasource="payfrit">
|
||||
DELETE FROM lt_Beacon_Businesses_ServicePoints
|
||||
WHERE lt_Beacon_Businesses_ServicePointID =
|
||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#RelID#">
|
||||
|
|
|
|||
|
|
@ -1,91 +1,49 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<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) {
|
||||
writeOutput('{');
|
||||
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('}');
|
||||
writeOutput(serializeJSON(obj));
|
||||
abort;
|
||||
}
|
||||
|
||||
// NOTE: Do NOT use 'var' at top-level in a .cfm (local scope is function-only).
|
||||
dsn = "";
|
||||
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 (!structKeyExists(request, "BusinessID") || !isNumeric(request.BusinessID) || request.BusinessID LTE 0) {
|
||||
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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 ---------- */
|
||||
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){
|
||||
apiAbort({OK=false,ERROR="no_business_selected"});
|
||||
}
|
||||
|
|
@ -58,11 +55,11 @@ if (structKeyExists(data,"Notes")){
|
|||
</cfscript>
|
||||
|
||||
<!--- Validate Beacon belongs to Business --->
|
||||
<cfquery name="qB" datasource="#application.datasource#">
|
||||
<cfquery name="qB" datasource="payfrit">
|
||||
SELECT BeaconID
|
||||
FROM Beacons
|
||||
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
|
||||
</cfquery>
|
||||
<cfif qB.recordCount EQ 0>
|
||||
|
|
@ -71,11 +68,11 @@ if (structKeyExists(data,"Notes")){
|
|||
</cfif>
|
||||
|
||||
<!--- Validate ServicePoint belongs to Business --->
|
||||
<cfquery name="qS" datasource="#application.datasource#">
|
||||
<cfquery name="qS" datasource="payfrit">
|
||||
SELECT ServicePointID
|
||||
FROM ServicePoints
|
||||
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
|
||||
</cfquery>
|
||||
<cfif qS.recordCount EQ 0>
|
||||
|
|
@ -84,7 +81,7 @@ if (structKeyExists(data,"Notes")){
|
|||
</cfif>
|
||||
|
||||
<!--- Enforce 1:1 uniqueness --->
|
||||
<cfquery name="qBeaconTaken" datasource="#application.datasource#">
|
||||
<cfquery name="qBeaconTaken" datasource="payfrit">
|
||||
SELECT lt_Beacon_Businesses_ServicePointID
|
||||
FROM lt_Beacon_Businesses_ServicePoints
|
||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||
|
|
@ -96,7 +93,7 @@ if (structKeyExists(data,"Notes")){
|
|||
<cfabort>
|
||||
</cfif>
|
||||
|
||||
<cfquery name="qServicePointTaken" datasource="#application.datasource#">
|
||||
<cfquery name="qServicePointTaken" datasource="payfrit">
|
||||
SELECT lt_Beacon_Businesses_ServicePointID
|
||||
FROM lt_Beacon_Businesses_ServicePoints
|
||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||
|
|
@ -109,7 +106,7 @@ if (structKeyExists(data,"Notes")){
|
|||
</cfif>
|
||||
|
||||
<!--- INSERT --->
|
||||
<cfquery datasource="#application.datasource#">
|
||||
<cfquery datasource="payfrit">
|
||||
INSERT INTO lt_Beacon_Businesses_ServicePoints
|
||||
(BusinessID, BeaconID, ServicePointID,
|
||||
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="#BeaconID#">,
|
||||
<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)#">
|
||||
)
|
||||
</cfquery>
|
||||
|
||||
<cfquery name="qID" datasource="#application.datasource#">
|
||||
<cfquery name="qID" datasource="payfrit">
|
||||
SELECT LAST_INSERT_ID() AS NewID
|
||||
</cfquery>
|
||||
|
||||
|
|
|
|||
|
|
@ -75,16 +75,25 @@ try {
|
|||
userId: { value: qUser.UserID, cfsqltype: "cf_sql_integer" }
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
// Send OTP via Twilio
|
||||
// Send OTP via Twilio (if available)
|
||||
smsMessage = "Code saved (SMS skipped in dev)";
|
||||
if (structKeyExists(application, "twilioObj")) {
|
||||
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({
|
||||
"OK": true,
|
||||
"UUID": qUser.UserUUID,
|
||||
"MESSAGE": smsResult.success ? "Login code sent" : "SMS failed - please try again"
|
||||
"MESSAGE": smsMessage,
|
||||
"DEV_OTP": otp
|
||||
}));
|
||||
|
||||
} catch (any e) {
|
||||
|
|
|
|||
|
|
@ -23,11 +23,20 @@ function readJsonBody() {
|
|||
}
|
||||
|
||||
data = readJsonBody();
|
||||
httpHeaders = getHttpRequestData().headers;
|
||||
|
||||
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
|
||||
apiAbort({ OK=false, ERROR="not_logged_in" });
|
||||
// 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 (!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" });
|
||||
}
|
||||
|
||||
|
|
@ -38,19 +47,19 @@ if (!structKeyExists(data, "BeaconID") || !isNumeric(data.BeaconID) || int(data.
|
|||
beaconId = int(data.BeaconID);
|
||||
</cfscript>
|
||||
|
||||
<cfquery datasource="#application.datasource#">
|
||||
<cfquery datasource="payfrit">
|
||||
UPDATE Beacons
|
||||
SET IsActive = 0
|
||||
SET BeaconIsActive = 0
|
||||
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>
|
||||
|
||||
<!--- confirm --->
|
||||
<cfquery name="qCheck" datasource="#application.datasource#">
|
||||
SELECT BeaconID, IsActive
|
||||
<cfquery name="qCheck" datasource="payfrit">
|
||||
SELECT BeaconID, BeaconIsActive
|
||||
FROM Beacons
|
||||
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
|
||||
</cfquery>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,6 @@ function 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) {
|
||||
apiAbort({ OK=false, ERROR="no_business_selected" });
|
||||
}
|
||||
|
|
@ -38,20 +35,16 @@ if (!structKeyExists(data, "BeaconID") || !isNumeric(data.BeaconID) || int(data.
|
|||
beaconId = int(data.BeaconID);
|
||||
</cfscript>
|
||||
|
||||
<cfquery name="q" datasource="#application.datasource#">
|
||||
<cfquery name="q" datasource="payfrit">
|
||||
SELECT
|
||||
BeaconID,
|
||||
BusinessID,
|
||||
BeaconBusinessID,
|
||||
BeaconName,
|
||||
UUID,
|
||||
NamespaceId,
|
||||
InstanceId,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt
|
||||
BeaconUUID,
|
||||
BeaconIsActive
|
||||
FROM Beacons
|
||||
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
|
||||
</cfquery>
|
||||
|
||||
|
|
@ -62,14 +55,10 @@ beaconId = int(data.BeaconID);
|
|||
|
||||
<cfset beacon = {
|
||||
"BeaconID" = q.BeaconID,
|
||||
"BusinessID" = q.BusinessID,
|
||||
"BusinessID" = q.BeaconBusinessID,
|
||||
"BeaconName" = q.BeaconName,
|
||||
"UUID" = q.UUID,
|
||||
"NamespaceId" = q.NamespaceId,
|
||||
"InstanceId" = q.InstanceId,
|
||||
"IsActive" = q.IsActive,
|
||||
"CreatedAt" = q.CreatedAt,
|
||||
"UpdatedAt" = q.UpdatedAt
|
||||
"UUID" = q.BeaconUUID,
|
||||
"IsActive" = q.BeaconIsActive
|
||||
}>
|
||||
|
||||
<cfoutput>#serializeJSON({ OK=true, ERROR="", BEACON=beacon })#</cfoutput>
|
||||
|
|
|
|||
|
|
@ -10,27 +10,35 @@ function apiAbort(obj) {
|
|||
abort;
|
||||
}
|
||||
|
||||
function readJsonBody() {
|
||||
raw = toString(getHttpRequestData().content);
|
||||
if (isNull(raw) || len(trim(raw)) EQ 0) return {};
|
||||
// Read JSON body once
|
||||
data = {};
|
||||
try {
|
||||
parsed = deserializeJSON(raw);
|
||||
raw = toString(getHttpRequestData().content);
|
||||
if (len(trim(raw))) {
|
||||
data = deserializeJSON(raw);
|
||||
if (!isStruct(data)) data = {};
|
||||
}
|
||||
} catch (any e) {
|
||||
apiAbort({ OK=false, ERROR="bad_json", MESSAGE="Invalid JSON body" });
|
||||
}
|
||||
// We only accept object bodies
|
||||
if (!isStruct(parsed)) return {};
|
||||
return parsed;
|
||||
data = {};
|
||||
}
|
||||
|
||||
data = readJsonBody();
|
||||
httpHeaders = getHttpRequestData().headers;
|
||||
|
||||
// Auth/business gating should already happen in /api/Application.cfm,
|
||||
// but keep this defensive so the file is safe standalone.
|
||||
if (!structKeyExists(request, "UserID") || !isNumeric(request.UserID) || request.UserID LTE 0) {
|
||||
apiAbort({ OK=false, ERROR="not_logged_in" });
|
||||
// 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);
|
||||
}
|
||||
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" });
|
||||
}
|
||||
|
||||
|
|
@ -47,21 +55,17 @@ if (structKeyExists(data, "onlyActive")) {
|
|||
}
|
||||
</cfscript>
|
||||
|
||||
<cfquery name="q" datasource="#application.datasource#">
|
||||
<cfquery name="q" datasource="payfrit">
|
||||
SELECT
|
||||
BeaconID,
|
||||
BusinessID,
|
||||
BeaconBusinessID,
|
||||
BeaconName,
|
||||
UUID,
|
||||
NamespaceId,
|
||||
InstanceId,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt
|
||||
BeaconUUID,
|
||||
BeaconIsActive
|
||||
FROM Beacons
|
||||
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">
|
||||
WHERE BeaconBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#bizId#">
|
||||
<cfif onlyActive>
|
||||
AND IsActive = 1
|
||||
AND BeaconIsActive = 1
|
||||
</cfif>
|
||||
ORDER BY BeaconName, BeaconID
|
||||
</cfquery>
|
||||
|
|
@ -70,15 +74,11 @@ if (structKeyExists(data, "onlyActive")) {
|
|||
<cfloop query="q">
|
||||
<cfset arrayAppend(beacons, {
|
||||
"BeaconID" = q.BeaconID,
|
||||
"BusinessID" = q.BusinessID,
|
||||
"BusinessID" = q.BeaconBusinessID,
|
||||
"BeaconName" = q.BeaconName,
|
||||
"UUID" = q.UUID,
|
||||
"NamespaceId" = q.NamespaceId,
|
||||
"InstanceId" = q.InstanceId,
|
||||
"IsActive" = q.IsActive,
|
||||
"CreatedAt" = q.CreatedAt,
|
||||
"UpdatedAt" = q.UpdatedAt
|
||||
"UUID" = q.BeaconUUID,
|
||||
"IsActive" = q.BeaconIsActive
|
||||
})>
|
||||
</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">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<cftry>
|
||||
<cfscript>
|
||||
function apiAbort(obj) {
|
||||
writeOutput(serializeJSON(obj));
|
||||
|
|
@ -29,13 +30,20 @@ function normStr(v) {
|
|||
|
||||
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) {
|
||||
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) {
|
||||
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);
|
||||
uuid = structKeyExists(data, "UUID") ? normStr(data.UUID) : "";
|
||||
namespaceId = structKeyExists(data, "NamespaceId") ? normStr(data.NamespaceId) : "";
|
||||
instanceId = structKeyExists(data, "InstanceId") ? normStr(data.InstanceId) : "";
|
||||
|
||||
isActive = 1;
|
||||
if (structKeyExists(data, "IsActive")) {
|
||||
|
|
@ -60,86 +66,79 @@ if (structKeyExists(data, "IsActive")) {
|
|||
|
||||
<cfif beaconId GT 0>
|
||||
<!--- Update, scoped to this business --->
|
||||
<cfquery datasource="#application.datasource#">
|
||||
<cfquery datasource="payfrit">
|
||||
UPDATE Beacons
|
||||
SET
|
||||
BeaconName = <cfqueryparam cfsqltype="cf_sql_varchar" value="#beaconName#">,
|
||||
UUID = <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)#">,
|
||||
InstanceId = <cfqueryparam cfsqltype="cf_sql_varchar" value="#instanceId#" null="#(len(instanceId) EQ 0)#">,
|
||||
IsActive = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
|
||||
BeaconUUID = <cfqueryparam cfsqltype="cf_sql_varchar" value="#uuid#" null="#(len(uuid) EQ 0)#">,
|
||||
BeaconIsActive = <cfqueryparam cfsqltype="cf_sql_tinyint" value="#isActive#">
|
||||
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>
|
||||
|
||||
<!--- confirm it exists/belongs to business --->
|
||||
<cfquery name="qCheck" datasource="#application.datasource#">
|
||||
<cfquery name="qCheck" datasource="payfrit">
|
||||
SELECT BeaconID
|
||||
FROM Beacons
|
||||
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
|
||||
</cfquery>
|
||||
|
||||
<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>
|
||||
</cfif>
|
||||
|
||||
<cfelse>
|
||||
<!--- Insert --->
|
||||
<cfquery datasource="#application.datasource#">
|
||||
<cfquery datasource="payfrit">
|
||||
INSERT INTO Beacons (
|
||||
BusinessID,
|
||||
BeaconBusinessID,
|
||||
BeaconName,
|
||||
UUID,
|
||||
NamespaceId,
|
||||
InstanceId,
|
||||
IsActive
|
||||
BeaconUUID,
|
||||
BeaconIsActive
|
||||
) VALUES (
|
||||
<cfqueryparam cfsqltype="cf_sql_integer" value="#request.BusinessID#">,
|
||||
<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="#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#">
|
||||
)
|
||||
</cfquery>
|
||||
|
||||
<cfquery name="qId" datasource="#application.datasource#">
|
||||
<cfquery name="qId" datasource="payfrit">
|
||||
SELECT LAST_INSERT_ID() AS BeaconID
|
||||
</cfquery>
|
||||
<cfset beaconId = qId.BeaconID>
|
||||
</cfif>
|
||||
|
||||
<!--- Return saved row --->
|
||||
<cfquery name="qOut" datasource="#application.datasource#">
|
||||
<cfquery name="qOut" datasource="payfrit">
|
||||
SELECT
|
||||
BeaconID,
|
||||
BusinessID,
|
||||
BeaconBusinessID,
|
||||
BeaconName,
|
||||
UUID,
|
||||
NamespaceId,
|
||||
InstanceId,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt
|
||||
BeaconUUID,
|
||||
BeaconIsActive
|
||||
FROM Beacons
|
||||
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
|
||||
</cfquery>
|
||||
|
||||
<cfset beacon = {
|
||||
"BeaconID" = qOut.BeaconID,
|
||||
"BusinessID" = qOut.BusinessID,
|
||||
"BusinessID" = qOut.BeaconBusinessID,
|
||||
"BeaconName" = qOut.BeaconName,
|
||||
"UUID" = qOut.UUID,
|
||||
"NamespaceId" = qOut.NamespaceId,
|
||||
"InstanceId" = qOut.InstanceId,
|
||||
"IsActive" = qOut.IsActive,
|
||||
"CreatedAt" = qOut.CreatedAt,
|
||||
"UpdatedAt" = qOut.UpdatedAt
|
||||
"UUID" = qOut.BeaconUUID,
|
||||
"IsActive" = qOut.BeaconIsActive
|
||||
}>
|
||||
|
||||
<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,
|
||||
BusinessStripeAccountID,
|
||||
BusinessStripeOnboardingComplete,
|
||||
BusinessIsHiring
|
||||
BusinessIsHiring,
|
||||
BusinessHeaderImageExtension
|
||||
FROM Businesses
|
||||
WHERE BusinessID = :businessID
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
|
@ -51,17 +52,27 @@ try {
|
|||
abort;
|
||||
}
|
||||
|
||||
// Get address from Addresses table
|
||||
// Get address from Addresses table (either linked via AddressBusinessID or via Businesses.BusinessAddressID)
|
||||
qAddr = queryExecute("
|
||||
SELECT a.AddressLine1, a.AddressLine2, a.AddressCity, a.AddressZIPCode, s.tt_StateAbbreviation
|
||||
FROM Addresses a
|
||||
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
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
||||
addressStr = "";
|
||||
addressLine1 = "";
|
||||
addressCity = "";
|
||||
addressState = "";
|
||||
addressZip = "";
|
||||
if (qAddr.recordCount > 0) {
|
||||
addressLine1 = qAddr.AddressLine1;
|
||||
addressCity = qAddr.AddressCity;
|
||||
addressState = qAddr.tt_StateAbbreviation;
|
||||
addressZip = qAddr.AddressZIPCode;
|
||||
|
||||
addressParts = [];
|
||||
if (len(qAddr.AddressLine1)) arrayAppend(addressParts, qAddr.AddressLine1);
|
||||
if (len(qAddr.AddressLine2)) arrayAppend(addressParts, qAddr.AddressLine2);
|
||||
|
|
@ -116,6 +127,10 @@ try {
|
|||
"BusinessID": q.BusinessID,
|
||||
"BusinessName": q.BusinessName,
|
||||
"BusinessAddress": addressStr,
|
||||
"AddressLine1": addressLine1,
|
||||
"AddressCity": addressCity,
|
||||
"AddressState": addressState,
|
||||
"AddressZip": addressZip,
|
||||
"BusinessPhone": q.BusinessPhone,
|
||||
"BusinessHours": hoursStr,
|
||||
"BusinessHoursDetail": hoursArr,
|
||||
|
|
@ -123,6 +138,11 @@ try {
|
|||
"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["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]);
|
||||
}
|
||||
|
||||
// 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["MENU"] = { "categories": categories };
|
||||
response["TEMPLATES"] = templateLibrary;
|
||||
response["BRANDCOLOR"] = brandColor;
|
||||
response["CATEGORY_COUNT"] = arrayLen(categories);
|
||||
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
||||
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
|
||||
|
|
|
|||
|
|
@ -412,12 +412,29 @@
|
|||
</cfloop>
|
||||
</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({
|
||||
"OK": true,
|
||||
"ERROR": "",
|
||||
"Items": rows,
|
||||
"COUNT": arrayLen(rows),
|
||||
"SCHEMA": newSchemaActive ? "unified" : "legacy"
|
||||
"SCHEMA": newSchemaActive ? "unified" : "legacy",
|
||||
"BRANDCOLOR": brandColor
|
||||
})>
|
||||
|
||||
<cfcatch>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
<cfscript>
|
||||
// Save menu data from the builder UI
|
||||
// Save menu data from the builder UI (OPTIMIZED)
|
||||
// Input: BusinessID, Menu (JSON structure)
|
||||
// Output: { OK: true }
|
||||
//
|
||||
// Supports both old schema (Categories table) and new unified schema (Categories as Items)
|
||||
|
||||
response = { "OK": false };
|
||||
|
||||
// Log file for debugging
|
||||
logFile = expandPath("./saveFromBuilder.log");
|
||||
// Track which templates we've already saved options for (to avoid duplicate saves)
|
||||
savedTemplates = {};
|
||||
|
||||
// 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;
|
||||
|
||||
var optSortOrder = 0;
|
||||
|
|
@ -22,11 +20,8 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
|||
var isDefault = (structKeyExists(opt, "isDefault") && opt.isDefault) ? 1 : 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) {
|
||||
optionID = optDbId;
|
||||
// Update existing option
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemName = :name,
|
||||
|
|
@ -48,16 +43,15 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
|||
maxSelections: maxSelections
|
||||
});
|
||||
} else {
|
||||
// Insert new option
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemParentItemID, ItemName, ItemPrice,
|
||||
ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn,
|
||||
ItemRequiresChildSelection, ItemMaxNumSelectionReq
|
||||
ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemCategoryID
|
||||
) VALUES (
|
||||
:businessID, :parentID, :name, :price,
|
||||
:isDefault, :sortOrder, 1, NOW(),
|
||||
:requiresSelection, :maxSelections
|
||||
:requiresSelection, :maxSelections, 0
|
||||
)
|
||||
", {
|
||||
businessID: businessID,
|
||||
|
|
@ -74,9 +68,8 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
|||
optionID = result.newID;
|
||||
}
|
||||
|
||||
// Recursively save nested options
|
||||
if (structKeyExists(opt, "options") && isArray(opt.options) && arrayLen(opt.options) > 0) {
|
||||
saveOptionsRecursive(opt.options, optionID, businessID, logFile);
|
||||
saveOptionsRecursive(opt.options, optionID, businessID);
|
||||
}
|
||||
|
||||
optSortOrder++;
|
||||
|
|
@ -85,7 +78,6 @@ function saveOptionsRecursive(options, parentID, businessID, logFile) {
|
|||
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
fileAppend(logFile, "[#dateTimeFormat(now(), 'yyyy-mm-dd HH:nn:ss')#] Request received, length: #len(requestBody)##chr(10)#");
|
||||
|
||||
if (!len(requestBody)) {
|
||||
throw("Request body is required");
|
||||
|
|
@ -95,8 +87,6 @@ try {
|
|||
businessID = val(jsonData.BusinessID ?: 0);
|
||||
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) {
|
||||
throw("BusinessID is required");
|
||||
}
|
||||
|
|
@ -105,38 +95,29 @@ try {
|
|||
throw("Menu categories are required");
|
||||
}
|
||||
|
||||
// Log each category and its items
|
||||
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)
|
||||
// Check if new schema is active
|
||||
newSchemaActive = false;
|
||||
try {
|
||||
qCheck = queryExecute("
|
||||
SELECT COUNT(*) as cnt FROM Items
|
||||
SELECT 1 FROM Items
|
||||
WHERE ItemBusinessID = :businessID AND ItemBusinessID > 0
|
||||
LIMIT 1
|
||||
", { businessID: businessID });
|
||||
newSchemaActive = (qCheck.cnt > 0);
|
||||
newSchemaActive = (qCheck.recordCount > 0);
|
||||
} catch (any e) {
|
||||
newSchemaActive = false;
|
||||
}
|
||||
|
||||
// Process each category
|
||||
// Wrap everything in a transaction for speed and consistency
|
||||
transaction {
|
||||
catSortOrder = 0;
|
||||
for (cat in menu.categories) {
|
||||
categoryID = 0;
|
||||
categoryDbId = structKeyExists(cat, "dbId") ? val(cat.dbId) : 0;
|
||||
|
||||
if (newSchemaActive) {
|
||||
// NEW SCHEMA: Categories are Items with ParentID=0 and Template=0
|
||||
if (categoryDbId > 0) {
|
||||
categoryID = categoryDbId;
|
||||
// Update existing category Item
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemName = :name,
|
||||
|
|
@ -147,54 +128,49 @@ try {
|
|||
categoryID: categoryID,
|
||||
businessID: businessID,
|
||||
name: cat.name,
|
||||
sortOrder: val(cat.sortOrder ?: 0)
|
||||
sortOrder: catSortOrder
|
||||
});
|
||||
} else {
|
||||
// Insert new category as Item
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemName, ItemDescription,
|
||||
ItemParentItemID, ItemPrice, ItemIsActive,
|
||||
ItemSortOrder, ItemIsModifierTemplate, ItemAddedOn
|
||||
ItemSortOrder, ItemAddedOn, ItemCategoryID
|
||||
) VALUES (
|
||||
:businessID, :name, '',
|
||||
0, 0, 1,
|
||||
:sortOrder, 0, NOW()
|
||||
:sortOrder, NOW(), 0
|
||||
)
|
||||
", {
|
||||
businessID: businessID,
|
||||
name: cat.name,
|
||||
sortOrder: val(cat.sortOrder ?: 0)
|
||||
sortOrder: catSortOrder
|
||||
});
|
||||
|
||||
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)
|
||||
sortOrder: catSortOrder
|
||||
});
|
||||
} else {
|
||||
queryExecute("
|
||||
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategoryDescription, CategorySortOrder)
|
||||
VALUES (:businessID, :name, :description, :sortOrder)
|
||||
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategorySortOrder, CategoryAddedOn)
|
||||
VALUES (:businessID, :name, :sortOrder, NOW())
|
||||
", {
|
||||
businessID: businessID,
|
||||
name: cat.name,
|
||||
description: cat.description ?: "",
|
||||
sortOrder: val(cat.sortOrder ?: 0)
|
||||
sortOrder: catSortOrder
|
||||
});
|
||||
|
||||
result = queryExecute("SELECT LAST_INSERT_ID() as newID");
|
||||
|
|
@ -202,8 +178,9 @@ try {
|
|||
}
|
||||
}
|
||||
|
||||
// Process items in this category
|
||||
// Process items
|
||||
if (structKeyExists(cat, "items") && isArray(cat.items)) {
|
||||
itemSortOrder = 0;
|
||||
for (item in cat.items) {
|
||||
itemID = 0;
|
||||
itemDbId = structKeyExists(item, "dbId") ? val(item.dbId) : 0;
|
||||
|
|
@ -212,7 +189,6 @@ try {
|
|||
itemID = itemDbId;
|
||||
|
||||
if (newSchemaActive) {
|
||||
// Update existing item - set parent to category Item
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemName = :name,
|
||||
|
|
@ -227,10 +203,9 @@ try {
|
|||
description: item.description ?: "",
|
||||
price: val(item.price ?: 0),
|
||||
categoryID: categoryID,
|
||||
sortOrder: val(item.sortOrder ?: 0)
|
||||
sortOrder: itemSortOrder
|
||||
});
|
||||
} else {
|
||||
// Update existing item - old schema with CategoryID
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemName = :name,
|
||||
|
|
@ -245,19 +220,18 @@ try {
|
|||
description: item.description ?: "",
|
||||
price: val(item.price ?: 0),
|
||||
categoryID: categoryID,
|
||||
sortOrder: val(item.sortOrder ?: 0)
|
||||
sortOrder: itemSortOrder
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Insert new item
|
||||
if (newSchemaActive) {
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemParentItemID, ItemName, ItemDescription,
|
||||
ItemPrice, ItemSortOrder, ItemIsActive, ItemAddedOn
|
||||
ItemPrice, ItemSortOrder, ItemIsActive, ItemAddedOn, ItemCategoryID
|
||||
) VALUES (
|
||||
:businessID, :categoryID, :name, :description,
|
||||
:price, :sortOrder, 1, NOW()
|
||||
:price, :sortOrder, 1, NOW(), 0
|
||||
)
|
||||
", {
|
||||
businessID: businessID,
|
||||
|
|
@ -265,16 +239,16 @@ try {
|
|||
name: item.name,
|
||||
description: item.description ?: "",
|
||||
price: val(item.price ?: 0),
|
||||
sortOrder: val(item.sortOrder ?: 0)
|
||||
sortOrder: itemSortOrder
|
||||
});
|
||||
} else {
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemCategoryID, ItemName, ItemDescription,
|
||||
ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID
|
||||
ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID, ItemAddedOn
|
||||
) VALUES (
|
||||
:businessID, :categoryID, :name, :description,
|
||||
:price, :sortOrder, 1, 0
|
||||
:price, :sortOrder, 1, 0, NOW()
|
||||
)
|
||||
", {
|
||||
businessID: businessID,
|
||||
|
|
@ -282,7 +256,7 @@ try {
|
|||
name: item.name,
|
||||
description: item.description ?: "",
|
||||
price: val(item.price ?: 0),
|
||||
sortOrder: val(item.sortOrder ?: 0)
|
||||
sortOrder: itemSortOrder
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -290,24 +264,19 @@ try {
|
|||
itemID = result.newID;
|
||||
}
|
||||
|
||||
// Handle template links for modifiers
|
||||
if (structKeyExists(item, "modifiers") && isArray(item.modifiers)) {
|
||||
// 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 });
|
||||
queryExecute("DELETE FROM ItemTemplateLinks WHERE ItemID = :itemID", { itemID: itemID });
|
||||
|
||||
modSortOrder = 0;
|
||||
for (mod in item.modifiers) {
|
||||
modDbId = structKeyExists(mod, "dbId") ? val(mod.dbId) : 0;
|
||||
|
||||
// Get selection rules (for modifier groups)
|
||||
requiresSelection = (structKeyExists(mod, "requiresSelection") && mod.requiresSelection) ? 1 : 0;
|
||||
maxSelections = structKeyExists(mod, "maxSelections") ? val(mod.maxSelections) : 0;
|
||||
|
||||
// Check if this is a template reference
|
||||
if (structKeyExists(mod, "isTemplate") && mod.isTemplate && modDbId > 0) {
|
||||
// Create template link
|
||||
// This is a template reference - create link
|
||||
queryExecute("
|
||||
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
|
||||
VALUES (:itemID, :templateID, :sortOrder)
|
||||
|
|
@ -318,7 +287,7 @@ try {
|
|||
sortOrder: modSortOrder
|
||||
});
|
||||
|
||||
// Also update the template's selection rules
|
||||
// Update template's selection rules
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemRequiresChildSelection = :requiresSelection,
|
||||
|
|
@ -330,13 +299,15 @@ try {
|
|||
maxSelections: maxSelections
|
||||
});
|
||||
|
||||
// Save the template's options (children) recursively
|
||||
// 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)) {
|
||||
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);
|
||||
saveOptionsRecursive(mod.options, modDbId, businessID);
|
||||
}
|
||||
}
|
||||
} else if (modDbId > 0) {
|
||||
// Update existing direct modifier
|
||||
// Direct modifier (not a template) - update it
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemName = :name,
|
||||
|
|
@ -344,10 +315,12 @@ try {
|
|||
ItemIsCheckedByDefault = :isDefault,
|
||||
ItemSortOrder = :sortOrder,
|
||||
ItemRequiresChildSelection = :requiresSelection,
|
||||
ItemMaxNumSelectionReq = :maxSelections
|
||||
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,
|
||||
|
|
@ -356,22 +329,20 @@ try {
|
|||
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);
|
||||
saveOptionsRecursive(mod.options, modDbId, businessID);
|
||||
}
|
||||
} else {
|
||||
// Insert new direct modifier (non-template)
|
||||
// New direct modifier - insert it
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemParentItemID, ItemName, ItemPrice,
|
||||
ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive, ItemAddedOn,
|
||||
ItemRequiresChildSelection, ItemMaxNumSelectionReq
|
||||
ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemCategoryID
|
||||
) VALUES (
|
||||
:businessID, :parentID, :name, :price,
|
||||
:isDefault, :sortOrder, 1, NOW(),
|
||||
:requiresSelection, :maxSelections
|
||||
:requiresSelection, :maxSelections, 0
|
||||
)
|
||||
", {
|
||||
businessID: businessID,
|
||||
|
|
@ -384,19 +355,20 @@ try {
|
|||
maxSelections: maxSelections
|
||||
});
|
||||
|
||||
// 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);
|
||||
saveOptionsRecursive(mod.options, newModID, businessID);
|
||||
}
|
||||
}
|
||||
modSortOrder++;
|
||||
}
|
||||
}
|
||||
itemSortOrder++;
|
||||
}
|
||||
}
|
||||
catSortOrder++;
|
||||
}
|
||||
}
|
||||
|
||||
response = { "OK": true, "SCHEMA": newSchemaActive ? "unified" : "legacy" };
|
||||
|
|
|
|||
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 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>
|
||||
function apiAbort(payload) {
|
||||
|
|
@ -8,129 +10,85 @@ function apiAbort(payload) {
|
|||
abort;
|
||||
}
|
||||
|
||||
// Resolve BusinessID tolerant MVP way
|
||||
bizId = 0;
|
||||
|
||||
if (structKeyExists(request, "BusinessID") && isNumeric(request.BusinessID)) {
|
||||
bizId = int(request.BusinessID);
|
||||
}
|
||||
|
||||
if (bizId LTE 0) {
|
||||
// Read JSON body once
|
||||
data = {};
|
||||
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);
|
||||
data = deserializeJSON(raw);
|
||||
if (!isStruct(data)) data = {};
|
||||
}
|
||||
}
|
||||
} catch (any e) {}
|
||||
} catch (any e) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
if (bizId LTE 0 && structKeyExists(url, "BusinessID") && isNumeric(url.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);
|
||||
}
|
||||
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": "missing_businessid", "DETAIL": "" });
|
||||
apiAbort({ "OK": false, "ERROR": "missing_businessid" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Detect the correct business FK column in ServicePoints
|
||||
qCols = queryExecute(
|
||||
"
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'ServicePoints'
|
||||
",
|
||||
[],
|
||||
{ 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;
|
||||
// Default behavior: only active service points unless onlyActive is explicitly false/0
|
||||
onlyActive = true;
|
||||
if (structKeyExists(data, "onlyActive")) {
|
||||
if (isBoolean(data.onlyActive)) {
|
||||
onlyActive = data.onlyActive;
|
||||
} else if (isNumeric(data.onlyActive)) {
|
||||
onlyActive = (int(data.onlyActive) EQ 1);
|
||||
} else if (isSimpleValue(data.onlyActive)) {
|
||||
onlyActive = (lcase(trim(toString(data.onlyActive))) EQ "true");
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
<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) {
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -49,21 +50,30 @@ try {
|
|||
throw(message="userId is required to create new business");
|
||||
}
|
||||
|
||||
// Create address record first (use extracted address fields)
|
||||
addressLine1 = structKeyExists(biz, "addressLine1") ? biz.addressLine1 : "";
|
||||
city = structKeyExists(biz, "city") ? biz.city : "";
|
||||
state = structKeyExists(biz, "state") ? biz.state : "";
|
||||
zip = structKeyExists(biz, "zip") ? biz.zip : "";
|
||||
// Extract phone number
|
||||
bizPhone = structKeyExists(biz, "phone") && isSimpleValue(biz.phone) ? trim(biz.phone) : "";
|
||||
|
||||
// Create address record first (use extracted address fields) - safely extract as simple values
|
||||
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
|
||||
stateID = 0;
|
||||
response.steps.append("State value received: '" & state & "' (len: " & len(state) & ")");
|
||||
if (len(state)) {
|
||||
qState = queryExecute("
|
||||
SELECT tt_StateID FROM tt_States WHERE tt_StateAbbreviation = :abbr
|
||||
", { abbr: uCase(state) }, { datasource: "payfrit" });
|
||||
|
||||
response.steps.append("State lookup for '" & uCase(state) & "' found " & qState.recordCount & " records");
|
||||
if (qState.recordCount > 0) {
|
||||
stateID = qState.tt_StateID;
|
||||
response.steps.append("Using stateID: " & stateID);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,12 +93,13 @@ try {
|
|||
addressId = qNewAddr.id;
|
||||
response.steps.append("Created address record (ID: " & addressId & ")");
|
||||
|
||||
// Create new business with address link
|
||||
// Create new business with address link and phone
|
||||
queryExecute("
|
||||
INSERT INTO Businesses (BusinessName, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessAddedOn)
|
||||
VALUES (:name, :userId, :addressId, :deliveryZips, NOW())
|
||||
INSERT INTO Businesses (BusinessName, BusinessPhone, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessAddedOn)
|
||||
VALUES (:name, :phone, :userId, :addressId, :deliveryZips, NOW())
|
||||
", {
|
||||
name: biz.name,
|
||||
name: bizName,
|
||||
phone: bizPhone,
|
||||
userId: userId,
|
||||
addressId: addressId,
|
||||
deliveryZips: len(zip) ? zip : ""
|
||||
|
|
@ -96,7 +107,7 @@ try {
|
|||
|
||||
qNewBiz = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||
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
|
||||
queryExecute("
|
||||
|
|
@ -114,9 +125,11 @@ try {
|
|||
|
||||
for (i = 1; i <= arrayLen(hoursSchedule); i++) {
|
||||
dayData = hoursSchedule[i];
|
||||
if (!isStruct(dayData)) continue;
|
||||
|
||||
dayID = structKeyExists(dayData, "dayId") ? val(dayData.dayId) : 0;
|
||||
openTime = structKeyExists(dayData, "open") ? dayData.open : "09:00";
|
||||
closeTime = structKeyExists(dayData, "close") ? dayData.close : "17:00";
|
||||
openTime = structKeyExists(dayData, "open") && isSimpleValue(dayData.open) ? dayData.open : "09:00";
|
||||
closeTime = structKeyExists(dayData, "close") && isSimpleValue(dayData.close) ? dayData.close : "17:00";
|
||||
|
||||
// Convert HH:MM to HH:MM:SS if needed
|
||||
if (len(openTime) == 5) openTime = openTime & ":00";
|
||||
|
|
@ -158,9 +171,13 @@ try {
|
|||
|
||||
for (i = 1; i <= arrayLen(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;
|
||||
options = structKeyExists(tmpl, "options") ? tmpl.options : [];
|
||||
options = structKeyExists(tmpl, "options") && isArray(tmpl.options) ? tmpl.options : [];
|
||||
|
||||
// Debug: Log options info
|
||||
response.steps.append("Template '" & tmplName & "' has " & arrayLen(options) & " options (type: " & (isArray(options) ? "array" : "other") & ")");
|
||||
|
|
@ -204,13 +221,14 @@ try {
|
|||
optionOrder = 1;
|
||||
for (j = 1; j <= arrayLen(options); j++) {
|
||||
opt = options[j];
|
||||
// Safety check: ensure opt is a struct with a name
|
||||
if (!isStruct(opt) || !structKeyExists(opt, "name") || !len(opt.name)) {
|
||||
continue;
|
||||
}
|
||||
// Safety check: ensure opt is a struct with a name that's a simple value
|
||||
if (!isStruct(opt)) continue;
|
||||
if (!structKeyExists(opt, "name")) continue;
|
||||
if (!isSimpleValue(opt.name)) continue;
|
||||
if (!len(opt.name)) continue;
|
||||
|
||||
optName = opt.name;
|
||||
optPrice = structKeyExists(opt, "price") ? val(opt.price) : 0;
|
||||
optPrice = structKeyExists(opt, "price") && isSimpleValue(opt.price) ? val(opt.price) : 0;
|
||||
|
||||
qOpt = queryExecute("
|
||||
SELECT ItemID FROM Items
|
||||
|
|
@ -246,7 +264,11 @@ try {
|
|||
catOrder = 1;
|
||||
for (c = 1; c <= arrayLen(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
|
||||
qCat = queryExecute("
|
||||
|
|
@ -293,15 +315,37 @@ try {
|
|||
|
||||
for (n = 1; n <= arrayLen(items); n++) {
|
||||
item = items[n];
|
||||
itemName = item.name;
|
||||
itemDesc = structKeyExists(item, "description") ? item.description : "";
|
||||
itemPrice = structKeyExists(item, "price") ? val(item.price) : 0;
|
||||
itemCategory = structKeyExists(item, "category") ? item.category : "";
|
||||
itemModifiers = structKeyExists(item, "modifiers") ? item.modifiers : [];
|
||||
if (!isStruct(item)) continue;
|
||||
|
||||
// Safely extract all item fields - ensure they're simple values
|
||||
itemName = structKeyExists(item, "name") && isSimpleValue(item.name) ? item.name : "";
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -363,7 +407,15 @@ try {
|
|||
// Link modifier templates to this item
|
||||
modOrder = 1;
|
||||
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)) {
|
||||
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>
|
||||
<span>Team</span>
|
||||
</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">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
|
|
@ -341,6 +349,50 @@
|
|||
</div>
|
||||
</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 -->
|
||||
<section class="page" id="page-settings">
|
||||
<div class="settings-grid">
|
||||
|
|
@ -349,37 +401,44 @@
|
|||
<h3>Business Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="businessInfoForm" class="form">
|
||||
<form id="businessInfoForm" class="form" onsubmit="Portal.saveBusinessInfo(event)">
|
||||
<div class="form-group">
|
||||
<label>Business Name</label>
|
||||
<input type="text" id="settingBusinessName" class="form-input">
|
||||
<input type="text" id="settingBusinessName" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<textarea id="settingDescription" class="form-textarea" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Address</label>
|
||||
<input type="text" id="settingAddress" class="form-input">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Phone</label>
|
||||
<input type="tel" id="settingPhone" class="form-input">
|
||||
<input type="tel" id="settingPhone" class="form-input" placeholder="(555) 123-4567">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="settingEmail" class="form-input">
|
||||
<label>Street Address</label>
|
||||
<input type="text" id="settingAddressLine1" class="form-input" placeholder="123 Main St">
|
||||
</div>
|
||||
<div class="form-row" style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:12px;">
|
||||
<div class="form-group">
|
||||
<label>City</label>
|
||||
<input type="text" id="settingCity" class="form-input" placeholder="Los Angeles">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>State</label>
|
||||
<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>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<button type="submit" class="btn btn-primary" id="saveBusinessBtn">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button class="btn btn-sm btn-secondary" onclick="Portal.saveHours()">Save Hours</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="hours-list" id="hoursEditor">
|
||||
|
|
|
|||
|
|
@ -652,13 +652,18 @@
|
|||
.toolbar-btn.primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toolbar-btn.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.toolbar-btn.primary:active {
|
||||
background: var(--primary-dark);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.toolbar-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
|
@ -790,17 +795,11 @@
|
|||
</button>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
Import
|
||||
</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
|
||||
Outline
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar-group" style="margin-left: auto;">
|
||||
|
|
@ -874,6 +873,26 @@
|
|||
<strong id="statPhotosMissing" style="color: var(--warning);">0</strong>
|
||||
</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>
|
||||
|
||||
<!-- Main Canvas -->
|
||||
|
|
@ -2836,6 +2855,242 @@
|
|||
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
|
||||
async loadMenu() {
|
||||
try {
|
||||
|
|
@ -2856,6 +3111,12 @@
|
|||
console.log('[MenuBuilder] Menu categories:', this.menu.categories?.length || 0);
|
||||
// Store templates from API (default to empty array if not provided)
|
||||
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.render();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
--primary: #6366f1;
|
||||
--primary-dark: #4f46e5;
|
||||
--primary-light: #818cf8;
|
||||
--primary-hover: #4f46e5;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
|
|
@ -23,6 +24,13 @@
|
|||
--radius: 8px;
|
||||
--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);
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
display: flex;
|
||||
|
|
|
|||
664
portal/portal.js
664
portal/portal.js
|
|
@ -225,6 +225,9 @@ const Portal = {
|
|||
case 'team':
|
||||
await this.loadTeam();
|
||||
break;
|
||||
case 'beacons':
|
||||
await this.loadBeaconsPage();
|
||||
break;
|
||||
case 'settings':
|
||||
await this.loadSettings();
|
||||
break;
|
||||
|
|
@ -652,6 +655,9 @@ const Portal = {
|
|||
async loadSettings() {
|
||||
console.log('[Portal] Loading settings...');
|
||||
|
||||
// Load states dropdown first
|
||||
await this.loadStatesDropdown();
|
||||
|
||||
// Load business info
|
||||
await this.loadBusinessInfo();
|
||||
|
||||
|
|
@ -659,6 +665,24 @@ const Portal = {
|
|||
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
|
||||
async loadBusinessInfo() {
|
||||
try {
|
||||
|
|
@ -671,17 +695,172 @@ const Portal = {
|
|||
|
||||
if (data.OK && data.BUSINESS) {
|
||||
const biz = data.BUSINESS;
|
||||
this.currentBusiness = biz;
|
||||
|
||||
// Populate form fields
|
||||
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('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) {
|
||||
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
|
||||
async checkStripeStatus() {
|
||||
const statusContainer = document.getElementById('stripeStatus');
|
||||
|
|
@ -1097,6 +1276,485 @@ const Portal = {
|
|||
this.toast(`Invitation sent to ${email}`, 'success');
|
||||
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() {
|
||||
// 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);
|
||||
config.businessId = urlParams.get('bid') || localStorage.getItem('payfrit_portal_business') || null;
|
||||
config.businessId = urlParams.get('bid') || null;
|
||||
|
||||
// Determine API base URL
|
||||
const basePath = window.location.pathname.includes('/biz.payfrit.com/')
|
||||
|
|
@ -1211,110 +1213,102 @@
|
|||
'sunday': 6, 'sun': 6
|
||||
};
|
||||
|
||||
// Initialize all days as closed
|
||||
// Initialize all days as open with defaults
|
||||
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()) {
|
||||
return schedule;
|
||||
}
|
||||
|
||||
hoursText = hoursText.toLowerCase();
|
||||
|
||||
// Extract time range - find first time pattern
|
||||
// Try to extract a time range
|
||||
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);
|
||||
|
||||
let openTime = '09:00';
|
||||
let closeTime = '17:00';
|
||||
|
||||
if (timeMatch) {
|
||||
const convertTo24Hour = (hour, minute, ampm) => {
|
||||
let h = parseInt(hour);
|
||||
const m = minute ? parseInt(minute) : 0;
|
||||
|
||||
if (ampm) {
|
||||
ampm = ampm.replace(/\./g, '').toLowerCase();
|
||||
if (ampm === 'pm' && h < 12) h += 12;
|
||||
if (ampm === 'am' && h === 12) h = 0;
|
||||
}
|
||||
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
|
||||
closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
|
||||
}
|
||||
const openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
|
||||
const closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
|
||||
|
||||
// Find which days are mentioned
|
||||
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
|
||||
// Apply extracted time to all days
|
||||
for (let i = 0; i < 7; i++) {
|
||||
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;
|
||||
}
|
||||
// Toggle day closed/open - time inputs always stay editable
|
||||
function toggleDayClosed(dayIdx) {
|
||||
// No-op: time inputs stay editable so user can set hours before unchecking "Closed"
|
||||
}
|
||||
|
||||
// Individual day
|
||||
daysSet.add(currentDayIdx);
|
||||
i++;
|
||||
}
|
||||
|
||||
// Apply times to the days found
|
||||
daysSet.forEach(dayIdx => {
|
||||
schedule[dayIdx] = { open: openTime, close: closeTime, closed: false };
|
||||
// Check for duplicate businesses before creating a new one
|
||||
async function checkForDuplicateBusiness(biz) {
|
||||
try {
|
||||
const response = await fetch(`${config.apiBaseUrl}/setup/checkDuplicate.cfm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: biz.name || '',
|
||||
addressLine1: biz.addressLine1 || '',
|
||||
city: biz.city || '',
|
||||
state: biz.state || '',
|
||||
zip: biz.zip || ''
|
||||
})
|
||||
});
|
||||
|
||||
return schedule;
|
||||
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>
|
||||
`);
|
||||
}
|
||||
|
||||
// Toggle day closed/open
|
||||
function toggleDayClosed(dayIdx) {
|
||||
const closedCheckbox = document.getElementById(`closed_${dayIdx}`);
|
||||
const openInput = document.getElementById(`open_${dayIdx}`);
|
||||
const closeInput = document.getElementById(`close_${dayIdx}`);
|
||||
|
||||
if (closedCheckbox.checked) {
|
||||
openInput.disabled = true;
|
||||
closeInput.disabled = true;
|
||||
} else {
|
||||
openInput.disabled = false;
|
||||
closeInput.disabled = false;
|
||||
} catch (error) {
|
||||
console.error('Error checking for duplicates:', error);
|
||||
// Don't block the wizard if duplicate check fails
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Business Info
|
||||
function showBusinessInfoStep() {
|
||||
async function showBusinessInfoStep() {
|
||||
updateProgress(2);
|
||||
const biz = config.extractedData.business || {};
|
||||
|
||||
// Check for duplicate businesses before showing the form
|
||||
await checkForDuplicateBusiness(biz);
|
||||
|
||||
console.log('Business data:', biz);
|
||||
|
||||
// Parse address into components if it's a single string
|
||||
|
|
@ -1348,8 +1342,8 @@
|
|||
// What's left is AddressLine1 + City
|
||||
// Try to split by comma first
|
||||
if (remaining.includes(',')) {
|
||||
const parts = remaining.split(',').map(p => p.trim());
|
||||
addressLine1 = parts[0];
|
||||
const parts = remaining.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
||||
addressLine1 = parts[0] || '';
|
||||
city = parts.slice(1).join(', ');
|
||||
} else {
|
||||
// No comma - try to find last sequence of words as city
|
||||
|
|
@ -1368,6 +1362,9 @@
|
|||
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 });
|
||||
|
|
@ -1422,13 +1419,11 @@
|
|||
<td style="padding:8px;font-weight:500;">${day}</td>
|
||||
<td style="padding:8px;">
|
||||
<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;"
|
||||
${dayData.closed ? 'disabled' : ''}>
|
||||
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;">
|
||||
</td>
|
||||
<td style="padding:8px;">
|
||||
<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;"
|
||||
${dayData.closed ? 'disabled' : ''}>
|
||||
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;">
|
||||
</td>
|
||||
<td style="padding:8px;text-align:center;">
|
||||
<input type="checkbox" id="closed_${idx}" ${dayData.closed ? 'checked' : ''}
|
||||
|
|
@ -1454,7 +1449,7 @@
|
|||
}
|
||||
|
||||
function confirmBusinessInfo() {
|
||||
// Collect hours from the table
|
||||
// Collect hours from the table - only include non-closed days
|
||||
const hoursSchedule = [];
|
||||
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
|
||||
|
|
@ -1463,13 +1458,16 @@
|
|||
const closeTime = document.getElementById(`close_${i}`).value;
|
||||
const isClosed = document.getElementById(`closed_${i}`).checked;
|
||||
|
||||
// Only add days that are not closed
|
||||
if (!isClosed) {
|
||||
hoursSchedule.push({
|
||||
day: dayNames[i],
|
||||
dayId: i + 1, // 1=Monday, 7=Sunday
|
||||
open: isClosed ? openTime : openTime, // If closed, both times will be the same
|
||||
close: isClosed ? openTime : closeTime // Set close = open when closed
|
||||
open: openTime,
|
||||
close: closeTime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update stored data with any edits
|
||||
config.extractedData.business = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue