App Store Version 2: Multi-menu support, beacon lookup, category scheduling
Features: - Multi-menu support with time-based availability - Menu hours validation against business operating hours - Setup wizard now creates Menu records and links categories - New menus.cfm API for menu CRUD operations - Category schedule filtering (day/time based visibility) - Beacon UUID lookup API for customer app - Parent/child business relationships for franchises - Category listing API for menu builder Portal improvements: - Menu builder theming to match admin UI - Brand color picker fix - Header image preview improvements API fixes: - Filter demo/hidden businesses from restaurant list - Improved error handling throughout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
72f5b7eb12
commit
c2ae037e71
35 changed files with 2599 additions and 175 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
<cfsetting enablecfoutputonly="true">
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
|
||||||
<!---
|
<!---
|
||||||
Payfrit API Application.cfm (updated)
|
Payfrit API Application.cfm (updated 2026-01-22)
|
||||||
|
|
||||||
FIX: Provide a DEFAULT datasource so endpoints can call queryExecute()
|
FIX: Provide a DEFAULT datasource so endpoints can call queryExecute()
|
||||||
without specifying { datasource="payfrit" } every time.
|
without specifying { datasource="payfrit" } every time.
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
<!--- Magic OTP bypass for App Store review (set to true to enable 123456 as universal OTP) --->
|
<!--- Magic OTP bypass for App Store review (set to true to enable 123456 as universal OTP) --->
|
||||||
<cfset application.MAGIC_OTP_ENABLED = true>
|
<cfset application.MAGIC_OTP_ENABLED = false>
|
||||||
<cfset application.MAGIC_OTP_CODE = "123456">
|
<cfset application.MAGIC_OTP_CODE = "123456">
|
||||||
|
|
||||||
<!--- Initialize Twilio for SMS --->
|
<!--- Initialize Twilio for SMS --->
|
||||||
|
|
@ -95,6 +95,7 @@ if (len(request._api_path)) {
|
||||||
|
|
||||||
if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/businesses/get.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/get.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/businesses/getChildren.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/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/businesses/updateHours.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/servicepoints/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -109,6 +110,7 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/list_all.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/beacons/reassign_all.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/beacons/reassign_all.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/beacons/lookup.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/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/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/assignments/delete.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -173,6 +175,8 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/updateStations.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/updateStations.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/menu/uploadHeader.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/menu/uploadHeader.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/menu/listCategories.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/menu/saveCategory.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/businesses/saveBrandColor.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/businesses/saveBrandColor.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/create.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/create.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
|
|
|
||||||
75
api/admin/addCategoryScheduleFields.cfm
Normal file
75
api/admin/addCategoryScheduleFields.cfm
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Add Schedule Fields to Categories Table
|
||||||
|
*
|
||||||
|
* Adds time-based scheduling fields:
|
||||||
|
* - CategoryScheduleStart: TIME - Start time when category is available (e.g., 06:00:00 for breakfast)
|
||||||
|
* - CategoryScheduleEnd: TIME - End time when category stops being available (e.g., 11:00:00)
|
||||||
|
* - CategoryScheduleDays: VARCHAR(20) - Comma-separated list of day IDs (1=Sun, 2=Mon, etc.) or NULL for all days
|
||||||
|
*
|
||||||
|
* Run this once to migrate the schema.
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if columns already exist
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT COLUMN_NAME
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'payfrit'
|
||||||
|
AND TABLE_NAME = 'Categories'
|
||||||
|
AND COLUMN_NAME IN ('CategoryScheduleStart', 'CategoryScheduleEnd', 'CategoryScheduleDays')
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
existingCols = valueList(qCheck.COLUMN_NAME);
|
||||||
|
|
||||||
|
added = [];
|
||||||
|
|
||||||
|
// Add CategoryScheduleStart if not exists
|
||||||
|
if (!listFindNoCase(existingCols, "CategoryScheduleStart")) {
|
||||||
|
queryExecute("
|
||||||
|
ALTER TABLE Categories
|
||||||
|
ADD COLUMN CategoryScheduleStart TIME NULL
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
arrayAppend(added, "CategoryScheduleStart");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CategoryScheduleEnd if not exists
|
||||||
|
if (!listFindNoCase(existingCols, "CategoryScheduleEnd")) {
|
||||||
|
queryExecute("
|
||||||
|
ALTER TABLE Categories
|
||||||
|
ADD COLUMN CategoryScheduleEnd TIME NULL
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
arrayAppend(added, "CategoryScheduleEnd");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CategoryScheduleDays if not exists
|
||||||
|
if (!listFindNoCase(existingCols, "CategoryScheduleDays")) {
|
||||||
|
queryExecute("
|
||||||
|
ALTER TABLE Categories
|
||||||
|
ADD COLUMN CategoryScheduleDays VARCHAR(20) NULL
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
arrayAppend(added, "CategoryScheduleDays");
|
||||||
|
}
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["ColumnsAdded"] = added;
|
||||||
|
response["AlreadyExisted"] = listToArray(existingCols);
|
||||||
|
response["MESSAGE"] = arrayLen(added) > 0
|
||||||
|
? "Added #arrayLen(added)# column(s): #arrayToList(added)#"
|
||||||
|
: "All columns already exist";
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = "server_error";
|
||||||
|
response["MESSAGE"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
30
api/admin/addLatLng.cfm
Normal file
30
api/admin/addLatLng.cfm
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
try {
|
||||||
|
// Check if columns already exist
|
||||||
|
checkCols = queryExecute(
|
||||||
|
"SHOW COLUMNS FROM Addresses LIKE 'AddressLat'",
|
||||||
|
[],
|
||||||
|
{ datasource = "payfrit" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (checkCols.recordCount EQ 0) {
|
||||||
|
// Add the columns
|
||||||
|
queryExecute(
|
||||||
|
"ALTER TABLE Addresses
|
||||||
|
ADD COLUMN AddressLat DECIMAL(10,7) NULL,
|
||||||
|
ADD COLUMN AddressLng DECIMAL(10,7) NULL",
|
||||||
|
[],
|
||||||
|
{ datasource = "payfrit" }
|
||||||
|
);
|
||||||
|
writeOutput(serializeJSON({ "OK": true, "MESSAGE": "Columns added successfully" }));
|
||||||
|
} else {
|
||||||
|
writeOutput(serializeJSON({ "OK": true, "MESSAGE": "Columns already exist" }));
|
||||||
|
}
|
||||||
|
} catch (any e) {
|
||||||
|
writeOutput(serializeJSON({ "OK": false, "ERROR": e.message }));
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
85
api/admin/createMenusTable.cfm
Normal file
85
api/admin/createMenusTable.cfm
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Create Menus table for multiple menu support
|
||||||
|
*
|
||||||
|
* Menus can have:
|
||||||
|
* - Name (e.g., "Lunch Menu", "Dinner Menu", "Happy Hour")
|
||||||
|
* - Active days (bitmask linked to business hours days)
|
||||||
|
* - Start/End times for when the menu is available
|
||||||
|
* - Sort order for display
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if Menus table exists
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT 1 FROM INFORMATION_SCHEMA.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = 'payfrit'
|
||||||
|
AND TABLE_NAME = 'Menus'
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount > 0) {
|
||||||
|
response["OK"] = true;
|
||||||
|
response["MESSAGE"] = "Menus table already exists";
|
||||||
|
} else {
|
||||||
|
// Create Menus table
|
||||||
|
queryExecute("
|
||||||
|
CREATE TABLE Menus (
|
||||||
|
MenuID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
MenuBusinessID INT NOT NULL,
|
||||||
|
MenuName VARCHAR(100) NOT NULL,
|
||||||
|
MenuDescription VARCHAR(500) NULL,
|
||||||
|
MenuDaysActive INT NOT NULL DEFAULT 127,
|
||||||
|
MenuStartTime TIME NULL,
|
||||||
|
MenuEndTime TIME NULL,
|
||||||
|
MenuSortOrder INT NOT NULL DEFAULT 0,
|
||||||
|
MenuIsActive TINYINT NOT NULL DEFAULT 1,
|
||||||
|
MenuAddedOn DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_menus_business (MenuBusinessID),
|
||||||
|
INDEX idx_menus_active (MenuBusinessID, MenuIsActive)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["MESSAGE"] = "Menus table created successfully";
|
||||||
|
response["SCHEMA"] = {
|
||||||
|
"MenuDaysActive": "Bitmask: 1=Sun, 2=Mon, 4=Tue, 8=Wed, 16=Thu, 32=Fri, 64=Sat (127 = all days)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CategoryMenuID column exists in Categories table
|
||||||
|
qCatCol = queryExecute("
|
||||||
|
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'payfrit'
|
||||||
|
AND TABLE_NAME = 'Categories'
|
||||||
|
AND COLUMN_NAME = 'CategoryMenuID'
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCatCol.recordCount == 0) {
|
||||||
|
// Add CategoryMenuID column to Categories table
|
||||||
|
queryExecute("
|
||||||
|
ALTER TABLE Categories
|
||||||
|
ADD COLUMN CategoryMenuID INT NULL DEFAULT NULL AFTER CategoryBusinessID,
|
||||||
|
ADD INDEX idx_categories_menu (CategoryMenuID)
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
response["CATEGORIES_UPDATED"] = true;
|
||||||
|
response["CATEGORIES_MESSAGE"] = "Added CategoryMenuID column to Categories table";
|
||||||
|
} else {
|
||||||
|
response["CATEGORIES_UPDATED"] = false;
|
||||||
|
response["CATEGORIES_MESSAGE"] = "CategoryMenuID column already exists";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = "server_error";
|
||||||
|
response["MESSAGE"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail ?: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
119
api/admin/createParentBusiness.cfm
Normal file
119
api/admin/createParentBusiness.cfm
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Create Parent Business (Shell)
|
||||||
|
*
|
||||||
|
* Creates a minimal business record to serve as a parent for other businesses.
|
||||||
|
* No menu items, categories, or hours required.
|
||||||
|
*
|
||||||
|
* POST body:
|
||||||
|
* {
|
||||||
|
* "BusinessName": "Century Casino",
|
||||||
|
* "UserID": 1,
|
||||||
|
* "ChildBusinessIDs": [47, 48] // Optional: link existing businesses as children
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Returns the new BusinessID
|
||||||
|
*/
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw) || len(trim(raw)) == 0) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
return isStruct(data) ? data : {};
|
||||||
|
} catch (any e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
BusinessName = structKeyExists(data, "BusinessName") ? trim(data.BusinessName) : "";
|
||||||
|
UserID = structKeyExists(data, "UserID") ? val(data.UserID) : 0;
|
||||||
|
ChildBusinessIDs = structKeyExists(data, "ChildBusinessIDs") && isArray(data.ChildBusinessIDs) ? data.ChildBusinessIDs : [];
|
||||||
|
|
||||||
|
if (!len(BusinessName)) {
|
||||||
|
response["ERROR"] = "missing_name";
|
||||||
|
response["MESSAGE"] = "BusinessName is required";
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UserID <= 0) {
|
||||||
|
response["ERROR"] = "missing_userid";
|
||||||
|
response["MESSAGE"] = "UserID is required";
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create minimal address record (just a placeholder)
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Addresses (AddressLine1, AddressUserID, AddressTypeID, AddressAddedOn)
|
||||||
|
VALUES ('Parent Business - No Physical Location', :userID, 2, NOW())
|
||||||
|
", {
|
||||||
|
userID: UserID
|
||||||
|
}, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
qAddr = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource = "payfrit" });
|
||||||
|
addressId = qAddr.id;
|
||||||
|
|
||||||
|
// Create parent business (no menu, no hours, just a shell)
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Businesses (BusinessName, BusinessUserID, BusinessAddressID, BusinessParentBusinessID, BusinessDeliveryZipCodes, BusinessAddedOn)
|
||||||
|
VALUES (:name, :userId, :addressId, NULL, '', NOW())
|
||||||
|
", {
|
||||||
|
name: BusinessName,
|
||||||
|
userId: UserID,
|
||||||
|
addressId: addressId
|
||||||
|
}, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
qBiz = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource = "payfrit" });
|
||||||
|
newBusinessID = qBiz.id;
|
||||||
|
|
||||||
|
// Link address back to business
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Addresses SET AddressBusinessID = :bizId WHERE AddressID = :addrId
|
||||||
|
", {
|
||||||
|
bizId: newBusinessID,
|
||||||
|
addrId: addressId
|
||||||
|
}, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
// Update child businesses if provided
|
||||||
|
linkedChildren = [];
|
||||||
|
for (childID in ChildBusinessIDs) {
|
||||||
|
childID = val(childID);
|
||||||
|
if (childID > 0) {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Businesses SET BusinessParentBusinessID = :parentId WHERE BusinessID = :childId
|
||||||
|
", {
|
||||||
|
parentId: newBusinessID,
|
||||||
|
childId: childID
|
||||||
|
}, { datasource = "payfrit" });
|
||||||
|
arrayAppend(linkedChildren, childID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["BusinessID"] = newBusinessID;
|
||||||
|
response["BusinessName"] = BusinessName;
|
||||||
|
response["MESSAGE"] = "Parent business created";
|
||||||
|
if (arrayLen(linkedChildren) > 0) {
|
||||||
|
response["LinkedChildren"] = linkedChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = "server_error";
|
||||||
|
response["MESSAGE"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
55
api/admin/deleteBusiness.cfm
Normal file
55
api/admin/deleteBusiness.cfm
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Delete a business (and its address)
|
||||||
|
* POST: { "BusinessID": 52 }
|
||||||
|
*/
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw) || len(trim(raw)) == 0) return {};
|
||||||
|
try {
|
||||||
|
return deserializeJSON(raw);
|
||||||
|
} catch (any e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
bizID = val(data.BusinessID ?: 0);
|
||||||
|
|
||||||
|
if (bizID <= 0) {
|
||||||
|
response["ERROR"] = "BusinessID required";
|
||||||
|
} else {
|
||||||
|
// Get address ID first
|
||||||
|
qBiz = queryExecute("SELECT BusinessAddressID FROM Businesses WHERE BusinessID = :id", { id: bizID }, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
if (qBiz.recordCount == 0) {
|
||||||
|
response["ERROR"] = "Business not found";
|
||||||
|
} else {
|
||||||
|
addrID = qBiz.BusinessAddressID;
|
||||||
|
|
||||||
|
// Delete business
|
||||||
|
queryExecute("DELETE FROM Businesses WHERE BusinessID = :id", { id: bizID }, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
// Delete address if exists
|
||||||
|
if (val(addrID) > 0) {
|
||||||
|
queryExecute("DELETE FROM Addresses WHERE AddressID = :id", { id: addrID }, { datasource = "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["MESSAGE"] = "Deleted BusinessID " & bizID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
204
api/admin/geocode.cfm
Normal file
204
api/admin/geocode.cfm
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Address Geocoding</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 1000px; margin: 40px auto; padding: 20px; background: ##1a1a1a; color: ##fff; }
|
||||||
|
h1 { color: ##4CAF50; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||||
|
th, td { padding: 10px; text-align: left; border-bottom: 1px solid ##333; }
|
||||||
|
th { background: ##2a2a2a; }
|
||||||
|
tr:hover { background: ##2a2a2a; }
|
||||||
|
.address { color: ##aaa; font-size: 13px; }
|
||||||
|
.success { color: ##4CAF50; padding: 10px; background: ##1b3d1b; border-radius: 4px; margin: 10px 0; }
|
||||||
|
.error { color: ##f44336; padding: 10px; background: ##3d1b1b; border-radius: 4px; margin: 10px 0; }
|
||||||
|
.has-coords { color: ##4CAF50; }
|
||||||
|
.no-coords { color: ##ff9800; }
|
||||||
|
button { padding: 6px 12px; background: ##4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
button:hover { background: ##45a049; }
|
||||||
|
.btn-lookup { background: ##2196F3; }
|
||||||
|
.btn-lookup:hover { background: ##1976D2; }
|
||||||
|
.coords { font-family: monospace; font-size: 12px; color: ##888; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Address Geocoding</h1>
|
||||||
|
<p>Auto-geocode addresses using OpenStreetMap Nominatim (free, no API key required)</p>
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
function geocodeAddress(addressString) {
|
||||||
|
var result = { "success": false, "error": "" };
|
||||||
|
if (len(trim(addressString)) EQ 0) {
|
||||||
|
result.error = "Empty address";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var httpService = new http();
|
||||||
|
httpService.setMethod("GET");
|
||||||
|
httpService.setUrl("https://nominatim.openstreetmap.org/search?q=" & urlEncodedFormat(addressString) & "&format=json&limit=1");
|
||||||
|
httpService.addParam(type="header", name="User-Agent", value="Payfrit/1.0");
|
||||||
|
httpService.setTimeout(10);
|
||||||
|
|
||||||
|
var httpResult = httpService.send().getPrefix();
|
||||||
|
|
||||||
|
if (httpResult.statusCode CONTAINS "200") {
|
||||||
|
var data = deserializeJSON(httpResult.fileContent);
|
||||||
|
if (arrayLen(data) GT 0) {
|
||||||
|
result.success = true;
|
||||||
|
result.lat = data[1].lat;
|
||||||
|
result.lng = data[1].lon;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.error = "No results found";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.error = "HTTP " & httpResult.statusCode;
|
||||||
|
return result;
|
||||||
|
} catch (any e) {
|
||||||
|
result.error = e.message;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAddressString(line1, line2, city, zipCode) {
|
||||||
|
var parts = [];
|
||||||
|
if (len(trim(line1))) arrayAppend(parts, trim(line1));
|
||||||
|
if (len(trim(line2))) arrayAppend(parts, trim(line2));
|
||||||
|
if (len(trim(city))) arrayAppend(parts, trim(city));
|
||||||
|
if (len(trim(zipCode))) arrayAppend(parts, trim(zipCode));
|
||||||
|
return arrayToList(parts, ", ");
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cfif structKeyExists(url, "geocode") AND structKeyExists(url, "addressId")>
|
||||||
|
<cfset addressId = val(url.addressId)>
|
||||||
|
<cfquery name="addr" datasource="payfrit">
|
||||||
|
SELECT AddressLine1, AddressLine2, AddressCity, AddressZIPCode
|
||||||
|
FROM Addresses WHERE AddressID = <cfqueryparam value="#addressId#" cfsqltype="cf_sql_integer">
|
||||||
|
</cfquery>
|
||||||
|
<cfif addr.recordCount GT 0>
|
||||||
|
<cfset fullAddress = buildAddressString(addr.AddressLine1, addr.AddressLine2, addr.AddressCity, addr.AddressZIPCode)>
|
||||||
|
<cfset geo = geocodeAddress(fullAddress)>
|
||||||
|
<cfif geo.success>
|
||||||
|
<cfquery datasource="payfrit">
|
||||||
|
UPDATE Addresses SET AddressLat = <cfqueryparam value="#geo.lat#" cfsqltype="cf_sql_decimal">,
|
||||||
|
AddressLng = <cfqueryparam value="#geo.lng#" cfsqltype="cf_sql_decimal">
|
||||||
|
WHERE AddressID = <cfqueryparam value="#addressId#" cfsqltype="cf_sql_integer">
|
||||||
|
</cfquery>
|
||||||
|
<cfoutput><div class="success">Geocoded Address ID #addressId#: #geo.lat#, #geo.lng#</div></cfoutput>
|
||||||
|
<cfelse>
|
||||||
|
<cfoutput><div class="error">Failed: #geo.error#</div></cfoutput>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfif structKeyExists(url, "geocodeAll")>
|
||||||
|
<cfquery name="missing" datasource="payfrit">
|
||||||
|
SELECT AddressID, AddressLine1, AddressLine2, AddressCity, AddressZIPCode
|
||||||
|
FROM Addresses
|
||||||
|
WHERE (AddressLat IS NULL OR AddressLat = 0)
|
||||||
|
AND AddressLine1 IS NOT NULL AND AddressLine1 != ''
|
||||||
|
</cfquery>
|
||||||
|
<cfset successCount = 0>
|
||||||
|
<cfset failCount = 0>
|
||||||
|
<cfloop query="missing">
|
||||||
|
<cfset fullAddress = buildAddressString(missing.AddressLine1, missing.AddressLine2, missing.AddressCity, missing.AddressZIPCode)>
|
||||||
|
<cfset geo = geocodeAddress(fullAddress)>
|
||||||
|
<cfif geo.success>
|
||||||
|
<cfquery datasource="payfrit">
|
||||||
|
UPDATE Addresses SET AddressLat = <cfqueryparam value="#geo.lat#" cfsqltype="cf_sql_decimal">,
|
||||||
|
AddressLng = <cfqueryparam value="#geo.lng#" cfsqltype="cf_sql_decimal">
|
||||||
|
WHERE AddressID = <cfqueryparam value="#missing.AddressID#" cfsqltype="cf_sql_integer">
|
||||||
|
</cfquery>
|
||||||
|
<cfset successCount = successCount + 1>
|
||||||
|
<cfelse>
|
||||||
|
<cfset failCount = failCount + 1>
|
||||||
|
</cfif>
|
||||||
|
<cfset sleep(1100)>
|
||||||
|
</cfloop>
|
||||||
|
<cfoutput><div class="success">Geocoded #successCount# addresses. #failCount# failed.</div></cfoutput>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfquery name="addresses" datasource="payfrit">
|
||||||
|
SELECT
|
||||||
|
b.BusinessID,
|
||||||
|
b.BusinessName,
|
||||||
|
a.AddressID,
|
||||||
|
a.AddressLine1,
|
||||||
|
a.AddressLine2,
|
||||||
|
a.AddressCity,
|
||||||
|
a.AddressZIPCode,
|
||||||
|
a.AddressLat,
|
||||||
|
a.AddressLng
|
||||||
|
FROM Businesses b
|
||||||
|
LEFT JOIN Addresses a ON b.BusinessAddressID = a.AddressID
|
||||||
|
ORDER BY b.BusinessName
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfset missingCount = 0>
|
||||||
|
<cfloop query="addresses">
|
||||||
|
<cfif (NOT len(addresses.AddressLat) OR val(addresses.AddressLat) EQ 0) AND len(addresses.AddressLine1)>
|
||||||
|
<cfset missingCount = missingCount + 1>
|
||||||
|
</cfif>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
|
<cfoutput>
|
||||||
|
<p>
|
||||||
|
<strong>#missingCount#</strong> addresses missing coordinates.
|
||||||
|
<cfif missingCount GT 0>
|
||||||
|
<a href="?geocodeAll=1"><button class="btn-lookup">Geocode All Missing (#missingCount#)</button></a>
|
||||||
|
<small style="color:##888;">(~#missingCount# seconds due to rate limiting)</small>
|
||||||
|
</cfif>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Business</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Coordinates</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<cfloop query="addresses">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
#addresses.BusinessName#
|
||||||
|
<cfif len(addresses.AddressLat) AND val(addresses.AddressLat) NEQ 0>
|
||||||
|
<span class="has-coords">#chr(10003)#</span>
|
||||||
|
<cfelseif len(addresses.AddressLine1)>
|
||||||
|
<span class="no-coords">#chr(9679)#</span>
|
||||||
|
</cfif>
|
||||||
|
</td>
|
||||||
|
<td class="address">
|
||||||
|
<cfif len(addresses.AddressLine1)>
|
||||||
|
#addresses.AddressLine1#<cfif len(addresses.AddressLine2)>, #addresses.AddressLine2#</cfif><br>
|
||||||
|
#addresses.AddressCity# #addresses.AddressZIPCode#
|
||||||
|
<cfelse>
|
||||||
|
<em style="color:##666;">No address</em>
|
||||||
|
</cfif>
|
||||||
|
</td>
|
||||||
|
<td class="coords">
|
||||||
|
<cfif len(addresses.AddressLat) AND val(addresses.AddressLat) NEQ 0>
|
||||||
|
#numberFormat(addresses.AddressLat, "_.______")#<br>
|
||||||
|
#numberFormat(addresses.AddressLng, "_.______")#
|
||||||
|
<cfelse>
|
||||||
|
-
|
||||||
|
</cfif>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<cfif len(addresses.AddressLine1) AND len(addresses.AddressID)>
|
||||||
|
<a href="?geocode=1&addressId=#addresses.AddressID#"><button class="btn-lookup">Lookup</button></a>
|
||||||
|
</cfif>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</cfloop>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</cfoutput>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
api/admin/linkChildBusiness.cfm
Normal file
46
api/admin/linkChildBusiness.cfm
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Link a child business to a parent
|
||||||
|
* POST: { "ChildBusinessID": 49, "ParentBusinessID": 51 }
|
||||||
|
*/
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw) || len(trim(raw)) == 0) return {};
|
||||||
|
try {
|
||||||
|
return deserializeJSON(raw);
|
||||||
|
} catch (any e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
childID = val(data.ChildBusinessID ?: 0);
|
||||||
|
parentID = val(data.ParentBusinessID ?: 0);
|
||||||
|
|
||||||
|
if (childID <= 0) {
|
||||||
|
response["ERROR"] = "ChildBusinessID required";
|
||||||
|
} else {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Businesses SET BusinessParentBusinessID = :parentId WHERE BusinessID = :childId
|
||||||
|
", {
|
||||||
|
parentId: { value = parentID > 0 ? parentID : javaCast("null", ""), cfsqltype = "cf_sql_integer", null = parentID == 0 },
|
||||||
|
childId: childID
|
||||||
|
}, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["MESSAGE"] = "Updated BusinessID " & childID & " parent to " & (parentID > 0 ? parentID : "NULL");
|
||||||
|
}
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
|
|
@ -46,7 +46,6 @@ beaconId = int(data.BeaconID);
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Get all businesses that have assignments to this beacon --->
|
<!--- Get all businesses that have assignments to this beacon --->
|
||||||
<!--- This includes the beacon owner AND any child businesses that have claimed this beacon --->
|
|
||||||
<cfquery name="qAssignments" datasource="payfrit">
|
<cfquery name="qAssignments" datasource="payfrit">
|
||||||
SELECT
|
SELECT
|
||||||
lt.BusinessID,
|
lt.BusinessID,
|
||||||
|
|
@ -62,17 +61,63 @@ beaconId = int(data.BeaconID);
|
||||||
ORDER BY biz.BusinessParentBusinessID IS NULL DESC, biz.BusinessName ASC
|
ORDER BY biz.BusinessParentBusinessID IS NULL DESC, biz.BusinessName ASC
|
||||||
</cfquery>
|
</cfquery>
|
||||||
|
|
||||||
|
<!--- Check if any assigned business is a parent (has children) --->
|
||||||
|
<cfset parentBusinessID = 0>
|
||||||
|
<cfloop query="qAssignments">
|
||||||
|
<!--- Check if this business has children --->
|
||||||
|
<cfquery name="qChildren" datasource="payfrit">
|
||||||
|
SELECT COUNT(*) as cnt FROM Businesses
|
||||||
|
WHERE BusinessParentBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#qAssignments.BusinessID#">
|
||||||
|
</cfquery>
|
||||||
|
<cfif qChildren.cnt GT 0>
|
||||||
|
<cfset parentBusinessID = qAssignments.BusinessID>
|
||||||
|
<cfbreak>
|
||||||
|
</cfif>
|
||||||
|
</cfloop>
|
||||||
|
|
||||||
<!--- Build response with array of businesses --->
|
<!--- Build response with array of businesses --->
|
||||||
<cfset businesses = []>
|
<cfset businesses = []>
|
||||||
<cfloop query="qAssignments">
|
|
||||||
<cfset arrayAppend(businesses, {
|
<!--- If beacon is assigned to a parent, return the child businesses instead --->
|
||||||
"BusinessID" = qAssignments.BusinessID,
|
<cfif parentBusinessID GT 0>
|
||||||
"BusinessName" = qAssignments.BusinessName,
|
<!--- Get parent business info for header image --->
|
||||||
"ServicePointID" = qAssignments.ServicePointID,
|
<cfquery name="qParent" datasource="payfrit">
|
||||||
"ServicePointName" = qAssignments.ServicePointName,
|
SELECT BusinessName, BusinessHeaderImageExtension
|
||||||
"IsParent" = isNull(qAssignments.BusinessParentBusinessID) OR qAssignments.BusinessParentBusinessID EQ 0
|
FROM Businesses
|
||||||
})>
|
WHERE BusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#parentBusinessID#">
|
||||||
</cfloop>
|
</cfquery>
|
||||||
|
<cfquery name="qChildBusinesses" datasource="payfrit">
|
||||||
|
SELECT
|
||||||
|
BusinessID,
|
||||||
|
BusinessName,
|
||||||
|
BusinessParentBusinessID,
|
||||||
|
BusinessHeaderImageExtension
|
||||||
|
FROM Businesses
|
||||||
|
WHERE BusinessParentBusinessID = <cfqueryparam cfsqltype="cf_sql_integer" value="#parentBusinessID#">
|
||||||
|
ORDER BY BusinessName ASC
|
||||||
|
</cfquery>
|
||||||
|
<cfloop query="qChildBusinesses">
|
||||||
|
<cfset arrayAppend(businesses, {
|
||||||
|
"BusinessID" = qChildBusinesses.BusinessID,
|
||||||
|
"BusinessName" = qChildBusinesses.BusinessName,
|
||||||
|
"ServicePointID" = qAssignments.ServicePointID,
|
||||||
|
"ServicePointName" = qAssignments.ServicePointName,
|
||||||
|
"IsParent" = false,
|
||||||
|
"ParentBusinessID" = parentBusinessID
|
||||||
|
})>
|
||||||
|
</cfloop>
|
||||||
|
<cfelse>
|
||||||
|
<!--- Normal case: return directly assigned businesses --->
|
||||||
|
<cfloop query="qAssignments">
|
||||||
|
<cfset arrayAppend(businesses, {
|
||||||
|
"BusinessID" = qAssignments.BusinessID,
|
||||||
|
"BusinessName" = qAssignments.BusinessName,
|
||||||
|
"ServicePointID" = qAssignments.ServicePointID,
|
||||||
|
"ServicePointName" = qAssignments.ServicePointName,
|
||||||
|
"IsParent" = isNull(qAssignments.BusinessParentBusinessID) OR qAssignments.BusinessParentBusinessID EQ 0
|
||||||
|
})>
|
||||||
|
</cfloop>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<cfset response = {
|
<cfset response = {
|
||||||
"OK" = true,
|
"OK" = true,
|
||||||
|
|
@ -91,4 +136,13 @@ beaconId = int(data.BeaconID);
|
||||||
} : {}
|
} : {}
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
<!--- Add parent info if this is a parent-child scenario --->
|
||||||
|
<cfif parentBusinessID GT 0>
|
||||||
|
<cfset response["PARENT"] = {
|
||||||
|
"BusinessID" = parentBusinessID,
|
||||||
|
"BusinessName" = qParent.BusinessName,
|
||||||
|
"BusinessHeaderImageExtension" = len(trim(qParent.BusinessHeaderImageExtension)) ? qParent.BusinessHeaderImageExtension : ""
|
||||||
|
}>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<cfoutput>#serializeJSON(response)#</cfoutput>
|
<cfoutput>#serializeJSON(response)#</cfoutput>
|
||||||
|
|
|
||||||
113
api/beacons/lookup.cfm
Normal file
113
api/beacons/lookup.cfm
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Lookup beacons by UUID
|
||||||
|
*
|
||||||
|
* POST: {
|
||||||
|
* UUIDs: array of UUID strings (without dashes, uppercase)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Returns: {
|
||||||
|
* OK: true,
|
||||||
|
* BEACONS: [
|
||||||
|
* {
|
||||||
|
* UUID: "...",
|
||||||
|
* BeaconID: int,
|
||||||
|
* BeaconName: string,
|
||||||
|
* BusinessID: int,
|
||||||
|
* BusinessName: string,
|
||||||
|
* ServicePointID: int,
|
||||||
|
* ServicePointName: string,
|
||||||
|
* ParentBusinessID: int (if applicable),
|
||||||
|
* ParentBusinessName: string (if applicable),
|
||||||
|
* HasChildren: boolean
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestData = deserializeJSON(toString(getHttpRequestData().content));
|
||||||
|
|
||||||
|
uuids = requestData.UUIDs ?: [];
|
||||||
|
|
||||||
|
if (!isArray(uuids) || arrayLen(uuids) == 0) {
|
||||||
|
response["OK"] = true;
|
||||||
|
response["BEACONS"] = [];
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean and normalize UUIDs (remove dashes, uppercase)
|
||||||
|
cleanUUIDs = [];
|
||||||
|
for (uuid in uuids) {
|
||||||
|
cleanUUID = uCase(reReplace(uuid, "-", "", "all"));
|
||||||
|
if (len(cleanUUID) == 32) {
|
||||||
|
arrayAppend(cleanUUIDs, cleanUUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrayLen(cleanUUIDs) == 0) {
|
||||||
|
response["OK"] = true;
|
||||||
|
response["BEACONS"] = [];
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for matching beacons with business info
|
||||||
|
// Beacons link to ServicePoints via lt_Beacon_Businesses_ServicePoints
|
||||||
|
qBeacons = queryExecute("
|
||||||
|
SELECT
|
||||||
|
b.BeaconID,
|
||||||
|
b.BeaconName,
|
||||||
|
b.BeaconUUID,
|
||||||
|
COALESCE(link.ServicePointID, 0) AS ServicePointID,
|
||||||
|
COALESCE(sp.ServicePointName, '') AS ServicePointName,
|
||||||
|
COALESCE(link.BusinessID, b.BeaconBusinessID) AS BusinessID,
|
||||||
|
biz.BusinessName,
|
||||||
|
biz.BusinessParentBusinessID,
|
||||||
|
parent.BusinessName AS ParentBusinessName,
|
||||||
|
(SELECT COUNT(*) FROM Businesses WHERE BusinessParentBusinessID = biz.BusinessID) AS ChildCount
|
||||||
|
FROM Beacons b
|
||||||
|
LEFT JOIN lt_Beacon_Businesses_ServicePoints link ON b.BeaconID = link.BeaconID
|
||||||
|
LEFT JOIN ServicePoints sp ON link.ServicePointID = sp.ServicePointID
|
||||||
|
INNER JOIN Businesses biz ON COALESCE(link.BusinessID, b.BeaconBusinessID) = biz.BusinessID
|
||||||
|
LEFT JOIN Businesses parent ON biz.BusinessParentBusinessID = parent.BusinessID
|
||||||
|
WHERE b.BeaconUUID IN (:uuids)
|
||||||
|
AND b.BeaconIsActive = 1
|
||||||
|
AND biz.BusinessIsDemo = 0
|
||||||
|
AND biz.BusinessIsPrivate = 0
|
||||||
|
", {
|
||||||
|
uuids: { value: arrayToList(cleanUUIDs), cfsqltype: "cf_sql_varchar", list: true }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
beacons = [];
|
||||||
|
for (row in qBeacons) {
|
||||||
|
arrayAppend(beacons, {
|
||||||
|
"UUID": row.BeaconUUID,
|
||||||
|
"BeaconID": row.BeaconID,
|
||||||
|
"BeaconName": row.BeaconName,
|
||||||
|
"BusinessID": row.BusinessID,
|
||||||
|
"BusinessName": row.BusinessName,
|
||||||
|
"ServicePointID": row.ServicePointID,
|
||||||
|
"ServicePointName": row.ServicePointName,
|
||||||
|
"ParentBusinessID": val(row.BusinessParentBusinessID),
|
||||||
|
"ParentBusinessName": row.ParentBusinessName ?: "",
|
||||||
|
"HasChildren": row.ChildCount > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["BEACONS"] = beacons;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
76
api/businesses/getChildren.cfm
Normal file
76
api/businesses/getChildren.cfm
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<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(payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support GET param or POST body
|
||||||
|
parentBusinessId = 0;
|
||||||
|
if (structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID)) {
|
||||||
|
parentBusinessId = int(url.BusinessID);
|
||||||
|
} else {
|
||||||
|
data = readJsonBody();
|
||||||
|
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) {
|
||||||
|
parentBusinessId = int(data.BusinessID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentBusinessId LTE 0) {
|
||||||
|
apiAbort({ OK=false, ERROR="missing_business_id", MESSAGE="BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
q = queryExecute(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
BusinessID,
|
||||||
|
BusinessName
|
||||||
|
FROM Businesses
|
||||||
|
WHERE BusinessParentBusinessID = :parentId
|
||||||
|
ORDER BY BusinessName
|
||||||
|
",
|
||||||
|
{ parentId = { value = parentBusinessId, cfsqltype = "cf_sql_integer" } },
|
||||||
|
{ datasource = "payfrit" }
|
||||||
|
);
|
||||||
|
|
||||||
|
rows = [];
|
||||||
|
for (i = 1; i <= q.recordCount; i++) {
|
||||||
|
arrayAppend(rows, {
|
||||||
|
"BusinessID": q.BusinessID[i],
|
||||||
|
"BusinessName": q.BusinessName[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"ERROR": "",
|
||||||
|
"BUSINESSES": rows
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"DETAIL": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -31,7 +31,10 @@ function haversineDistance(lat1, lng1, lat2, lng2) {
|
||||||
var a = sin(dLat/2) * sin(dLat/2) +
|
var a = sin(dLat/2) * sin(dLat/2) +
|
||||||
cos(lat1 * 3.14159265359 / 180) * cos(lat2 * 3.14159265359 / 180) *
|
cos(lat1 * 3.14159265359 / 180) * cos(lat2 * 3.14159265359 / 180) *
|
||||||
sin(dLng/2) * sin(dLng/2);
|
sin(dLng/2) * sin(dLng/2);
|
||||||
var c = 2 * atn(sqr(a) / sqr(1-a));
|
// Clamp a to avoid NaN from sqrt of negative or division by zero
|
||||||
|
if (a < 0) a = 0;
|
||||||
|
if (a > 1) a = 1;
|
||||||
|
var c = 2 * asin(sqr(a));
|
||||||
return R * c;
|
return R * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +44,7 @@ try {
|
||||||
userLng = structKeyExists(data, "lng") ? val(data.lng) : 0;
|
userLng = structKeyExists(data, "lng") ? val(data.lng) : 0;
|
||||||
hasUserLocation = (userLat != 0 AND userLng != 0);
|
hasUserLocation = (userLat != 0 AND userLng != 0);
|
||||||
|
|
||||||
// Get businesses with their address coordinates (exclude demo and hidden)
|
// Get businesses with their address coordinates (exclude demo and private)
|
||||||
q = queryExecute(
|
q = queryExecute(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -54,7 +57,7 @@ try {
|
||||||
FROM Businesses b
|
FROM Businesses b
|
||||||
LEFT JOIN Addresses a ON b.BusinessAddressID = a.AddressID
|
LEFT JOIN Addresses a ON b.BusinessAddressID = a.AddressID
|
||||||
WHERE (b.BusinessIsDemo = 0 OR b.BusinessIsDemo IS NULL)
|
WHERE (b.BusinessIsDemo = 0 OR b.BusinessIsDemo IS NULL)
|
||||||
AND (b.BusinessIsHidden = 0 OR b.BusinessIsHidden IS NULL)
|
AND (b.BusinessIsPrivate = 0 OR b.BusinessIsPrivate IS NULL)
|
||||||
ORDER BY b.BusinessName
|
ORDER BY b.BusinessName
|
||||||
",
|
",
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
<cfscript>
|
<cfscript>
|
||||||
// Check for an active (uncompleted) chat task at a business
|
// Check for an active (uncompleted) chat task at a service point
|
||||||
// Input: BusinessID, ServicePointID (optional), UserID (optional)
|
// Input: BusinessID, ServicePointID
|
||||||
// Output: { OK: true, HAS_ACTIVE_CHAT: true/false, TASK_ID: ... }
|
// Output: { OK: true, HAS_ACTIVE_CHAT: true/false, TASK_ID: ... }
|
||||||
|
|
||||||
function apiAbort(required struct payload) {
|
function apiAbort(required struct payload) {
|
||||||
|
|
@ -27,31 +27,39 @@ try {
|
||||||
data = readJsonBody();
|
data = readJsonBody();
|
||||||
businessID = val(structKeyExists(data, "BusinessID") ? data.BusinessID : 0);
|
businessID = val(structKeyExists(data, "BusinessID") ? data.BusinessID : 0);
|
||||||
servicePointID = val(structKeyExists(data, "ServicePointID") ? data.ServicePointID : 0);
|
servicePointID = val(structKeyExists(data, "ServicePointID") ? data.ServicePointID : 0);
|
||||||
userID = val(structKeyExists(data, "UserID") ? data.UserID : 0);
|
|
||||||
|
|
||||||
if (businessID == 0) {
|
if (businessID == 0) {
|
||||||
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for any active chat task at this business (TaskTypeID = 2, not completed)
|
if (servicePointID == 0) {
|
||||||
// Priority order:
|
// No service point - can't find specific chat
|
||||||
// 1. Chats that are claimed (worker is responding)
|
apiAbort({
|
||||||
// 2. Chats that have messages (ongoing conversation)
|
"OK": true,
|
||||||
// 3. Most recently created chat
|
"HAS_ACTIVE_CHAT": false,
|
||||||
|
"TASK_ID": 0,
|
||||||
|
"TASK_TITLE": ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for any active chat task at this service point
|
||||||
|
// TaskTypeID = 2 (Chat), not completed, at this service point
|
||||||
qChat = queryExecute("
|
qChat = queryExecute("
|
||||||
SELECT t.TaskID, t.TaskTitle, t.TaskSourceID,
|
SELECT t.TaskID, t.TaskTitle,
|
||||||
(SELECT COUNT(*) FROM ChatMessages m WHERE m.TaskID = t.TaskID) as MessageCount
|
(SELECT MAX(cm.CreatedOn) FROM ChatMessages cm WHERE cm.TaskID = t.TaskID) as LastMessageTime
|
||||||
FROM Tasks t
|
FROM Tasks t
|
||||||
WHERE t.TaskBusinessID = :businessID
|
WHERE t.TaskBusinessID = :businessID
|
||||||
AND t.TaskTypeID = 2
|
AND t.TaskTypeID = 2
|
||||||
AND t.TaskCompletedOn IS NULL
|
AND t.TaskCompletedOn IS NULL
|
||||||
|
AND t.TaskSourceType = 'servicepoint'
|
||||||
|
AND t.TaskSourceID = :servicePointID
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN t.TaskClaimedByUserID > 0 THEN 0 ELSE 1 END,
|
CASE WHEN t.TaskClaimedByUserID > 0 THEN 0 ELSE 1 END,
|
||||||
(SELECT COUNT(*) FROM ChatMessages m WHERE m.TaskID = t.TaskID) DESC,
|
|
||||||
t.TaskAddedOn DESC
|
t.TaskAddedOn DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
", {
|
", {
|
||||||
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
|
||||||
|
servicePointID: { value: servicePointID, cfsqltype: "cf_sql_integer" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
if (qChat.recordCount > 0) {
|
if (qChat.recordCount > 0) {
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,20 @@ try {
|
||||||
senderType = "customer";
|
senderType = "customer";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify task exists
|
// Verify task exists and is still open
|
||||||
taskQuery = queryExecute("
|
taskQuery = queryExecute("
|
||||||
SELECT TaskID, TaskClaimedByUserID FROM Tasks WHERE TaskID = :taskID
|
SELECT TaskID, TaskClaimedByUserID, TaskCompletedOn FROM Tasks WHERE TaskID = :taskID
|
||||||
", { taskID: { value: taskID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
", { taskID: { value: taskID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
|
||||||
if (taskQuery.recordCount == 0) {
|
if (taskQuery.recordCount == 0) {
|
||||||
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found" });
|
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if chat has been closed
|
||||||
|
if (len(trim(taskQuery.TaskCompletedOn)) > 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "chat_closed", "MESSAGE": "This chat has ended" });
|
||||||
|
}
|
||||||
|
|
||||||
// Insert message
|
// Insert message
|
||||||
queryExecute("
|
queryExecute("
|
||||||
INSERT INTO ChatMessages (TaskID, SenderUserID, SenderType, MessageText)
|
INSERT INTO ChatMessages (TaskID, SenderUserID, SenderType, MessageText)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,35 @@ try {
|
||||||
abort;
|
abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for MenuID filter (optional - if provided, only return categories for that menu)
|
||||||
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
||||||
|
|
||||||
|
// Get all menus for this business
|
||||||
|
allMenus = [];
|
||||||
|
try {
|
||||||
|
qMenus = queryExecute("
|
||||||
|
SELECT MenuID, MenuName, MenuDescription, MenuDaysActive,
|
||||||
|
MenuStartTime, MenuEndTime, MenuSortOrder
|
||||||
|
FROM Menus
|
||||||
|
WHERE MenuBusinessID = :businessID AND MenuIsActive = 1
|
||||||
|
ORDER BY MenuSortOrder, MenuName
|
||||||
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
for (m = 1; m <= qMenus.recordCount; m++) {
|
||||||
|
arrayAppend(allMenus, {
|
||||||
|
"MenuID": qMenus.MenuID[m],
|
||||||
|
"MenuName": qMenus.MenuName[m],
|
||||||
|
"MenuDescription": isNull(qMenus.MenuDescription[m]) ? "" : qMenus.MenuDescription[m],
|
||||||
|
"MenuDaysActive": qMenus.MenuDaysActive[m],
|
||||||
|
"MenuStartTime": isNull(qMenus.MenuStartTime[m]) ? "" : timeFormat(qMenus.MenuStartTime[m], "HH:mm"),
|
||||||
|
"MenuEndTime": isNull(qMenus.MenuEndTime[m]) ? "" : timeFormat(qMenus.MenuEndTime[m], "HH:mm"),
|
||||||
|
"MenuSortOrder": qMenus.MenuSortOrder[m]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (any e) {
|
||||||
|
// Menus table might not exist yet
|
||||||
|
}
|
||||||
|
|
||||||
// Check if Categories table has data for this business
|
// Check if Categories table has data for this business
|
||||||
hasCategoriesData = false;
|
hasCategoriesData = false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -70,15 +99,24 @@ try {
|
||||||
|
|
||||||
if (hasCategoriesData) {
|
if (hasCategoriesData) {
|
||||||
// OLD SCHEMA: Use Categories table for categories
|
// OLD SCHEMA: Use Categories table for categories
|
||||||
|
// Build menu filter clause
|
||||||
|
menuFilter = "";
|
||||||
|
menuParams = { businessID: businessID };
|
||||||
|
if (menuID > 0) {
|
||||||
|
menuFilter = " AND CategoryMenuID = :menuID";
|
||||||
|
menuParams["menuID"] = menuID;
|
||||||
|
}
|
||||||
|
|
||||||
qCategories = queryExecute("
|
qCategories = queryExecute("
|
||||||
SELECT
|
SELECT
|
||||||
CategoryID,
|
CategoryID,
|
||||||
CategoryName,
|
CategoryName,
|
||||||
CategorySortOrder as ItemSortOrder
|
CategorySortOrder as ItemSortOrder,
|
||||||
|
CategoryMenuID
|
||||||
FROM Categories
|
FROM Categories
|
||||||
WHERE CategoryBusinessID = :businessID
|
WHERE CategoryBusinessID = :businessID #menuFilter#
|
||||||
ORDER BY CategorySortOrder, CategoryName
|
ORDER BY CategorySortOrder, CategoryName
|
||||||
", { businessID: businessID }, { datasource: "payfrit" });
|
", menuParams, { datasource: "payfrit" });
|
||||||
|
|
||||||
// Get menu items - items that belong to categories (not modifiers)
|
// Get menu items - items that belong to categories (not modifiers)
|
||||||
qItems = queryExecute("
|
qItems = queryExecute("
|
||||||
|
|
@ -328,14 +366,25 @@ try {
|
||||||
catID = qCategories.CategoryID[i];
|
catID = qCategories.CategoryID[i];
|
||||||
catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : [];
|
catItems = structKeyExists(itemsByCategory, catID) ? itemsByCategory[catID] : [];
|
||||||
|
|
||||||
arrayAppend(categories, {
|
catStruct = {
|
||||||
"id": "cat_" & qCategories.CategoryID[i],
|
"id": "cat_" & qCategories.CategoryID[i],
|
||||||
"dbId": qCategories.CategoryID[i],
|
"dbId": qCategories.CategoryID[i],
|
||||||
"name": qCategories.CategoryName[i],
|
"name": qCategories.CategoryName[i],
|
||||||
"description": "",
|
"description": "",
|
||||||
"sortOrder": catIndex,
|
"sortOrder": catIndex,
|
||||||
"items": catItems
|
"items": catItems
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Include MenuID if available (legacy schema with Categories table)
|
||||||
|
if (hasCategoriesData) {
|
||||||
|
try {
|
||||||
|
catStruct["menuId"] = isNull(qCategories.CategoryMenuID[i]) ? 0 : val(qCategories.CategoryMenuID[i]);
|
||||||
|
} catch (any e) {
|
||||||
|
catStruct["menuId"] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayAppend(categories, catStruct);
|
||||||
catIndex++;
|
catIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -360,10 +409,13 @@ try {
|
||||||
|
|
||||||
response["OK"] = true;
|
response["OK"] = true;
|
||||||
response["MENU"] = { "categories": categories };
|
response["MENU"] = { "categories": categories };
|
||||||
|
response["MENUS"] = allMenus;
|
||||||
|
response["SELECTED_MENU_ID"] = menuID;
|
||||||
response["TEMPLATES"] = templateLibrary;
|
response["TEMPLATES"] = templateLibrary;
|
||||||
response["BRANDCOLOR"] = brandColor;
|
response["BRANDCOLOR"] = brandColor;
|
||||||
response["CATEGORY_COUNT"] = arrayLen(categories);
|
response["CATEGORY_COUNT"] = arrayLen(categories);
|
||||||
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
||||||
|
response["MENU_COUNT"] = arrayLen(allMenus);
|
||||||
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
|
response["SCHEMA"] = hasCategoriesData ? "legacy" : "unified";
|
||||||
|
|
||||||
totalItems = 0;
|
totalItems = 0;
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,20 @@
|
||||||
<cfset BusinessID = val(data.BusinessID)>
|
<cfset BusinessID = val(data.BusinessID)>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Optional OrderTypeID for channel filtering (1=Dine-In, 2=Takeaway, 3=Delivery) --->
|
||||||
|
<cfset OrderTypeID = 0>
|
||||||
|
<cfif structKeyExists(data, "OrderTypeID")>
|
||||||
|
<cfset OrderTypeID = val(data.OrderTypeID)>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
<cfif BusinessID LTE 0>
|
<cfif BusinessID LTE 0>
|
||||||
<cfset apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required.", "DETAIL": "" })>
|
<cfset apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required.", "DETAIL": "" })>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
|
<!--- Get current time and day for schedule filtering --->
|
||||||
|
<cfset currentTime = timeFormat(now(), "HH:mm:ss")>
|
||||||
|
<cfset currentDayID = dayOfWeek(now())>
|
||||||
|
|
||||||
<cftry>
|
<cftry>
|
||||||
<!--- Check if new schema is active (ItemBusinessID column exists and has data) --->
|
<!--- Check if new schema is active (ItemBusinessID column exists and has data) --->
|
||||||
<cfset newSchemaActive = false>
|
<cfset newSchemaActive = false>
|
||||||
|
|
@ -70,24 +80,89 @@
|
||||||
|
|
||||||
<cfif hasCategoriesData>
|
<cfif hasCategoriesData>
|
||||||
<!--- Use Categories table with ItemCategoryID --->
|
<!--- Use Categories table with ItemCategoryID --->
|
||||||
<!--- First, get category headers as virtual items --->
|
<!--- First, find which menus are currently active based on day/time --->
|
||||||
|
<cfset activeMenuIds = "">
|
||||||
|
<cftry>
|
||||||
|
<cfset qActiveMenus = queryExecute(
|
||||||
|
"
|
||||||
|
SELECT MenuID FROM Menus
|
||||||
|
WHERE MenuBusinessID = :bizId
|
||||||
|
AND MenuIsActive = 1
|
||||||
|
AND (MenuDaysActive & :dayBit) > 0
|
||||||
|
AND (
|
||||||
|
(MenuStartTime IS NULL OR MenuEndTime IS NULL)
|
||||||
|
OR (TIME(:currentTime) >= MenuStartTime AND TIME(:currentTime) <= MenuEndTime)
|
||||||
|
)
|
||||||
|
",
|
||||||
|
{
|
||||||
|
bizId: { value = BusinessID, cfsqltype = "cf_sql_integer" },
|
||||||
|
dayBit: { value = 2 ^ (currentDayID - 1), cfsqltype = "cf_sql_integer" },
|
||||||
|
currentTime: { value = currentTime, cfsqltype = "cf_sql_varchar" }
|
||||||
|
},
|
||||||
|
{ datasource = "payfrit" }
|
||||||
|
)>
|
||||||
|
<cfset activeMenuIds = valueList(qActiveMenus.MenuID)>
|
||||||
|
<cfcatch>
|
||||||
|
<!--- Menus table might not exist yet --->
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
||||||
|
<!--- Get category headers as virtual items --->
|
||||||
|
<!--- Apply schedule filtering, order type filtering, and menu filtering --->
|
||||||
<cfset qCategories = queryExecute(
|
<cfset qCategories = queryExecute(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
CategoryID,
|
CategoryID,
|
||||||
CategoryName,
|
CategoryName,
|
||||||
CategorySortOrder
|
CategorySortOrder,
|
||||||
|
CategoryOrderTypes,
|
||||||
|
CategoryScheduleStart,
|
||||||
|
CategoryScheduleEnd,
|
||||||
|
CategoryScheduleDays,
|
||||||
|
CategoryMenuID
|
||||||
FROM Categories
|
FROM Categories
|
||||||
WHERE CategoryBusinessID = ?
|
WHERE CategoryBusinessID = :bizId
|
||||||
|
AND (
|
||||||
|
:orderTypeId = 0
|
||||||
|
OR FIND_IN_SET(:orderTypeId, CategoryOrderTypes) > 0
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
CategoryScheduleStart IS NULL
|
||||||
|
OR CategoryScheduleEnd IS NULL
|
||||||
|
OR (
|
||||||
|
TIME(:currentTime) >= CategoryScheduleStart
|
||||||
|
AND TIME(:currentTime) <= CategoryScheduleEnd
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
CategoryScheduleDays IS NULL
|
||||||
|
OR CategoryScheduleDays = ''
|
||||||
|
OR FIND_IN_SET(:currentDay, CategoryScheduleDays) > 0
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
CategoryMenuID IS NULL
|
||||||
|
OR CategoryMenuID = 0
|
||||||
|
#len(activeMenuIds) ? "OR CategoryMenuID IN (#activeMenuIds#)" : ""#
|
||||||
|
)
|
||||||
ORDER BY CategorySortOrder
|
ORDER BY CategorySortOrder
|
||||||
",
|
",
|
||||||
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
|
{
|
||||||
|
bizId: { value = BusinessID, cfsqltype = "cf_sql_integer" },
|
||||||
|
orderTypeId: { value = OrderTypeID, cfsqltype = "cf_sql_integer" },
|
||||||
|
currentTime: { value = currentTime, cfsqltype = "cf_sql_varchar" },
|
||||||
|
currentDay: { value = currentDayID, cfsqltype = "cf_sql_integer" }
|
||||||
|
},
|
||||||
{ datasource = "payfrit" }
|
{ datasource = "payfrit" }
|
||||||
)>
|
)>
|
||||||
|
|
||||||
<!--- Get menu items --->
|
<!--- Get menu items --->
|
||||||
<!--- Exclude old-style category headers (ParentID=0 AND CategoryID=0 AND no children with CategoryID>0) --->
|
<!--- Exclude old-style category headers (ParentID=0 AND CategoryID=0 AND no children with CategoryID>0) --->
|
||||||
<!--- These are legacy category headers that should be replaced by Categories table entries --->
|
<!--- These are legacy category headers that should be replaced by Categories table entries --->
|
||||||
|
<!--- Only include items from visible categories (after schedule/channel filtering) --->
|
||||||
|
<cfset visibleCategoryIds = valueList(qCategories.CategoryID)>
|
||||||
|
<cfif len(trim(visibleCategoryIds)) EQ 0>
|
||||||
|
<cfset visibleCategoryIds = "0">
|
||||||
|
</cfif>
|
||||||
<cfset q = queryExecute(
|
<cfset q = queryExecute(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -110,14 +185,15 @@
|
||||||
FROM Items i
|
FROM Items i
|
||||||
LEFT JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
LEFT JOIN Categories c ON c.CategoryID = i.ItemCategoryID
|
||||||
LEFT JOIN Stations s ON s.StationID = i.ItemStationID
|
LEFT JOIN Stations s ON s.StationID = i.ItemStationID
|
||||||
WHERE i.ItemBusinessID = ?
|
WHERE i.ItemBusinessID = :bizId
|
||||||
AND i.ItemIsActive = 1
|
AND i.ItemIsActive = 1
|
||||||
|
AND i.ItemCategoryID IN (#visibleCategoryIds#)
|
||||||
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID)
|
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID)
|
||||||
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl2 WHERE tl2.TemplateItemID = i.ItemParentItemID)
|
AND NOT EXISTS (SELECT 1 FROM ItemTemplateLinks tl2 WHERE tl2.TemplateItemID = i.ItemParentItemID)
|
||||||
AND NOT (i.ItemParentItemID = 0 AND i.ItemCategoryID = 0 AND i.ItemPrice = 0)
|
AND NOT (i.ItemParentItemID = 0 AND i.ItemCategoryID = 0 AND i.ItemPrice = 0)
|
||||||
ORDER BY COALESCE(c.CategorySortOrder, 999), i.ItemSortOrder, i.ItemID
|
ORDER BY COALESCE(c.CategorySortOrder, 999), i.ItemSortOrder, i.ItemID
|
||||||
",
|
",
|
||||||
[ { value = BusinessID, cfsqltype = "cf_sql_integer" } ],
|
{ bizId: { value = BusinessID, cfsqltype = "cf_sql_integer" } },
|
||||||
{ datasource = "payfrit" }
|
{ datasource = "payfrit" }
|
||||||
)>
|
)>
|
||||||
<cfelse>
|
<cfelse>
|
||||||
|
|
|
||||||
89
api/menu/listCategories.cfm
Normal file
89
api/menu/listCategories.cfm
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* List Categories for a Business
|
||||||
|
*
|
||||||
|
* POST body:
|
||||||
|
* {
|
||||||
|
* "BusinessID": 37
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Returns all categories with their schedule settings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw) || len(trim(raw)) == 0) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
return isStruct(data) ? data : {};
|
||||||
|
} catch (any e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
BusinessID = structKeyExists(data, "BusinessID") ? val(data.BusinessID) : 0;
|
||||||
|
|
||||||
|
if (BusinessID <= 0) {
|
||||||
|
response["ERROR"] = "missing_businessid";
|
||||||
|
response["MESSAGE"] = "BusinessID is required";
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
q = queryExecute("
|
||||||
|
SELECT
|
||||||
|
CategoryID,
|
||||||
|
CategoryBusinessID,
|
||||||
|
CategoryParentCategoryID,
|
||||||
|
CategoryName,
|
||||||
|
CategoryImageExtension,
|
||||||
|
CategoryOrderTypes,
|
||||||
|
CategoryScheduleStart,
|
||||||
|
CategoryScheduleEnd,
|
||||||
|
CategoryScheduleDays,
|
||||||
|
CategorySortOrder,
|
||||||
|
CategoryAddedOn
|
||||||
|
FROM Categories
|
||||||
|
WHERE CategoryBusinessID = :bizId
|
||||||
|
ORDER BY CategorySortOrder, CategoryName
|
||||||
|
", { bizId: BusinessID }, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
categories = [];
|
||||||
|
for (row in q) {
|
||||||
|
arrayAppend(categories, {
|
||||||
|
"CategoryID": row.CategoryID,
|
||||||
|
"CategoryBusinessID": row.CategoryBusinessID,
|
||||||
|
"CategoryParentCategoryID": row.CategoryParentCategoryID,
|
||||||
|
"CategoryName": row.CategoryName,
|
||||||
|
"CategoryImageExtension": len(trim(row.CategoryImageExtension)) ? row.CategoryImageExtension : "",
|
||||||
|
"CategoryOrderTypes": row.CategoryOrderTypes,
|
||||||
|
"CategoryScheduleStart": isNull(row.CategoryScheduleStart) ? "" : timeFormat(row.CategoryScheduleStart, "HH:mm:ss"),
|
||||||
|
"CategoryScheduleEnd": isNull(row.CategoryScheduleEnd) ? "" : timeFormat(row.CategoryScheduleEnd, "HH:mm:ss"),
|
||||||
|
"CategoryScheduleDays": isNull(row.CategoryScheduleDays) ? "" : row.CategoryScheduleDays,
|
||||||
|
"CategorySortOrder": row.CategorySortOrder,
|
||||||
|
"CategoryAddedOn": dateTimeFormat(row.CategoryAddedOn, "yyyy-mm-dd HH:nn:ss")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["Categories"] = categories;
|
||||||
|
response["COUNT"] = arrayLen(categories);
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = "server_error";
|
||||||
|
response["MESSAGE"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
248
api/menu/menus.cfm
Normal file
248
api/menu/menus.cfm
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Menu CRUD API
|
||||||
|
*
|
||||||
|
* GET: List all menus for a business
|
||||||
|
* POST: Create or update a menu
|
||||||
|
* DELETE: Soft-delete a menu
|
||||||
|
*
|
||||||
|
* Input: BusinessID, and optionally Menu data for POST
|
||||||
|
*/
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
function apiAbort(payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestBody = toString(getHttpRequestData().content);
|
||||||
|
requestData = {};
|
||||||
|
if (len(requestBody)) {
|
||||||
|
requestData = deserializeJSON(requestBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
businessID = 0;
|
||||||
|
if (structKeyExists(requestData, "BusinessID")) {
|
||||||
|
businessID = val(requestData.BusinessID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_business_id", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
method = cgi.REQUEST_METHOD;
|
||||||
|
action = structKeyExists(requestData, "action") ? lCase(requestData.action) : "list";
|
||||||
|
|
||||||
|
// Handle different actions
|
||||||
|
switch (action) {
|
||||||
|
case "list":
|
||||||
|
// Get all active menus for this business
|
||||||
|
qMenus = queryExecute("
|
||||||
|
SELECT
|
||||||
|
MenuID,
|
||||||
|
MenuName,
|
||||||
|
MenuDescription,
|
||||||
|
MenuDaysActive,
|
||||||
|
MenuStartTime,
|
||||||
|
MenuEndTime,
|
||||||
|
MenuSortOrder,
|
||||||
|
MenuIsActive
|
||||||
|
FROM Menus
|
||||||
|
WHERE MenuBusinessID = :businessID
|
||||||
|
AND MenuIsActive = 1
|
||||||
|
ORDER BY MenuSortOrder, MenuName
|
||||||
|
", { businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
menus = [];
|
||||||
|
for (i = 1; i <= qMenus.recordCount; i++) {
|
||||||
|
// Count categories in this menu
|
||||||
|
qCatCount = queryExecute("
|
||||||
|
SELECT COUNT(*) as cnt FROM Categories
|
||||||
|
WHERE CategoryBusinessID = :businessID
|
||||||
|
AND CategoryMenuID = :menuID
|
||||||
|
", { businessID: businessID, menuID: qMenus.MenuID[i] }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
arrayAppend(menus, {
|
||||||
|
"MenuID": qMenus.MenuID[i],
|
||||||
|
"MenuName": qMenus.MenuName[i],
|
||||||
|
"MenuDescription": isNull(qMenus.MenuDescription[i]) ? "" : qMenus.MenuDescription[i],
|
||||||
|
"MenuDaysActive": qMenus.MenuDaysActive[i],
|
||||||
|
"MenuStartTime": isNull(qMenus.MenuStartTime[i]) ? "" : timeFormat(qMenus.MenuStartTime[i], "HH:mm"),
|
||||||
|
"MenuEndTime": isNull(qMenus.MenuEndTime[i]) ? "" : timeFormat(qMenus.MenuEndTime[i], "HH:mm"),
|
||||||
|
"MenuSortOrder": qMenus.MenuSortOrder[i],
|
||||||
|
"CategoryCount": qCatCount.cnt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"OK": true,
|
||||||
|
"MENUS": menus,
|
||||||
|
"COUNT": arrayLen(menus)
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "get":
|
||||||
|
// Get a single menu by ID
|
||||||
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
||||||
|
if (menuID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_menu_id", "MESSAGE": "MenuID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
qMenu = queryExecute("
|
||||||
|
SELECT * FROM Menus
|
||||||
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
||||||
|
", { menuID: menuID, businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qMenu.recordCount == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "menu_not_found", "MESSAGE": "Menu not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"OK": true,
|
||||||
|
"MENU": {
|
||||||
|
"MenuID": qMenu.MenuID,
|
||||||
|
"MenuName": qMenu.MenuName,
|
||||||
|
"MenuDescription": isNull(qMenu.MenuDescription) ? "" : qMenu.MenuDescription,
|
||||||
|
"MenuDaysActive": qMenu.MenuDaysActive,
|
||||||
|
"MenuStartTime": isNull(qMenu.MenuStartTime) ? "" : timeFormat(qMenu.MenuStartTime, "HH:mm"),
|
||||||
|
"MenuEndTime": isNull(qMenu.MenuEndTime) ? "" : timeFormat(qMenu.MenuEndTime, "HH:mm"),
|
||||||
|
"MenuSortOrder": qMenu.MenuSortOrder,
|
||||||
|
"MenuIsActive": qMenu.MenuIsActive
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "save":
|
||||||
|
// Create or update a menu
|
||||||
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
||||||
|
menuName = structKeyExists(requestData, "MenuName") ? trim(requestData.MenuName) : "";
|
||||||
|
menuDescription = structKeyExists(requestData, "MenuDescription") ? trim(requestData.MenuDescription) : "";
|
||||||
|
menuDaysActive = structKeyExists(requestData, "MenuDaysActive") ? val(requestData.MenuDaysActive) : 127;
|
||||||
|
menuStartTime = structKeyExists(requestData, "MenuStartTime") && len(trim(requestData.MenuStartTime)) ? trim(requestData.MenuStartTime) : javaCast("null", "");
|
||||||
|
menuEndTime = structKeyExists(requestData, "MenuEndTime") && len(trim(requestData.MenuEndTime)) ? trim(requestData.MenuEndTime) : javaCast("null", "");
|
||||||
|
menuSortOrder = structKeyExists(requestData, "MenuSortOrder") ? val(requestData.MenuSortOrder) : 0;
|
||||||
|
|
||||||
|
if (len(menuName) == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_menu_name", "MESSAGE": "Menu name is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuID > 0) {
|
||||||
|
// Update existing menu
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Menus SET
|
||||||
|
MenuName = :menuName,
|
||||||
|
MenuDescription = :menuDescription,
|
||||||
|
MenuDaysActive = :menuDaysActive,
|
||||||
|
MenuStartTime = :menuStartTime,
|
||||||
|
MenuEndTime = :menuEndTime,
|
||||||
|
MenuSortOrder = :menuSortOrder
|
||||||
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
||||||
|
", {
|
||||||
|
menuID: menuID,
|
||||||
|
businessID: businessID,
|
||||||
|
menuName: menuName,
|
||||||
|
menuDescription: menuDescription,
|
||||||
|
menuDaysActive: menuDaysActive,
|
||||||
|
menuStartTime: menuStartTime,
|
||||||
|
menuEndTime: menuEndTime,
|
||||||
|
menuSortOrder: menuSortOrder
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response = { "OK": true, "MenuID": menuID, "ACTION": "updated" };
|
||||||
|
} else {
|
||||||
|
// Create new menu
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Menus (
|
||||||
|
MenuBusinessID, MenuName, MenuDescription,
|
||||||
|
MenuDaysActive, MenuStartTime, MenuEndTime,
|
||||||
|
MenuSortOrder, MenuIsActive, MenuAddedOn
|
||||||
|
) VALUES (
|
||||||
|
:businessID, :menuName, :menuDescription,
|
||||||
|
:menuDaysActive, :menuStartTime, :menuEndTime,
|
||||||
|
:menuSortOrder, 1, NOW()
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
businessID: businessID,
|
||||||
|
menuName: menuName,
|
||||||
|
menuDescription: menuDescription,
|
||||||
|
menuDaysActive: menuDaysActive,
|
||||||
|
menuStartTime: menuStartTime,
|
||||||
|
menuEndTime: menuEndTime,
|
||||||
|
menuSortOrder: menuSortOrder
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
result = queryExecute("SELECT LAST_INSERT_ID() as newID", {}, { datasource: "payfrit" });
|
||||||
|
response = { "OK": true, "MenuID": result.newID, "ACTION": "created" };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
// Soft-delete a menu
|
||||||
|
menuID = structKeyExists(requestData, "MenuID") ? val(requestData.MenuID) : 0;
|
||||||
|
if (menuID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_menu_id", "MESSAGE": "MenuID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if menu has categories
|
||||||
|
qCatCheck = queryExecute("
|
||||||
|
SELECT COUNT(*) as cnt FROM Categories
|
||||||
|
WHERE CategoryMenuID = :menuID
|
||||||
|
", { menuID: menuID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCatCheck.cnt > 0) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "menu_has_categories",
|
||||||
|
"MESSAGE": "Cannot delete menu with categories. Move or delete categories first.",
|
||||||
|
"CATEGORY_COUNT": qCatCheck.cnt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Menus SET MenuIsActive = 0
|
||||||
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
||||||
|
", { menuID: menuID, businessID: businessID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
response = { "OK": true, "MenuID": menuID, "ACTION": "deleted" };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "reorder":
|
||||||
|
// Reorder menus
|
||||||
|
menuOrder = structKeyExists(requestData, "MenuOrder") ? requestData.MenuOrder : [];
|
||||||
|
if (!isArray(menuOrder) || arrayLen(menuOrder) == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_menu_order", "MESSAGE": "MenuOrder array is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 1; i <= arrayLen(menuOrder); i++) {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Menus SET MenuSortOrder = :sortOrder
|
||||||
|
WHERE MenuID = :menuID AND MenuBusinessID = :businessID
|
||||||
|
", {
|
||||||
|
menuID: val(menuOrder[i]),
|
||||||
|
businessID: businessID,
|
||||||
|
sortOrder: i - 1
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
response = { "OK": true, "ACTION": "reordered" };
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_action", "MESSAGE": "Unknown action: " & action });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = "server_error";
|
||||||
|
response["MESSAGE"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail ?: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
111
api/menu/saveCategory.cfm
Normal file
111
api/menu/saveCategory.cfm
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
<cfheader name="Cache-Control" value="no-store">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Save/Update Category
|
||||||
|
*
|
||||||
|
* POST body:
|
||||||
|
* {
|
||||||
|
* "CategoryID": 123, // Required for update
|
||||||
|
* "CategoryBusinessID": 37, // Required for insert
|
||||||
|
* "CategoryName": "Breakfast",
|
||||||
|
* "CategorySortOrder": 1,
|
||||||
|
* "CategoryOrderTypes": "1,2,3", // 1=Dine-In, 2=Takeaway, 3=Delivery
|
||||||
|
* "CategoryScheduleStart": "06:00:00", // Optional, null = always available
|
||||||
|
* "CategoryScheduleEnd": "11:00:00", // Optional
|
||||||
|
* "CategoryScheduleDays": "2,3,4,5,6" // Optional, 1=Sun..7=Sat, null = all days
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw) || len(trim(raw)) == 0) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
return isStruct(data) ? data : {};
|
||||||
|
} catch (any e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = { "OK": false };
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
CategoryID = structKeyExists(data, "CategoryID") ? val(data.CategoryID) : 0;
|
||||||
|
CategoryBusinessID = structKeyExists(data, "CategoryBusinessID") ? val(data.CategoryBusinessID) : 0;
|
||||||
|
CategoryName = structKeyExists(data, "CategoryName") ? left(trim(data.CategoryName), 30) : "";
|
||||||
|
CategorySortOrder = structKeyExists(data, "CategorySortOrder") ? val(data.CategorySortOrder) : 0;
|
||||||
|
CategoryOrderTypes = structKeyExists(data, "CategoryOrderTypes") ? trim(data.CategoryOrderTypes) : "1,2,3";
|
||||||
|
CategoryScheduleStart = structKeyExists(data, "CategoryScheduleStart") && len(trim(data.CategoryScheduleStart))
|
||||||
|
? trim(data.CategoryScheduleStart) : javaCast("null", "");
|
||||||
|
CategoryScheduleEnd = structKeyExists(data, "CategoryScheduleEnd") && len(trim(data.CategoryScheduleEnd))
|
||||||
|
? trim(data.CategoryScheduleEnd) : javaCast("null", "");
|
||||||
|
CategoryScheduleDays = structKeyExists(data, "CategoryScheduleDays") && len(trim(data.CategoryScheduleDays))
|
||||||
|
? trim(data.CategoryScheduleDays) : javaCast("null", "");
|
||||||
|
|
||||||
|
if (CategoryID > 0) {
|
||||||
|
// Update existing category
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Categories SET
|
||||||
|
CategoryName = :name,
|
||||||
|
CategorySortOrder = :sortOrder,
|
||||||
|
CategoryOrderTypes = :orderTypes,
|
||||||
|
CategoryScheduleStart = :schedStart,
|
||||||
|
CategoryScheduleEnd = :schedEnd,
|
||||||
|
CategoryScheduleDays = :schedDays
|
||||||
|
WHERE CategoryID = :catId
|
||||||
|
", {
|
||||||
|
catId: CategoryID,
|
||||||
|
name: CategoryName,
|
||||||
|
sortOrder: CategorySortOrder,
|
||||||
|
orderTypes: CategoryOrderTypes,
|
||||||
|
schedStart: { value = CategoryScheduleStart, cfsqltype = "cf_sql_time", null = isNull(CategoryScheduleStart) },
|
||||||
|
schedEnd: { value = CategoryScheduleEnd, cfsqltype = "cf_sql_time", null = isNull(CategoryScheduleEnd) },
|
||||||
|
schedDays: { value = CategoryScheduleDays, cfsqltype = "cf_sql_varchar", null = isNull(CategoryScheduleDays) }
|
||||||
|
}, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
response["OK"] = true;
|
||||||
|
response["CategoryID"] = CategoryID;
|
||||||
|
response["MESSAGE"] = "Category updated";
|
||||||
|
|
||||||
|
} else if (CategoryBusinessID > 0 && len(CategoryName)) {
|
||||||
|
// Insert new category
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Categories
|
||||||
|
(CategoryBusinessID, CategoryName, CategorySortOrder, CategoryOrderTypes,
|
||||||
|
CategoryScheduleStart, CategoryScheduleEnd, CategoryScheduleDays, CategoryAddedOn)
|
||||||
|
VALUES
|
||||||
|
(:bizId, :name, :sortOrder, :orderTypes, :schedStart, :schedEnd, :schedDays, NOW())
|
||||||
|
", {
|
||||||
|
bizId: CategoryBusinessID,
|
||||||
|
name: CategoryName,
|
||||||
|
sortOrder: CategorySortOrder,
|
||||||
|
orderTypes: CategoryOrderTypes,
|
||||||
|
schedStart: { value = CategoryScheduleStart, cfsqltype = "cf_sql_time", null = isNull(CategoryScheduleStart) },
|
||||||
|
schedEnd: { value = CategoryScheduleEnd, cfsqltype = "cf_sql_time", null = isNull(CategoryScheduleEnd) },
|
||||||
|
schedDays: { value = CategoryScheduleDays, cfsqltype = "cf_sql_varchar", null = isNull(CategoryScheduleDays) }
|
||||||
|
}, { datasource = "payfrit" });
|
||||||
|
|
||||||
|
qNew = queryExecute("SELECT LAST_INSERT_ID() as newId", {}, { datasource = "payfrit" });
|
||||||
|
response["OK"] = true;
|
||||||
|
response["CategoryID"] = qNew.newId;
|
||||||
|
response["MESSAGE"] = "Category created";
|
||||||
|
|
||||||
|
} else {
|
||||||
|
response["ERROR"] = "invalid_params";
|
||||||
|
response["MESSAGE"] = "CategoryID required for update, or CategoryBusinessID and CategoryName for insert";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
response["ERROR"] = "server_error";
|
||||||
|
response["MESSAGE"] = e.message;
|
||||||
|
response["DETAIL"] = e.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON(response));
|
||||||
|
</cfscript>
|
||||||
|
|
@ -151,24 +151,31 @@ try {
|
||||||
categoryID = result.newID;
|
categoryID = result.newID;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Get menu ID from category if provided
|
||||||
|
categoryMenuId = structKeyExists(cat, "menuId") ? val(cat.menuId) : 0;
|
||||||
|
categoryMenuIdParam = categoryMenuId > 0 ? categoryMenuId : javaCast("null", "");
|
||||||
|
|
||||||
if (categoryDbId > 0) {
|
if (categoryDbId > 0) {
|
||||||
categoryID = categoryDbId;
|
categoryID = categoryDbId;
|
||||||
queryExecute("
|
queryExecute("
|
||||||
UPDATE Categories
|
UPDATE Categories
|
||||||
SET CategoryName = :name,
|
SET CategoryName = :name,
|
||||||
CategorySortOrder = :sortOrder
|
CategorySortOrder = :sortOrder,
|
||||||
|
CategoryMenuID = :menuId
|
||||||
WHERE CategoryID = :categoryID
|
WHERE CategoryID = :categoryID
|
||||||
", {
|
", {
|
||||||
categoryID: categoryID,
|
categoryID: categoryID,
|
||||||
name: cat.name,
|
name: cat.name,
|
||||||
sortOrder: catSortOrder
|
sortOrder: catSortOrder,
|
||||||
|
menuId: categoryMenuIdParam
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
queryExecute("
|
queryExecute("
|
||||||
INSERT INTO Categories (CategoryBusinessID, CategoryName, CategorySortOrder, CategoryAddedOn)
|
INSERT INTO Categories (CategoryBusinessID, CategoryMenuID, CategoryName, CategorySortOrder, CategoryAddedOn)
|
||||||
VALUES (:businessID, :name, :sortOrder, NOW())
|
VALUES (:businessID, :menuId, :name, :sortOrder, NOW())
|
||||||
", {
|
", {
|
||||||
businessID: businessID,
|
businessID: businessID,
|
||||||
|
menuId: categoryMenuIdParam,
|
||||||
name: cat.name,
|
name: cat.name,
|
||||||
sortOrder: catSortOrder
|
sortOrder: catSortOrder
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,17 +50,16 @@
|
||||||
<cfset apiAbort({ "OK": false, "ERROR": "invalid_status", "MESSAGE": "Only cart orders can be abandoned.", "DETAIL": "" })>
|
<cfset apiAbort({ "OK": false, "ERROR": "invalid_status", "MESSAGE": "Only cart orders can be abandoned.", "DETAIL": "" })>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<!--- Delete the order completely (cascades to line items via FK or we delete them first) --->
|
<!--- Delete line items --->
|
||||||
<!--- First delete all line items --->
|
|
||||||
<cfset queryExecute(
|
<cfset queryExecute(
|
||||||
"DELETE FROM OrderLineItems WHERE OrderLineItemOrderID = ?",
|
"DELETE FROM OrderLineItems WHERE OrderLineItemOrderID = ?",
|
||||||
[{ value = OrderID, cfsqltype = "cf_sql_integer" }],
|
[{ value = OrderID, cfsqltype = "cf_sql_integer" }],
|
||||||
{ datasource = "payfrit" }
|
{ datasource = "payfrit" }
|
||||||
)>
|
)>
|
||||||
|
|
||||||
<!--- Then delete the order itself --->
|
<!--- Mark order with status 7 (Deleted and started new cart) --->
|
||||||
<cfset queryExecute(
|
<cfset queryExecute(
|
||||||
"DELETE FROM Orders WHERE OrderID = ?",
|
"UPDATE Orders SET OrderStatusID = 7, OrderLastEditedOn = NOW() WHERE OrderID = ?",
|
||||||
[{ value = OrderID, cfsqltype = "cf_sql_integer" }],
|
[{ value = OrderID, cfsqltype = "cf_sql_integer" }],
|
||||||
{ datasource = "payfrit" }
|
{ datasource = "payfrit" }
|
||||||
)>
|
)>
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ try {
|
||||||
", {
|
", {
|
||||||
line1: len(addressLine1) ? addressLine1 : "Address pending",
|
line1: len(addressLine1) ? addressLine1 : "Address pending",
|
||||||
city: len(city) ? city : "",
|
city: len(city) ? city : "",
|
||||||
stateID: stateID,
|
stateID: { value = stateID > 0 ? stateID : javaCast("null", ""), cfsqltype = "cf_sql_integer", null = stateID == 0 },
|
||||||
zip: len(zip) ? zip : "",
|
zip: len(zip) ? zip : "",
|
||||||
userID: userId,
|
userID: userId,
|
||||||
typeID: 2
|
typeID: 2
|
||||||
|
|
@ -118,6 +118,32 @@ try {
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
response.steps.append("Linked address to business");
|
response.steps.append("Linked address to business");
|
||||||
|
|
||||||
|
// Create default task types for the business
|
||||||
|
// 1. Call Server (notifications icon, purple)
|
||||||
|
// 2. Chat With Staff (chat icon, blue)
|
||||||
|
// 3. Pay With Cash (payments icon, green)
|
||||||
|
defaultTaskTypes = [
|
||||||
|
{ name: "Call Server", icon: "notifications", color: "##9C27B0", description: "Request server assistance" },
|
||||||
|
{ name: "Chat With Staff", icon: "chat", color: "##2196F3", description: "Open a chat conversation" },
|
||||||
|
{ name: "Pay With Cash", icon: "payments", color: "##4CAF50", description: "Request to pay with cash" }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (tt = 1; tt <= arrayLen(defaultTaskTypes); tt++) {
|
||||||
|
taskType = defaultTaskTypes[tt];
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO tt_TaskTypes (tt_TaskTypeName, tt_TaskTypeDescription, tt_TaskTypeIcon, tt_TaskTypeColor, tt_TaskTypeBusinessID, tt_TaskTypeSortOrder)
|
||||||
|
VALUES (:name, :description, :icon, :color, :businessID, :sortOrder)
|
||||||
|
", {
|
||||||
|
name: { value: taskType.name, cfsqltype: "cf_sql_varchar" },
|
||||||
|
description: { value: taskType.description, cfsqltype: "cf_sql_varchar" },
|
||||||
|
icon: { value: taskType.icon, cfsqltype: "cf_sql_varchar" },
|
||||||
|
color: { value: taskType.color, cfsqltype: "cf_sql_varchar" },
|
||||||
|
businessID: { value: businessId, cfsqltype: "cf_sql_integer" },
|
||||||
|
sortOrder: { value: tt, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
response.steps.append("Created 3 default task types (Call Server, Chat With Staff, Pay With Cash)");
|
||||||
|
|
||||||
// Save business hours from structured schedule
|
// Save business hours from structured schedule
|
||||||
if (structKeyExists(biz, "hoursSchedule") && isArray(biz.hoursSchedule)) {
|
if (structKeyExists(biz, "hoursSchedule") && isArray(biz.hoursSchedule)) {
|
||||||
hoursSchedule = biz.hoursSchedule;
|
hoursSchedule = biz.hoursSchedule;
|
||||||
|
|
@ -255,6 +281,83 @@ try {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a Menu record for this business (or get existing menu with same name)
|
||||||
|
menuName = structKeyExists(wizardData, "menuName") && isSimpleValue(wizardData.menuName) && len(trim(wizardData.menuName))
|
||||||
|
? trim(wizardData.menuName)
|
||||||
|
: "Main Menu";
|
||||||
|
|
||||||
|
// Get menu time range (optional)
|
||||||
|
menuStartTime = structKeyExists(wizardData, "menuStartTime") && isSimpleValue(wizardData.menuStartTime) && len(trim(wizardData.menuStartTime))
|
||||||
|
? trim(wizardData.menuStartTime)
|
||||||
|
: "";
|
||||||
|
menuEndTime = structKeyExists(wizardData, "menuEndTime") && isSimpleValue(wizardData.menuEndTime) && len(trim(wizardData.menuEndTime))
|
||||||
|
? trim(wizardData.menuEndTime)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Convert HH:MM to HH:MM:SS if needed
|
||||||
|
if (len(menuStartTime) == 5) menuStartTime = menuStartTime & ":00";
|
||||||
|
if (len(menuEndTime) == 5) menuEndTime = menuEndTime & ":00";
|
||||||
|
|
||||||
|
// Validate menu hours fall within business operating hours
|
||||||
|
if (len(menuStartTime) && len(menuEndTime)) {
|
||||||
|
qHours = queryExecute("
|
||||||
|
SELECT MIN(HoursOpenTime) as earliestOpen, MAX(HoursClosingTime) as latestClose
|
||||||
|
FROM Hours
|
||||||
|
WHERE HoursBusinessID = :bizID
|
||||||
|
", { bizID: businessId }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qHours.recordCount > 0 && !isNull(qHours.earliestOpen) && !isNull(qHours.latestClose)) {
|
||||||
|
earliestOpen = timeFormat(qHours.earliestOpen, "HH:mm:ss");
|
||||||
|
latestClose = timeFormat(qHours.latestClose, "HH:mm:ss");
|
||||||
|
|
||||||
|
if (menuStartTime < earliestOpen || menuEndTime > latestClose) {
|
||||||
|
throw(message="Menu hours (" & menuStartTime & " - " & menuEndTime & ") must be within business operating hours (" & earliestOpen & " - " & latestClose & ")");
|
||||||
|
}
|
||||||
|
response.steps.append("Validated menu hours against business hours (" & earliestOpen & " - " & latestClose & ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qMenu = queryExecute("
|
||||||
|
SELECT MenuID FROM Menus
|
||||||
|
WHERE MenuBusinessID = :bizID AND MenuName = :name AND MenuIsActive = 1
|
||||||
|
", { bizID: businessId, name: menuName }, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qMenu.recordCount > 0) {
|
||||||
|
menuID = qMenu.MenuID;
|
||||||
|
// Update existing menu with new time range if provided
|
||||||
|
if (len(menuStartTime) && len(menuEndTime)) {
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Menus SET MenuStartTime = :startTime, MenuEndTime = :endTime
|
||||||
|
WHERE MenuID = :menuID
|
||||||
|
", {
|
||||||
|
menuID: menuID,
|
||||||
|
startTime: menuStartTime,
|
||||||
|
endTime: menuEndTime
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
response.steps.append("Updated existing menu: " & menuName & " (ID: " & menuID & ") with hours " & menuStartTime & " - " & menuEndTime);
|
||||||
|
} else {
|
||||||
|
response.steps.append("Using existing menu: " & menuName & " (ID: " & menuID & ")");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Menus (
|
||||||
|
MenuBusinessID, MenuName, MenuDaysActive, MenuStartTime, MenuEndTime, MenuSortOrder, MenuIsActive, MenuAddedOn
|
||||||
|
) VALUES (
|
||||||
|
:bizID, :name, 127, :startTime, :endTime, 0, 1, NOW()
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
bizID: businessId,
|
||||||
|
name: menuName,
|
||||||
|
startTime: len(menuStartTime) ? menuStartTime : javaCast("null", ""),
|
||||||
|
endTime: len(menuEndTime) ? menuEndTime : javaCast("null", "")
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
qNewMenu = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
|
menuID = qNewMenu.id;
|
||||||
|
timeInfo = len(menuStartTime) && len(menuEndTime) ? " (" & menuStartTime & " - " & menuEndTime & ")" : " (all day)";
|
||||||
|
response.steps.append("Created menu: " & menuName & timeInfo & " (ID: " & menuID & ")");
|
||||||
|
}
|
||||||
|
|
||||||
// Build category map
|
// Build category map
|
||||||
categories = structKeyExists(wizardData, "categories") ? wizardData.categories : [];
|
categories = structKeyExists(wizardData, "categories") ? wizardData.categories : [];
|
||||||
categoryMap = {}; // Maps category name to CategoryID
|
categoryMap = {}; // Maps category name to CategoryID
|
||||||
|
|
@ -270,32 +373,33 @@ try {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if category exists in Categories table
|
// Check if category exists in Categories table for this menu
|
||||||
qCat = queryExecute("
|
qCat = queryExecute("
|
||||||
SELECT CategoryID FROM Categories
|
SELECT CategoryID FROM Categories
|
||||||
WHERE CategoryBusinessID = :bizID AND CategoryName = :name
|
WHERE CategoryBusinessID = :bizID AND CategoryName = :name AND CategoryMenuID = :menuID
|
||||||
", { bizID: businessId, name: catName }, { datasource: "payfrit" });
|
", { bizID: businessId, name: catName, menuID: menuID }, { datasource: "payfrit" });
|
||||||
|
|
||||||
if (qCat.recordCount > 0) {
|
if (qCat.recordCount > 0) {
|
||||||
categoryID = qCat.CategoryID;
|
categoryID = qCat.CategoryID;
|
||||||
response.steps.append("Category exists: " & catName & " (ID: " & categoryID & ")");
|
response.steps.append("Category exists: " & catName & " (ID: " & categoryID & ")");
|
||||||
} else {
|
} else {
|
||||||
// Create category in Categories table
|
// Create category in Categories table with MenuID
|
||||||
queryExecute("
|
queryExecute("
|
||||||
INSERT INTO Categories (
|
INSERT INTO Categories (
|
||||||
CategoryBusinessID, CategoryName, CategorySortOrder
|
CategoryBusinessID, CategoryMenuID, CategoryName, CategorySortOrder
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:bizID, :name, :sortOrder
|
:bizID, :menuID, :name, :sortOrder
|
||||||
)
|
)
|
||||||
", {
|
", {
|
||||||
bizID: businessId,
|
bizID: businessId,
|
||||||
|
menuID: menuID,
|
||||||
name: catName,
|
name: catName,
|
||||||
sortOrder: catOrder
|
sortOrder: catOrder
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||||
categoryID = qNewCat.id;
|
categoryID = qNewCat.id;
|
||||||
response.steps.append("Created category: " & catName & " (ID: " & categoryID & ")");
|
response.steps.append("Created category: " & catName & " in menu " & menuName & " (ID: " & categoryID & ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryMap[catName] = categoryID;
|
categoryMap[catName] = categoryID;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ try {
|
||||||
orderID = val(structKeyExists(data, "OrderID") ? data.OrderID : 0);
|
orderID = val(structKeyExists(data, "OrderID") ? data.OrderID : 0);
|
||||||
message = trim(structKeyExists(data, "Message") ? data.Message : "");
|
message = trim(structKeyExists(data, "Message") ? data.Message : "");
|
||||||
userID = val(structKeyExists(data, "UserID") ? data.UserID : 0);
|
userID = val(structKeyExists(data, "UserID") ? data.UserID : 0);
|
||||||
|
taskTypeID = val(structKeyExists(data, "TaskTypeID") ? data.TaskTypeID : 0);
|
||||||
|
|
||||||
if (businessID == 0) {
|
if (businessID == 0) {
|
||||||
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
|
@ -105,7 +106,7 @@ try {
|
||||||
:businessID,
|
:businessID,
|
||||||
:categoryID,
|
:categoryID,
|
||||||
:orderID,
|
:orderID,
|
||||||
1,
|
:taskTypeID,
|
||||||
:title,
|
:title,
|
||||||
:details,
|
:details,
|
||||||
0,
|
0,
|
||||||
|
|
@ -115,6 +116,7 @@ try {
|
||||||
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
|
||||||
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer" },
|
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer" },
|
||||||
orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 },
|
orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 },
|
||||||
|
taskTypeID: { value: taskTypeID > 0 ? taskTypeID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: taskTypeID == 0 },
|
||||||
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
|
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
|
||||||
details: { value: taskDetails, cfsqltype: "cf_sql_varchar" }
|
details: { value: taskDetails, cfsqltype: "cf_sql_varchar" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<cfscript>
|
<cfscript>
|
||||||
// Customer initiates a chat with staff
|
// Customer initiates a chat with staff
|
||||||
// Input: BusinessID, ServicePointID, OrderID (optional), Message (optional initial message)
|
// Input: BusinessID, ServicePointID, OrderID (optional), UserID (optional), Message (optional initial message)
|
||||||
// Output: { OK: true, TaskID: ... }
|
// Output: { OK: true, TaskID: ... }
|
||||||
|
|
||||||
function apiAbort(required struct payload) {
|
function apiAbort(required struct payload) {
|
||||||
|
|
@ -35,41 +35,48 @@ try {
|
||||||
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (servicePointID == 0) {
|
// ServicePointID = 0 is allowed for remote chats (non-dine-in users)
|
||||||
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "ServicePointID is required" });
|
// In that case, we use userID to match existing chats
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing open chat for this user at this business
|
// Check for existing open chat at this service point
|
||||||
// An open chat is one where TaskTypeID=2 (Chat) and TaskCompletedOn IS NULL
|
// An open chat is one where TaskTypeID=2 (Chat) and TaskCompletedOn IS NULL
|
||||||
// Also check it's tied to the current order if we have one
|
|
||||||
forceNew = structKeyExists(data, "ForceNew") && data.ForceNew == true;
|
forceNew = structKeyExists(data, "ForceNew") && data.ForceNew == true;
|
||||||
|
|
||||||
if (userID > 0 && !forceNew) {
|
if (!forceNew) {
|
||||||
|
// Look for any active chat for this business
|
||||||
|
// Check by: order match, service point match, OR user match (for remote chats)
|
||||||
existingChat = queryExecute("
|
existingChat = queryExecute("
|
||||||
SELECT t.TaskID, t.TaskAddedOn
|
SELECT t.TaskID, t.TaskAddedOn,
|
||||||
|
(SELECT MAX(cm.CreatedOn) FROM ChatMessages cm WHERE cm.TaskID = t.TaskID) as LastMessageTime
|
||||||
FROM Tasks t
|
FROM Tasks t
|
||||||
LEFT JOIN Orders o ON o.OrderID = t.TaskOrderID
|
LEFT JOIN ChatMessages cm2 ON cm2.TaskID = t.TaskID AND cm2.SenderUserID = :userID
|
||||||
WHERE t.TaskBusinessID = :businessID
|
WHERE t.TaskBusinessID = :businessID
|
||||||
AND t.TaskTypeID = 2
|
AND t.TaskTypeID = 2
|
||||||
AND t.TaskCompletedOn IS NULL
|
AND t.TaskCompletedOn IS NULL
|
||||||
AND (
|
AND (
|
||||||
(t.TaskOrderID = :orderID AND :orderID > 0)
|
(t.TaskOrderID = :orderID AND :orderID > 0)
|
||||||
OR (t.TaskOrderID IS NULL AND o.OrderUserID = :userID)
|
OR (t.TaskSourceType = 'servicepoint' AND t.TaskSourceID = :servicePointID AND :servicePointID > 0)
|
||||||
OR (t.TaskOrderID IS NULL AND t.TaskSourceID = :servicePointID)
|
OR (t.TaskSourceType = 'user' AND t.TaskSourceID = :userID AND :userID > 0)
|
||||||
|
OR (cm2.SenderUserID = :userID AND :userID > 0)
|
||||||
)
|
)
|
||||||
ORDER BY t.TaskAddedOn DESC
|
ORDER BY t.TaskAddedOn DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
", {
|
", {
|
||||||
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
|
||||||
userID: { value: userID, cfsqltype: "cf_sql_integer" },
|
|
||||||
servicePointID: { value: servicePointID, cfsqltype: "cf_sql_integer" },
|
servicePointID: { value: servicePointID, cfsqltype: "cf_sql_integer" },
|
||||||
orderID: { value: orderID, cfsqltype: "cf_sql_integer" }
|
orderID: { value: orderID, cfsqltype: "cf_sql_integer" },
|
||||||
|
userID: { value: userID, cfsqltype: "cf_sql_integer" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
if (existingChat.recordCount > 0) {
|
if (existingChat.recordCount > 0) {
|
||||||
// Check if chat is stale (more than 20 minutes old with no activity)
|
// Check if chat is stale (more than 30 minutes since last message, or 30 min since creation if no messages)
|
||||||
chatAge = dateDiff("n", existingChat.TaskAddedOn, now());
|
lastActivity = existingChat.LastMessageTime;
|
||||||
if (chatAge > 20) {
|
if (isNull(lastActivity) || !isDate(lastActivity)) {
|
||||||
|
lastActivity = existingChat.TaskAddedOn;
|
||||||
|
}
|
||||||
|
chatAge = dateDiff("n", lastActivity, now());
|
||||||
|
|
||||||
|
if (chatAge > 30) {
|
||||||
// Auto-close stale chat
|
// Auto-close stale chat
|
||||||
queryExecute("
|
queryExecute("
|
||||||
UPDATE Tasks SET TaskCompletedOn = NOW()
|
UPDATE Tasks SET TaskCompletedOn = NOW()
|
||||||
|
|
@ -87,12 +94,14 @@ try {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get service point info (table name)
|
// Get service point info (table name) - only if dine-in
|
||||||
spQuery = queryExecute("
|
tableName = "";
|
||||||
SELECT ServicePointName FROM ServicePoints WHERE ServicePointID = :spID
|
if (servicePointID > 0) {
|
||||||
", { spID: { value: servicePointID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
spQuery = queryExecute("
|
||||||
|
SELECT ServicePointName FROM ServicePoints WHERE ServicePointID = :spID
|
||||||
tableName = spQuery.recordCount ? spQuery.ServicePointName : "Table ##" & servicePointID;
|
", { spID: { value: servicePointID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||||
|
tableName = spQuery.recordCount ? spQuery.ServicePointName : "Table ##" & servicePointID;
|
||||||
|
}
|
||||||
|
|
||||||
// Get user name if available
|
// Get user name if available
|
||||||
userName = "";
|
userName = "";
|
||||||
|
|
@ -105,10 +114,20 @@ try {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create task title
|
// Create task title - different format for dine-in vs remote
|
||||||
taskTitle = "Chat - " & tableName;
|
if (servicePointID > 0) {
|
||||||
if (len(userName)) {
|
// Dine-in: "Chat - UserName (TableName)" or "Chat - TableName"
|
||||||
taskTitle = "Chat - " & userName & " (" & tableName & ")";
|
taskTitle = "Chat - " & tableName;
|
||||||
|
if (len(userName)) {
|
||||||
|
taskTitle = "Chat - " & userName & " (" & tableName & ")";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remote: "Chat - UserName (Remote)" or "Remote Chat"
|
||||||
|
if (len(userName)) {
|
||||||
|
taskTitle = "Chat - " & userName & " (Remote)";
|
||||||
|
} else {
|
||||||
|
taskTitle = "Remote Chat";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
taskDetails = "Customer initiated chat";
|
taskDetails = "Customer initiated chat";
|
||||||
|
|
@ -136,6 +155,15 @@ try {
|
||||||
categoryID = catQuery.TaskCategoryID;
|
categoryID = catQuery.TaskCategoryID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine source type and ID based on dine-in vs remote
|
||||||
|
if (servicePointID > 0) {
|
||||||
|
sourceType = "servicepoint";
|
||||||
|
sourceID = servicePointID;
|
||||||
|
} else {
|
||||||
|
sourceType = "user";
|
||||||
|
sourceID = userID;
|
||||||
|
}
|
||||||
|
|
||||||
// Insert task with TaskTypeID = 2 (Chat)
|
// Insert task with TaskTypeID = 2 (Chat)
|
||||||
queryExecute("
|
queryExecute("
|
||||||
INSERT INTO Tasks (
|
INSERT INTO Tasks (
|
||||||
|
|
@ -157,8 +185,8 @@ try {
|
||||||
:title,
|
:title,
|
||||||
:details,
|
:details,
|
||||||
0,
|
0,
|
||||||
'servicepoint',
|
:sourceType,
|
||||||
:servicePointID,
|
:sourceID,
|
||||||
NOW()
|
NOW()
|
||||||
)
|
)
|
||||||
", {
|
", {
|
||||||
|
|
@ -167,7 +195,8 @@ try {
|
||||||
orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 },
|
orderID: { value: orderID > 0 ? orderID : javaCast("null", ""), cfsqltype: "cf_sql_integer", null: orderID == 0 },
|
||||||
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
|
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
|
||||||
details: { value: taskDetails, cfsqltype: "cf_sql_varchar" },
|
details: { value: taskDetails, cfsqltype: "cf_sql_varchar" },
|
||||||
servicePointID: { value: servicePointID, cfsqltype: "cf_sql_integer" }
|
sourceType: { value: sourceType, cfsqltype: "cf_sql_varchar" },
|
||||||
|
sourceID: { value: sourceID, cfsqltype: "cf_sql_integer" }
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
// Get the new task ID
|
// Get the new task ID
|
||||||
|
|
|
||||||
86
api/tasks/deleteType.cfm
Normal file
86
api/tasks/deleteType.cfm
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Delete a task type for a business
|
||||||
|
// Input: TaskTypeID (required), BusinessID (required)
|
||||||
|
// Output: { OK: true }
|
||||||
|
|
||||||
|
function apiAbort(required struct payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw)) raw = "";
|
||||||
|
if (!len(trim(raw))) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
if (isStruct(data)) return data;
|
||||||
|
} catch (any e) {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
// Get TaskTypeID
|
||||||
|
taskTypeID = 0;
|
||||||
|
if (structKeyExists(data, "TaskTypeID") && isNumeric(data.TaskTypeID)) {
|
||||||
|
taskTypeID = int(data.TaskTypeID);
|
||||||
|
}
|
||||||
|
if (taskTypeID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskTypeID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get BusinessID
|
||||||
|
businessID = 0;
|
||||||
|
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) {
|
||||||
|
businessID = int(data.BusinessID);
|
||||||
|
}
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify task type exists and belongs to this business
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT tt_TaskTypeID, tt_TaskTypeBusinessID
|
||||||
|
FROM tt_TaskTypes
|
||||||
|
WHERE tt_TaskTypeID = :taskTypeID
|
||||||
|
", {
|
||||||
|
taskTypeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task type not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNull(qCheck.tt_TaskTypeBusinessID) || qCheck.tt_TaskTypeBusinessID != businessID) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "not_authorized", "MESSAGE": "Task type does not belong to this business" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the task type
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM tt_TaskTypes
|
||||||
|
WHERE tt_TaskTypeID = :taskTypeID
|
||||||
|
AND tt_TaskTypeBusinessID = :businessID
|
||||||
|
", {
|
||||||
|
taskTypeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" },
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Task type deleted"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
85
api/tasks/listAllTypes.cfm
Normal file
85
api/tasks/listAllTypes.cfm
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Returns all task types for a business (for portal management)
|
||||||
|
// Input: BusinessID (required)
|
||||||
|
// Output: { OK: true, TASK_TYPES: [...] }
|
||||||
|
|
||||||
|
function apiAbort(required struct payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw)) raw = "";
|
||||||
|
if (!len(trim(raw))) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
if (isStruct(data)) return data;
|
||||||
|
} catch (any e) {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
// Get BusinessID from body, header, or URL
|
||||||
|
businessID = 0;
|
||||||
|
httpHeaders = getHttpRequestData().headers;
|
||||||
|
|
||||||
|
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) {
|
||||||
|
businessID = int(data.BusinessID);
|
||||||
|
} else if (structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"])) {
|
||||||
|
businessID = int(httpHeaders["X-Business-ID"]);
|
||||||
|
} else if (structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID)) {
|
||||||
|
businessID = int(url.BusinessID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get task types for this business
|
||||||
|
q = queryExecute("
|
||||||
|
SELECT
|
||||||
|
tt_TaskTypeID as TaskTypeID,
|
||||||
|
tt_TaskTypeName as TaskTypeName,
|
||||||
|
tt_TaskTypeDescription as TaskTypeDescription,
|
||||||
|
tt_TaskTypeIcon as TaskTypeIcon,
|
||||||
|
tt_TaskTypeColor as TaskTypeColor,
|
||||||
|
tt_TaskTypeSortOrder as SortOrder
|
||||||
|
FROM tt_TaskTypes
|
||||||
|
WHERE tt_TaskTypeBusinessID = :businessID
|
||||||
|
ORDER BY tt_TaskTypeSortOrder, tt_TaskTypeID
|
||||||
|
", {
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
taskTypes = [];
|
||||||
|
for (row in q) {
|
||||||
|
arrayAppend(taskTypes, {
|
||||||
|
"TaskTypeID": row.TaskTypeID,
|
||||||
|
"TaskTypeName": row.TaskTypeName,
|
||||||
|
"TaskTypeDescription": isNull(row.TaskTypeDescription) ? "" : row.TaskTypeDescription,
|
||||||
|
"TaskTypeIcon": isNull(row.TaskTypeIcon) ? "notifications" : row.TaskTypeIcon,
|
||||||
|
"TaskTypeColor": isNull(row.TaskTypeColor) ? "##9C27B0" : row.TaskTypeColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"TASK_TYPES": taskTypes,
|
||||||
|
"COUNT": arrayLen(taskTypes)
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -71,15 +71,21 @@
|
||||||
t.TaskCategoryID,
|
t.TaskCategoryID,
|
||||||
t.TaskOrderID,
|
t.TaskOrderID,
|
||||||
t.TaskTypeID,
|
t.TaskTypeID,
|
||||||
|
t.TaskTitle,
|
||||||
|
t.TaskDetails,
|
||||||
t.TaskAddedOn,
|
t.TaskAddedOn,
|
||||||
t.TaskClaimedByUserID,
|
t.TaskClaimedByUserID,
|
||||||
t.TaskClaimedOn,
|
t.TaskClaimedOn,
|
||||||
t.TaskCompletedOn,
|
t.TaskCompletedOn,
|
||||||
tc.TaskCategoryName,
|
tc.TaskCategoryName,
|
||||||
tc.TaskCategoryColor,
|
tc.TaskCategoryColor,
|
||||||
|
tt.tt_TaskTypeName AS TaskTypeName,
|
||||||
|
tt.tt_TaskTypeIcon AS TaskTypeIcon,
|
||||||
|
tt.tt_TaskTypeColor AS TaskTypeColor,
|
||||||
b.BusinessName
|
b.BusinessName
|
||||||
FROM Tasks t
|
FROM Tasks t
|
||||||
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
|
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
|
||||||
|
LEFT JOIN tt_TaskTypes tt ON tt.tt_TaskTypeID = t.TaskTypeID
|
||||||
LEFT JOIN Businesses b ON b.BusinessID = t.TaskBusinessID
|
LEFT JOIN Businesses b ON b.BusinessID = t.TaskBusinessID
|
||||||
WHERE #whereSQL#
|
WHERE #whereSQL#
|
||||||
ORDER BY t.TaskClaimedOn DESC
|
ORDER BY t.TaskClaimedOn DESC
|
||||||
|
|
@ -88,9 +94,19 @@
|
||||||
<cfset tasks = []>
|
<cfset tasks = []>
|
||||||
|
|
||||||
<cfloop query="qTasks">
|
<cfloop query="qTasks">
|
||||||
<cfset taskTitle = "Task ##" & qTasks.TaskID>
|
<!--- Use stored title if available, otherwise build from order --->
|
||||||
<cfif qTasks.TaskOrderID GT 0>
|
<cfset taskTitle = "">
|
||||||
|
<cfif NOT isNull(qTasks.TaskTitle) AND len(trim(qTasks.TaskTitle)) GT 0>
|
||||||
|
<cfset taskTitle = qTasks.TaskTitle>
|
||||||
|
<cfelseif qTasks.TaskOrderID GT 0>
|
||||||
<cfset taskTitle = "Order ##" & qTasks.TaskOrderID>
|
<cfset taskTitle = "Order ##" & qTasks.TaskOrderID>
|
||||||
|
<cfelse>
|
||||||
|
<cfset taskTitle = "Task ##" & qTasks.TaskID>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfset taskDetails = "">
|
||||||
|
<cfif NOT isNull(qTasks.TaskDetails) AND len(trim(qTasks.TaskDetails)) GT 0>
|
||||||
|
<cfset taskDetails = qTasks.TaskDetails>
|
||||||
</cfif>
|
</cfif>
|
||||||
|
|
||||||
<cfset arrayAppend(tasks, {
|
<cfset arrayAppend(tasks, {
|
||||||
|
|
@ -100,7 +116,7 @@
|
||||||
"TaskCategoryID": qTasks.TaskCategoryID,
|
"TaskCategoryID": qTasks.TaskCategoryID,
|
||||||
"TaskTypeID": qTasks.TaskTypeID,
|
"TaskTypeID": qTasks.TaskTypeID,
|
||||||
"TaskTitle": taskTitle,
|
"TaskTitle": taskTitle,
|
||||||
"TaskDetails": "",
|
"TaskDetails": taskDetails,
|
||||||
"TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
|
"TaskCreatedOn": dateFormat(qTasks.TaskAddedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskAddedOn, "HH:mm:ss"),
|
||||||
"TaskClaimedOn": (isNull(qTasks.TaskClaimedOn) OR len(trim(qTasks.TaskClaimedOn)) EQ 0) ? "" : dateFormat(qTasks.TaskClaimedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskClaimedOn, "HH:mm:ss"),
|
"TaskClaimedOn": (isNull(qTasks.TaskClaimedOn) OR len(trim(qTasks.TaskClaimedOn)) EQ 0) ? "" : dateFormat(qTasks.TaskClaimedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskClaimedOn, "HH:mm:ss"),
|
||||||
"TaskCompletedOn": (isNull(qTasks.TaskCompletedOn) OR len(trim(qTasks.TaskCompletedOn)) EQ 0) ? "" : dateFormat(qTasks.TaskCompletedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskCompletedOn, "HH:mm:ss"),
|
"TaskCompletedOn": (isNull(qTasks.TaskCompletedOn) OR len(trim(qTasks.TaskCompletedOn)) EQ 0) ? "" : dateFormat(qTasks.TaskCompletedOn, "yyyy-mm-dd") & "T" & timeFormat(qTasks.TaskCompletedOn, "HH:mm:ss"),
|
||||||
|
|
@ -108,7 +124,10 @@
|
||||||
"TaskSourceType": "order",
|
"TaskSourceType": "order",
|
||||||
"TaskSourceID": qTasks.TaskOrderID,
|
"TaskSourceID": qTasks.TaskOrderID,
|
||||||
"TaskCategoryName": len(trim(qTasks.TaskCategoryName)) ? qTasks.TaskCategoryName : "General",
|
"TaskCategoryName": len(trim(qTasks.TaskCategoryName)) ? qTasks.TaskCategoryName : "General",
|
||||||
"TaskCategoryColor": len(trim(qTasks.TaskCategoryColor)) ? qTasks.TaskCategoryColor : "##888888"
|
"TaskCategoryColor": len(trim(qTasks.TaskCategoryColor)) ? qTasks.TaskCategoryColor : "##888888",
|
||||||
|
"TaskTypeName": len(trim(qTasks.TaskTypeName)) ? qTasks.TaskTypeName : "",
|
||||||
|
"TaskTypeIcon": len(trim(qTasks.TaskTypeIcon)) ? qTasks.TaskTypeIcon : "notifications",
|
||||||
|
"TaskTypeColor": len(trim(qTasks.TaskTypeColor)) ? qTasks.TaskTypeColor : "##9C27B0"
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,13 @@
|
||||||
t.TaskAddedOn,
|
t.TaskAddedOn,
|
||||||
t.TaskClaimedByUserID,
|
t.TaskClaimedByUserID,
|
||||||
tc.TaskCategoryName,
|
tc.TaskCategoryName,
|
||||||
tc.TaskCategoryColor
|
tc.TaskCategoryColor,
|
||||||
|
tt.tt_TaskTypeName AS TaskTypeName,
|
||||||
|
tt.tt_TaskTypeIcon AS TaskTypeIcon,
|
||||||
|
tt.tt_TaskTypeColor AS TaskTypeColor
|
||||||
FROM Tasks t
|
FROM Tasks t
|
||||||
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
|
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
|
||||||
|
LEFT JOIN tt_TaskTypes tt ON tt.tt_TaskTypeID = t.TaskTypeID
|
||||||
WHERE #whereSQL#
|
WHERE #whereSQL#
|
||||||
ORDER BY t.TaskAddedOn ASC
|
ORDER BY t.TaskAddedOn ASC
|
||||||
", params, { datasource = "payfrit" })>
|
", params, { datasource = "payfrit" })>
|
||||||
|
|
@ -100,7 +104,10 @@
|
||||||
"TaskSourceType": "order",
|
"TaskSourceType": "order",
|
||||||
"TaskSourceID": qTasks.TaskOrderID,
|
"TaskSourceID": qTasks.TaskOrderID,
|
||||||
"TaskCategoryName": len(trim(qTasks.TaskCategoryName)) ? qTasks.TaskCategoryName : "General",
|
"TaskCategoryName": len(trim(qTasks.TaskCategoryName)) ? qTasks.TaskCategoryName : "General",
|
||||||
"TaskCategoryColor": len(trim(qTasks.TaskCategoryColor)) ? qTasks.TaskCategoryColor : "##888888"
|
"TaskCategoryColor": len(trim(qTasks.TaskCategoryColor)) ? qTasks.TaskCategoryColor : "##888888",
|
||||||
|
"TaskTypeName": len(trim(qTasks.TaskTypeName)) ? qTasks.TaskTypeName : "",
|
||||||
|
"TaskTypeIcon": len(trim(qTasks.TaskTypeIcon)) ? qTasks.TaskTypeIcon : "notifications",
|
||||||
|
"TaskTypeColor": len(trim(qTasks.TaskTypeColor)) ? qTasks.TaskTypeColor : "##9C27B0"
|
||||||
})>
|
})>
|
||||||
</cfloop>
|
</cfloop>
|
||||||
|
|
||||||
|
|
|
||||||
86
api/tasks/listTypes.cfm
Normal file
86
api/tasks/listTypes.cfm
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Returns task types for a business
|
||||||
|
// Input: BusinessID (required)
|
||||||
|
// Output: { OK: true, TASK_TYPES: [...] }
|
||||||
|
// Each business has their own task types (no system defaults)
|
||||||
|
|
||||||
|
function apiAbort(required struct payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw)) raw = "";
|
||||||
|
if (!len(trim(raw))) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
if (isStruct(data)) return data;
|
||||||
|
} catch (any e) {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
// Get BusinessID from body, header, or URL
|
||||||
|
businessID = 0;
|
||||||
|
httpHeaders = getHttpRequestData().headers;
|
||||||
|
|
||||||
|
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) {
|
||||||
|
businessID = int(data.BusinessID);
|
||||||
|
} else if (structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"])) {
|
||||||
|
businessID = int(httpHeaders["X-Business-ID"]);
|
||||||
|
} else if (structKeyExists(url, "BusinessID") && isNumeric(url.BusinessID)) {
|
||||||
|
businessID = int(url.BusinessID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get task types for this business
|
||||||
|
q = queryExecute("
|
||||||
|
SELECT
|
||||||
|
tt_TaskTypeID as TaskTypeID,
|
||||||
|
tt_TaskTypeName as TaskTypeName,
|
||||||
|
tt_TaskTypeDescription as TaskTypeDescription,
|
||||||
|
tt_TaskTypeIcon as TaskTypeIcon,
|
||||||
|
tt_TaskTypeColor as TaskTypeColor,
|
||||||
|
tt_TaskTypeSortOrder as SortOrder
|
||||||
|
FROM tt_TaskTypes
|
||||||
|
WHERE tt_TaskTypeBusinessID = :businessID
|
||||||
|
ORDER BY tt_TaskTypeSortOrder, tt_TaskTypeID
|
||||||
|
", {
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
taskTypes = [];
|
||||||
|
for (row in q) {
|
||||||
|
arrayAppend(taskTypes, {
|
||||||
|
"TaskTypeID": row.TaskTypeID,
|
||||||
|
"TaskTypeName": row.TaskTypeName,
|
||||||
|
"TaskTypeDescription": isNull(row.TaskTypeDescription) ? "" : row.TaskTypeDescription,
|
||||||
|
"TaskTypeIcon": isNull(row.TaskTypeIcon) ? "notifications" : row.TaskTypeIcon,
|
||||||
|
"TaskTypeColor": isNull(row.TaskTypeColor) ? "##9C27B0" : row.TaskTypeColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"TASK_TYPES": taskTypes,
|
||||||
|
"COUNT": arrayLen(taskTypes)
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
74
api/tasks/reorderTypes.cfm
Normal file
74
api/tasks/reorderTypes.cfm
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Update sort order for task types
|
||||||
|
// Input: BusinessID (required), Order (array of TaskTypeIDs in desired order)
|
||||||
|
// Output: { OK: true }
|
||||||
|
|
||||||
|
function apiAbort(required struct payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw)) raw = "";
|
||||||
|
if (!len(trim(raw))) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
if (isStruct(data)) return data;
|
||||||
|
} catch (any e) {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
// Get BusinessID
|
||||||
|
businessID = 0;
|
||||||
|
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) {
|
||||||
|
businessID = int(data.BusinessID);
|
||||||
|
}
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Order array
|
||||||
|
if (!structKeyExists(data, "Order") || !isArray(data.Order)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Order array is required" });
|
||||||
|
}
|
||||||
|
orderArray = data.Order;
|
||||||
|
|
||||||
|
// Update sort order for each task type
|
||||||
|
sortOrder = 0;
|
||||||
|
for (taskTypeID in orderArray) {
|
||||||
|
if (isNumeric(taskTypeID)) {
|
||||||
|
sortOrder++;
|
||||||
|
queryExecute("
|
||||||
|
UPDATE tt_TaskTypes
|
||||||
|
SET tt_TaskTypeSortOrder = :sortOrder
|
||||||
|
WHERE tt_TaskTypeID = :taskTypeID
|
||||||
|
AND tt_TaskTypeBusinessID = :businessID
|
||||||
|
", {
|
||||||
|
sortOrder: { value: sortOrder, cfsqltype: "cf_sql_integer" },
|
||||||
|
taskTypeID: { value: int(taskTypeID), cfsqltype: "cf_sql_integer" },
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"MESSAGE": "Sort order updated"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
152
api/tasks/saveType.cfm
Normal file
152
api/tasks/saveType.cfm
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
// Create or update a task type for a business
|
||||||
|
// Input: BusinessID (required), TaskTypeName (required), TaskTypeDescription (optional), TaskTypeIcon (optional), TaskTypeID (optional - for update)
|
||||||
|
// Output: { OK: true, TASK_TYPE_ID: ... }
|
||||||
|
|
||||||
|
function apiAbort(required struct payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonBody() {
|
||||||
|
var raw = getHttpRequestData().content;
|
||||||
|
if (isNull(raw)) raw = "";
|
||||||
|
if (!len(trim(raw))) return {};
|
||||||
|
try {
|
||||||
|
var data = deserializeJSON(raw);
|
||||||
|
if (isStruct(data)) return data;
|
||||||
|
} catch (any e) {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = readJsonBody();
|
||||||
|
|
||||||
|
// Get BusinessID
|
||||||
|
businessID = 0;
|
||||||
|
if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) {
|
||||||
|
businessID = int(data.BusinessID);
|
||||||
|
}
|
||||||
|
if (businessID == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get TaskTypeName
|
||||||
|
taskTypeName = "";
|
||||||
|
if (structKeyExists(data, "TaskTypeName")) {
|
||||||
|
taskTypeName = trim(toString(data.TaskTypeName));
|
||||||
|
}
|
||||||
|
if (!len(taskTypeName)) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskTypeName is required" });
|
||||||
|
}
|
||||||
|
if (len(taskTypeName) > 45) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_params", "MESSAGE": "TaskTypeName must be 45 characters or less" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get TaskTypeDescription (optional)
|
||||||
|
taskTypeDescription = "";
|
||||||
|
if (structKeyExists(data, "TaskTypeDescription")) {
|
||||||
|
taskTypeDescription = trim(toString(data.TaskTypeDescription));
|
||||||
|
}
|
||||||
|
if (len(taskTypeDescription) > 100) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_params", "MESSAGE": "TaskTypeDescription must be 100 characters or less" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get TaskTypeIcon (optional, default to 'notifications')
|
||||||
|
taskTypeIcon = "notifications";
|
||||||
|
if (structKeyExists(data, "TaskTypeIcon") && len(trim(toString(data.TaskTypeIcon)))) {
|
||||||
|
taskTypeIcon = trim(toString(data.TaskTypeIcon));
|
||||||
|
}
|
||||||
|
if (len(taskTypeIcon) > 30) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_params", "MESSAGE": "TaskTypeIcon must be 30 characters or less" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get TaskTypeColor (optional, default to purple)
|
||||||
|
taskTypeColor = "##9C27B0";
|
||||||
|
if (structKeyExists(data, "TaskTypeColor") && len(trim(toString(data.TaskTypeColor)))) {
|
||||||
|
taskTypeColor = trim(toString(data.TaskTypeColor));
|
||||||
|
// Ensure it starts with # (incoming JSON has single #, we store single #)
|
||||||
|
if (left(taskTypeColor, 1) != chr(35)) {
|
||||||
|
taskTypeColor = chr(35) & taskTypeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (len(taskTypeColor) > 7) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "invalid_params", "MESSAGE": "TaskTypeColor must be a valid hex color" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get TaskTypeID (optional - for update)
|
||||||
|
taskTypeID = 0;
|
||||||
|
if (structKeyExists(data, "TaskTypeID") && isNumeric(data.TaskTypeID)) {
|
||||||
|
taskTypeID = int(data.TaskTypeID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskTypeID > 0) {
|
||||||
|
// UPDATE - verify it belongs to this business
|
||||||
|
qCheck = queryExecute("
|
||||||
|
SELECT tt_TaskTypeID FROM tt_TaskTypes
|
||||||
|
WHERE tt_TaskTypeID = :taskTypeID
|
||||||
|
AND tt_TaskTypeBusinessID = :businessID
|
||||||
|
", {
|
||||||
|
taskTypeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" },
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
if (qCheck.recordCount == 0) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task type not found or does not belong to this business" });
|
||||||
|
}
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE tt_TaskTypes
|
||||||
|
SET tt_TaskTypeName = :taskTypeName,
|
||||||
|
tt_TaskTypeDescription = :taskTypeDescription,
|
||||||
|
tt_TaskTypeIcon = :taskTypeIcon,
|
||||||
|
tt_TaskTypeColor = :taskTypeColor
|
||||||
|
WHERE tt_TaskTypeID = :taskTypeID
|
||||||
|
", {
|
||||||
|
taskTypeName: { value: taskTypeName, cfsqltype: "cf_sql_varchar" },
|
||||||
|
taskTypeDescription: { value: taskTypeDescription, cfsqltype: "cf_sql_varchar", null: !len(taskTypeDescription) },
|
||||||
|
taskTypeIcon: { value: taskTypeIcon, cfsqltype: "cf_sql_varchar" },
|
||||||
|
taskTypeColor: { value: taskTypeColor, cfsqltype: "cf_sql_varchar" },
|
||||||
|
taskTypeID: { value: taskTypeID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"TASK_TYPE_ID": taskTypeID,
|
||||||
|
"MESSAGE": "Task type updated"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// INSERT new task type
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO tt_TaskTypes (tt_TaskTypeName, tt_TaskTypeDescription, tt_TaskTypeIcon, tt_TaskTypeColor, tt_TaskTypeBusinessID)
|
||||||
|
VALUES (:taskTypeName, :taskTypeDescription, :taskTypeIcon, :taskTypeColor, :businessID)
|
||||||
|
", {
|
||||||
|
taskTypeName: { value: taskTypeName, cfsqltype: "cf_sql_varchar" },
|
||||||
|
taskTypeDescription: { value: taskTypeDescription, cfsqltype: "cf_sql_varchar", null: !len(taskTypeDescription) },
|
||||||
|
taskTypeIcon: { value: taskTypeIcon, cfsqltype: "cf_sql_varchar" },
|
||||||
|
taskTypeColor: { value: taskTypeColor, cfsqltype: "cf_sql_varchar" },
|
||||||
|
businessID: { value: businessID, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
qNew = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });
|
||||||
|
newID = qNew.newID;
|
||||||
|
|
||||||
|
apiAbort({
|
||||||
|
"OK": true,
|
||||||
|
"TASK_TYPE_ID": newID,
|
||||||
|
"MESSAGE": "Task type created"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
|
|
@ -24,14 +24,14 @@
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<a href="#dashboard" class="nav-item active" data-page="dashboard">
|
<a href="#dashboard" class="nav-item active" data-page="dashboard">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||||
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#orders" class="nav-item" data-page="orders">
|
<a href="#orders" class="nav-item" data-page="orders">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/>
|
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/>
|
||||||
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
||||||
<path d="M9 12h6M9 16h6"/>
|
<path d="M9 12h6M9 16h6"/>
|
||||||
|
|
@ -39,19 +39,19 @@
|
||||||
<span>Orders</span>
|
<span>Orders</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="menu-builder.html" class="nav-item">
|
<a href="menu-builder.html" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M3 6h18M3 12h18M3 18h18"/>
|
<path d="M3 6h18M3 12h18M3 18h18"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Menu</span>
|
<span>Menu</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#reports" class="nav-item" data-page="reports">
|
<a href="#reports" class="nav-item" data-page="reports">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M18 20V10M12 20V4M6 20v-6"/>
|
<path d="M18 20V10M12 20V4M6 20v-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Reports</span>
|
<span>Reports</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#team" class="nav-item" data-page="team">
|
<a href="#team" class="nav-item" data-page="team">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
|
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
|
||||||
<circle cx="9" cy="7" r="4"/>
|
<circle cx="9" cy="7" r="4"/>
|
||||||
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
|
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
<span>Team</span>
|
<span>Team</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#beacons" class="nav-item" data-page="beacons">
|
<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">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<path d="M12 2v4M12 18v4M2 12h4M18 12h4"/>
|
<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"/>
|
<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"/>
|
||||||
|
|
@ -67,14 +67,14 @@
|
||||||
<span>Beacons</span>
|
<span>Beacons</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#services" class="nav-item" data-page="services">
|
<a href="#services" class="nav-item" data-page="services">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||||
<path d="M13.73 21a2 2 0 01-3.46 0"/>
|
<path d="M13.73 21a2 2 0 01-3.46 0"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Services</span>
|
<span>Services</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#settings" class="nav-item" data-page="settings">
|
<a href="#settings" class="nav-item" data-page="settings">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
|
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="#logout" class="nav-item logout" data-page="logout">
|
<a href="#logout" class="nav-item logout" data-page="logout">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/>
|
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
|
|
|
||||||
|
|
@ -779,14 +779,14 @@
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<a href="index.html#dashboard" class="nav-item">
|
<a href="index.html#dashboard" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||||
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html#orders" class="nav-item">
|
<a href="index.html#orders" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/>
|
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/>
|
||||||
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
||||||
<path d="M9 12h6M9 16h6"/>
|
<path d="M9 12h6M9 16h6"/>
|
||||||
|
|
@ -794,19 +794,19 @@
|
||||||
<span>Orders</span>
|
<span>Orders</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="nav-item active">
|
<a href="#" class="nav-item active">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M3 6h18M3 12h18M3 18h18"/>
|
<path d="M3 6h18M3 12h18M3 18h18"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Menu</span>
|
<span>Menu</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html#reports" class="nav-item">
|
<a href="index.html#reports" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M18 20V10M12 20V4M6 20v-6"/>
|
<path d="M18 20V10M12 20V4M6 20v-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Reports</span>
|
<span>Reports</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html#team" class="nav-item">
|
<a href="index.html#team" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
|
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
|
||||||
<circle cx="9" cy="7" r="4"/>
|
<circle cx="9" cy="7" r="4"/>
|
||||||
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
|
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
|
||||||
|
|
@ -814,7 +814,7 @@
|
||||||
<span>Team</span>
|
<span>Team</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html#beacons" class="nav-item">
|
<a href="index.html#beacons" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<path d="M12 2v4M12 18v4M2 12h4M18 12h4"/>
|
<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"/>
|
<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"/>
|
||||||
|
|
@ -822,14 +822,14 @@
|
||||||
<span>Beacons</span>
|
<span>Beacons</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html#services" class="nav-item">
|
<a href="index.html#services" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||||
<path d="M13.73 21a2 2 0 01-3.46 0"/>
|
<path d="M13.73 21a2 2 0 01-3.46 0"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Services</span>
|
<span>Services</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html#settings" class="nav-item">
|
<a href="index.html#settings" class="nav-item">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
|
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -846,7 +846,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="#logout" class="nav-item logout" onclick="MenuBuilder.logout()">
|
<a href="#logout" class="nav-item logout" onclick="MenuBuilder.logout()">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/>
|
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
|
|
@ -860,67 +860,66 @@
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="builder-toolbar">
|
<div class="builder-toolbar">
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<div class="toolbar-group">
|
<button class="toolbar-btn" onclick="MenuBuilder.undo()" title="Undo (Ctrl+Z)">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.undo()" title="Undo (Ctrl+Z)">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M3 7v6h6M3 13a9 9 0 103-7.5"/>
|
||||||
<path d="M3 7v6h6M3 13a9 9 0 103-7.5"/>
|
</svg>
|
||||||
</svg>
|
Undo
|
||||||
Undo
|
</button>
|
||||||
</button>
|
<button class="toolbar-btn" onclick="MenuBuilder.redo()" title="Redo (Ctrl+Y)">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.redo()" title="Redo (Ctrl+Y)">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M21 7v6h-6M21 13a9 9 0 10-3-7.5"/>
|
||||||
<path d="M21 7v6h-6M21 13a9 9 0 10-3-7.5"/>
|
</svg>
|
||||||
</svg>
|
Redo
|
||||||
Redo
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="toolbar-group">
|
||||||
<div class="toolbar-group">
|
<button class="toolbar-btn" onclick="MenuBuilder.addCategory()" title="Add Category">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.addCategory()" title="Add Category">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M12 5v14M5 12h14"/>
|
||||||
<path d="M12 5v14M5 12h14"/>
|
</svg>
|
||||||
</svg>
|
Category
|
||||||
Category
|
</button>
|
||||||
</button>
|
<button class="toolbar-btn" onclick="MenuBuilder.addItem()" title="Add Item">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.addItem()" title="Add Item">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M12 5v14M5 12h14"/>
|
||||||
<path d="M12 5v14M5 12h14"/>
|
</svg>
|
||||||
</svg>
|
Item
|
||||||
Item
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="toolbar-group">
|
||||||
<div class="toolbar-group">
|
<button class="toolbar-btn" onclick="MenuBuilder.cloneSelected()" title="Clone (Ctrl+D)">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.cloneSelected()" title="Clone (Ctrl+D)">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
|
||||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
|
</svg>
|
||||||
</svg>
|
Clone
|
||||||
Clone
|
</button>
|
||||||
</button>
|
<button class="toolbar-btn" onclick="MenuBuilder.deleteSelected()" title="Delete (Del)">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.deleteSelected()" title="Delete (Del)">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
||||||
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
</svg>
|
||||||
</svg>
|
Delete
|
||||||
Delete
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="toolbar-group">
|
||||||
<div class="toolbar-group">
|
<button class="toolbar-btn" onclick="MenuBuilder.showOutlineModal()" title="View Full Menu Outline">
|
||||||
<button class="toolbar-btn" onclick="MenuBuilder.showOutlineModal()" title="View Full Menu Outline">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M4 6h16M4 12h16M4 18h12"/>
|
||||||
<path d="M4 6h16M4 12h16M4 18h12"/>
|
</svg>
|
||||||
</svg>
|
Outline
|
||||||
Outline
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="toolbar-group" style="margin-left: auto;">
|
||||||
<div class="toolbar-group" style="margin-left: auto;">
|
<button class="toolbar-btn primary" onclick="MenuBuilder.saveMenu()">
|
||||||
<button class="toolbar-btn primary" onclick="MenuBuilder.saveMenu()">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/>
|
||||||
<path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/>
|
<path d="M17 21v-8H7v8M7 3v5h8"/>
|
||||||
<path d="M17 21v-8H7v8M7 3v5h8"/>
|
</svg>
|
||||||
</svg>
|
Save Menu
|
||||||
Save Menu
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Builder -->
|
<!-- Main Builder -->
|
||||||
<div class="builder-container">
|
<div class="builder-container">
|
||||||
|
|
@ -1011,6 +1010,16 @@
|
||||||
<span id="menuName">Menu Builder</span>
|
<span id="menuName">Menu Builder</span>
|
||||||
<span style="font-size: 13px; color: var(--gray-500); font-weight: normal;" id="businessLabel"></span>
|
<span style="font-size: 13px; color: var(--gray-500); font-weight: normal;" id="businessLabel"></span>
|
||||||
</h2>
|
</h2>
|
||||||
|
<div class="canvas-actions" style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<select id="menuSelector" onchange="MenuBuilder.onMenuSelect(this.value)" style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; background: #fff; font-size: 14px; cursor: pointer;">
|
||||||
|
<option value="0">All Categories</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="MenuBuilder.showMenuManager()" title="Manage Menus">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-structure" id="menuStructure">
|
<div class="menu-structure" id="menuStructure">
|
||||||
|
|
@ -1132,6 +1141,8 @@
|
||||||
menu: {
|
menu: {
|
||||||
categories: []
|
categories: []
|
||||||
},
|
},
|
||||||
|
menus: [], // List of all menus for this business
|
||||||
|
selectedMenuId: 0, // Currently selected menu ID (0 = all/default)
|
||||||
templates: [],
|
templates: [],
|
||||||
stations: [],
|
stations: [],
|
||||||
selectedElement: null,
|
selectedElement: null,
|
||||||
|
|
@ -1685,6 +1696,10 @@
|
||||||
|
|
||||||
// Show properties for category
|
// Show properties for category
|
||||||
showPropertiesForCategory(category) {
|
showPropertiesForCategory(category) {
|
||||||
|
const menuOptions = this.menus.map(m =>
|
||||||
|
`<option value="${m.MenuID}" ${category.menuId === m.MenuID ? 'selected' : ''}>${this.escapeHtml(m.MenuName)}</option>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
document.getElementById('propertiesContent').innerHTML = `
|
document.getElementById('propertiesContent').innerHTML = `
|
||||||
<div class="property-group">
|
<div class="property-group">
|
||||||
<label>Category Name</label>
|
<label>Category Name</label>
|
||||||
|
|
@ -1696,6 +1711,15 @@
|
||||||
<textarea id="propCatDesc" rows="3"
|
<textarea id="propCatDesc" rows="3"
|
||||||
onchange="MenuBuilder.updateCategory('${category.id}', 'description', this.value)">${this.escapeHtml(category.description || '')}</textarea>
|
onchange="MenuBuilder.updateCategory('${category.id}', 'description', this.value)">${this.escapeHtml(category.description || '')}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
${this.menus.length > 0 ? `
|
||||||
|
<div class="property-group">
|
||||||
|
<label>Assign to Menu</label>
|
||||||
|
<select onchange="MenuBuilder.updateCategory('${category.id}', 'menuId', parseInt(this.value))">
|
||||||
|
<option value="0" ${!category.menuId ? 'selected' : ''}>No Menu (Always Show)</option>
|
||||||
|
${menuOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
<div class="property-group">
|
<div class="property-group">
|
||||||
<label>Sort Order</label>
|
<label>Sort Order</label>
|
||||||
<input type="number" id="propCatSort" value="${category.sortOrder}"
|
<input type="number" id="propCatSort" value="${category.sortOrder}"
|
||||||
|
|
@ -3226,14 +3250,21 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load menu from API
|
// Load menu from API
|
||||||
async loadMenu() {
|
async loadMenu(menuId = null) {
|
||||||
try {
|
try {
|
||||||
console.log('[MenuBuilder] Loading menu for BusinessID:', this.config.businessId);
|
// Use provided menuId or current selected
|
||||||
console.log('[MenuBuilder] API URL:', `${this.config.apiBaseUrl}/menu/getForBuilder.cfm`);
|
const loadMenuId = menuId !== null ? menuId : this.selectedMenuId;
|
||||||
|
console.log('[MenuBuilder] Loading menu for BusinessID:', this.config.businessId, 'MenuID:', loadMenuId);
|
||||||
|
|
||||||
|
const payload = { BusinessID: this.config.businessId };
|
||||||
|
if (loadMenuId > 0) {
|
||||||
|
payload.MenuID = loadMenuId;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`${this.config.apiBaseUrl}/menu/getForBuilder.cfm`, {
|
const response = await fetch(`${this.config.apiBaseUrl}/menu/getForBuilder.cfm`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ BusinessID: this.config.businessId })
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
console.log('[MenuBuilder] Response status:', response.status);
|
console.log('[MenuBuilder] Response status:', response.status);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -3243,6 +3274,12 @@
|
||||||
if (data.OK && data.MENU) {
|
if (data.OK && data.MENU) {
|
||||||
this.menu = data.MENU;
|
this.menu = data.MENU;
|
||||||
console.log('[MenuBuilder] Menu categories:', this.menu.categories?.length || 0);
|
console.log('[MenuBuilder] Menu categories:', this.menu.categories?.length || 0);
|
||||||
|
|
||||||
|
// Store menus list and update selector
|
||||||
|
this.menus = data.MENUS || [];
|
||||||
|
this.selectedMenuId = data.SELECTED_MENU_ID || 0;
|
||||||
|
this.updateMenuSelector();
|
||||||
|
|
||||||
// Store templates from API (default to empty array if not provided)
|
// Store templates from API (default to empty array if not provided)
|
||||||
this.templates = data.TEMPLATES || [];
|
this.templates = data.TEMPLATES || [];
|
||||||
// Load brand color if set
|
// Load brand color if set
|
||||||
|
|
@ -3257,6 +3294,8 @@
|
||||||
console.log('[MenuBuilder] No MENU in response or OK=false');
|
console.log('[MenuBuilder] No MENU in response or OK=false');
|
||||||
// Still clear the loading message
|
// Still clear the loading message
|
||||||
this.templates = [];
|
this.templates = [];
|
||||||
|
this.menus = data.MENUS || [];
|
||||||
|
this.updateMenuSelector();
|
||||||
this.renderTemplateLibrary();
|
this.renderTemplateLibrary();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -3267,6 +3306,210 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Update menu selector dropdown
|
||||||
|
updateMenuSelector() {
|
||||||
|
const selector = document.getElementById('menuSelector');
|
||||||
|
if (!selector) return;
|
||||||
|
|
||||||
|
let options = '<option value="0">All Categories</option>';
|
||||||
|
for (const menu of this.menus) {
|
||||||
|
const selected = menu.MenuID === this.selectedMenuId ? 'selected' : '';
|
||||||
|
options += `<option value="${menu.MenuID}" ${selected}>${this.escapeHtml(menu.MenuName)}</option>`;
|
||||||
|
}
|
||||||
|
selector.innerHTML = options;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle menu selection change
|
||||||
|
async onMenuSelect(menuId) {
|
||||||
|
this.selectedMenuId = parseInt(menuId) || 0;
|
||||||
|
await this.loadMenu(this.selectedMenuId);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show menu manager modal
|
||||||
|
showMenuManager() {
|
||||||
|
const content = `
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h4 style="margin: 0;">Your Menus</h4>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="MenuBuilder.showCreateMenuForm()">+ Add Menu</button>
|
||||||
|
</div>
|
||||||
|
<div id="menuManagerList" style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
|
${this.menus.length === 0 ? '<p style="color: var(--gray-500); text-align: center;">No menus created yet. Click "Add Menu" to create one.</p>' : ''}
|
||||||
|
${this.menus.map(menu => `
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--gray-50); border-radius: 8px;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-weight: 600;">${this.escapeHtml(menu.MenuName)}</div>
|
||||||
|
<div style="font-size: 12px; color: var(--gray-500);">
|
||||||
|
${menu.MenuStartTime && menu.MenuEndTime ? `${menu.MenuStartTime} - ${menu.MenuEndTime}` : 'All day'}
|
||||||
|
· ${this.formatDaysActive(menu.MenuDaysActive)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="MenuBuilder.editMenu(${menu.MenuID})">Edit</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="MenuBuilder.deleteMenu(${menu.MenuID})">Delete</button>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
<div id="menuFormContainer" style="display: none; padding: 16px; background: var(--gray-50); border-radius: 8px;">
|
||||||
|
<h4 id="menuFormTitle" style="margin: 0 0 16px 0;">Create Menu</h4>
|
||||||
|
<input type="hidden" id="editMenuId" value="0">
|
||||||
|
<div class="property-field">
|
||||||
|
<label>Menu Name</label>
|
||||||
|
<input type="text" id="menuNameInput" placeholder="e.g., Lunch Menu, Happy Hour">
|
||||||
|
</div>
|
||||||
|
<div class="property-field">
|
||||||
|
<label>Description (optional)</label>
|
||||||
|
<input type="text" id="menuDescInput" placeholder="Brief description">
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 12px;">
|
||||||
|
<div class="property-field" style="flex: 1;">
|
||||||
|
<label>Start Time</label>
|
||||||
|
<input type="time" id="menuStartInput">
|
||||||
|
</div>
|
||||||
|
<div class="property-field" style="flex: 1;">
|
||||||
|
<label>End Time</label>
|
||||||
|
<input type="time" id="menuEndInput">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="property-field">
|
||||||
|
<label>Active Days</label>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||||
|
${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day, i) => `
|
||||||
|
<label style="display: flex; align-items: center; gap: 4px; cursor: pointer;">
|
||||||
|
<input type="checkbox" class="menuDayCheck" data-day="${1 << i}" checked>
|
||||||
|
${day}
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; margin-top: 16px;">
|
||||||
|
<button class="btn btn-primary" onclick="MenuBuilder.saveMenuForm()">Save</button>
|
||||||
|
<button class="btn btn-secondary" onclick="MenuBuilder.cancelMenuForm()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.showModal('Manage Menus', content, [
|
||||||
|
{ text: 'Close', primary: false, action: () => this.closeModal() }
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format days active bitmask to readable string
|
||||||
|
formatDaysActive(bitmask) {
|
||||||
|
if (bitmask === 127) return 'Every day';
|
||||||
|
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
const active = [];
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
if (bitmask & (1 << i)) active.push(days[i]);
|
||||||
|
}
|
||||||
|
return active.join(', ') || 'No days';
|
||||||
|
},
|
||||||
|
|
||||||
|
showCreateMenuForm() {
|
||||||
|
document.getElementById('menuFormContainer').style.display = 'block';
|
||||||
|
document.getElementById('menuFormTitle').textContent = 'Create Menu';
|
||||||
|
document.getElementById('editMenuId').value = '0';
|
||||||
|
document.getElementById('menuNameInput').value = '';
|
||||||
|
document.getElementById('menuDescInput').value = '';
|
||||||
|
document.getElementById('menuStartInput').value = '';
|
||||||
|
document.getElementById('menuEndInput').value = '';
|
||||||
|
document.querySelectorAll('.menuDayCheck').forEach(cb => cb.checked = true);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelMenuForm() {
|
||||||
|
document.getElementById('menuFormContainer').style.display = 'none';
|
||||||
|
},
|
||||||
|
|
||||||
|
async editMenu(menuId) {
|
||||||
|
const menu = this.menus.find(m => m.MenuID === menuId);
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
document.getElementById('menuFormContainer').style.display = 'block';
|
||||||
|
document.getElementById('menuFormTitle').textContent = 'Edit Menu';
|
||||||
|
document.getElementById('editMenuId').value = menuId;
|
||||||
|
document.getElementById('menuNameInput').value = menu.MenuName;
|
||||||
|
document.getElementById('menuDescInput').value = menu.MenuDescription || '';
|
||||||
|
document.getElementById('menuStartInput').value = menu.MenuStartTime || '';
|
||||||
|
document.getElementById('menuEndInput').value = menu.MenuEndTime || '';
|
||||||
|
|
||||||
|
document.querySelectorAll('.menuDayCheck').forEach(cb => {
|
||||||
|
const day = parseInt(cb.dataset.day);
|
||||||
|
cb.checked = (menu.MenuDaysActive & day) !== 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveMenuForm() {
|
||||||
|
const menuId = parseInt(document.getElementById('editMenuId').value) || 0;
|
||||||
|
const menuName = document.getElementById('menuNameInput').value.trim();
|
||||||
|
const menuDesc = document.getElementById('menuDescInput').value.trim();
|
||||||
|
const menuStart = document.getElementById('menuStartInput').value;
|
||||||
|
const menuEnd = document.getElementById('menuEndInput').value;
|
||||||
|
|
||||||
|
let daysActive = 0;
|
||||||
|
document.querySelectorAll('.menuDayCheck').forEach(cb => {
|
||||||
|
if (cb.checked) daysActive |= parseInt(cb.dataset.day);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!menuName) {
|
||||||
|
this.toast('Menu name is required', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/menu/menus.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
BusinessID: this.config.businessId,
|
||||||
|
action: 'save',
|
||||||
|
MenuID: menuId,
|
||||||
|
MenuName: menuName,
|
||||||
|
MenuDescription: menuDesc,
|
||||||
|
MenuStartTime: menuStart,
|
||||||
|
MenuEndTime: menuEnd,
|
||||||
|
MenuDaysActive: daysActive
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast(`Menu ${data.ACTION}!`, 'success');
|
||||||
|
this.closeModal();
|
||||||
|
await this.loadMenu();
|
||||||
|
} else {
|
||||||
|
this.toast(data.MESSAGE || 'Failed to save menu', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.toast('Error saving menu', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteMenu(menuId) {
|
||||||
|
if (!confirm('Are you sure you want to delete this menu? Categories in this menu will become unassigned.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.config.apiBaseUrl}/menu/menus.cfm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
BusinessID: this.config.businessId,
|
||||||
|
action: 'delete',
|
||||||
|
MenuID: menuId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.OK) {
|
||||||
|
this.toast('Menu deleted', 'success');
|
||||||
|
this.closeModal();
|
||||||
|
await this.loadMenu();
|
||||||
|
} else {
|
||||||
|
this.toast(data.MESSAGE || 'Failed to delete menu', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.toast('Error deleting menu', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Render template library in sidebar
|
// Render template library in sidebar
|
||||||
renderTemplateLibrary() {
|
renderTemplateLibrary() {
|
||||||
const container = document.getElementById('templateLibrary');
|
const container = document.getElementById('templateLibrary');
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,17 @@ body {
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
|
|
@ -282,6 +292,20 @@ body {
|
||||||
background: var(--gray-300);
|
background: var(--gray-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
|
||||||
|
|
@ -807,6 +807,20 @@
|
||||||
<h3>Menu Summary</h3>
|
<h3>Menu Summary</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card-body">
|
<div class="summary-card-body">
|
||||||
|
<div class="summary-stat" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
||||||
|
<span class="summary-stat-label">Menu Name</span>
|
||||||
|
<input type="text" id="menuNameInput" value="Main Menu" placeholder="e.g., Main Menu, Lunch, Dinner"
|
||||||
|
style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 14px;">
|
||||||
|
</div>
|
||||||
|
<div class="summary-stat" style="flex-direction: column; align-items: stretch; gap: 8px;">
|
||||||
|
<span class="summary-stat-label">Menu Hours</span>
|
||||||
|
<div style="display: flex; gap: 12px; align-items: center;">
|
||||||
|
<input type="time" id="menuStartTime" style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 14px; flex: 1;">
|
||||||
|
<span style="color: var(--gray-500);">to</span>
|
||||||
|
<input type="time" id="menuEndTime" style="padding: 8px 12px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 14px; flex: 1;">
|
||||||
|
</div>
|
||||||
|
<small style="color: var(--gray-500);">Leave empty for all-day availability. You can create additional menus later in the Menu Builder.</small>
|
||||||
|
</div>
|
||||||
<div class="summary-stat">
|
<div class="summary-stat">
|
||||||
<span class="summary-stat-label">Categories</span>
|
<span class="summary-stat-label">Categories</span>
|
||||||
<span class="summary-stat-value" id="summaryCategories">0</span>
|
<span class="summary-stat-value" id="summaryCategories">0</span>
|
||||||
|
|
@ -1951,6 +1965,19 @@
|
||||||
document.getElementById('summaryModifiers').textContent = modifiers.length;
|
document.getElementById('summaryModifiers').textContent = modifiers.length;
|
||||||
document.getElementById('summaryItems').textContent = items.length;
|
document.getElementById('summaryItems').textContent = items.length;
|
||||||
|
|
||||||
|
// Set default menu hours based on business hours (earliest open, latest close)
|
||||||
|
const hoursSchedule = business.hoursSchedule || [];
|
||||||
|
if (hoursSchedule.length > 0) {
|
||||||
|
let earliestOpen = '23:59';
|
||||||
|
let latestClose = '00:00';
|
||||||
|
hoursSchedule.forEach(day => {
|
||||||
|
if (day.open && day.open < earliestOpen) earliestOpen = day.open;
|
||||||
|
if (day.close && day.close > latestClose) latestClose = day.close;
|
||||||
|
});
|
||||||
|
document.getElementById('menuStartTime').value = earliestOpen;
|
||||||
|
document.getElementById('menuEndTime').value = latestClose;
|
||||||
|
}
|
||||||
|
|
||||||
addMessage('ai', `
|
addMessage('ai', `
|
||||||
<p>Your menu is ready to save!</p>
|
<p>Your menu is ready to save!</p>
|
||||||
<p><strong>${business.name || 'Your Restaurant'}</strong></p>
|
<p><strong>${business.name || 'Your Restaurant'}</strong></p>
|
||||||
|
|
@ -1970,6 +1997,33 @@
|
||||||
console.log('=== SAVE MENU CALLED ===');
|
console.log('=== SAVE MENU CALLED ===');
|
||||||
console.log('Data to save:', config.extractedData);
|
console.log('Data to save:', config.extractedData);
|
||||||
|
|
||||||
|
// Get menu name and time range from inputs
|
||||||
|
const menuName = document.getElementById('menuNameInput').value.trim() || 'Main Menu';
|
||||||
|
const menuStartTime = document.getElementById('menuStartTime').value || '';
|
||||||
|
const menuEndTime = document.getElementById('menuEndTime').value || '';
|
||||||
|
|
||||||
|
// Validate menu hours fall within business operating hours
|
||||||
|
if (menuStartTime && menuEndTime) {
|
||||||
|
const hoursSchedule = config.extractedData.business.hoursSchedule || [];
|
||||||
|
if (hoursSchedule.length > 0) {
|
||||||
|
let earliestOpen = '23:59';
|
||||||
|
let latestClose = '00:00';
|
||||||
|
hoursSchedule.forEach(day => {
|
||||||
|
if (day.open && day.open < earliestOpen) earliestOpen = day.open;
|
||||||
|
if (day.close && day.close > latestClose) latestClose = day.close;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (menuStartTime < earliestOpen || menuEndTime > latestClose) {
|
||||||
|
showToast(`Menu hours must be within business operating hours (${earliestOpen} - ${latestClose})`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.extractedData.menuName = menuName;
|
||||||
|
config.extractedData.menuStartTime = menuStartTime;
|
||||||
|
config.extractedData.menuEndTime = menuEndTime;
|
||||||
|
|
||||||
const saveBtn = document.querySelector('#finalActions .btn-success');
|
const saveBtn = document.querySelector('#finalActions .btn-success');
|
||||||
const originalText = saveBtn.innerHTML;
|
const originalText = saveBtn.innerHTML;
|
||||||
saveBtn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;border-width:2px;"></div> Saving...';
|
saveBtn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;border-width:2px;"></div> Saving...';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue