Add business info, order details, beacon management, and admin tools
API Improvements: - api/businesses/get.cfm: Fetch address from Addresses table, hours from Hours table - api/tasks/getDetails.cfm: Add CustomerPhone field from UserContactNumber - api/orders/getDetail.cfm: New endpoint for order details with line items - api/Application.cfm: Add new admin endpoints to public allowlist Admin Tools: - api/admin/beaconStatus.cfm: View all beacon-to-business mappings - api/admin/updateBeaconMapping.cfm: Change beacon business assignment - api/admin/setupBigDeansInfo.cfm: Set Big Dean's address and hours - api/admin/listTables.cfm: List all database tables - api/admin/describeTable.cfm: Get table structure and sample data - api/admin/randomizePrices.cfm: Randomize item prices for testing - Various Big Dean's debug/update scripts Portal Enhancements: - Enhanced CSS styling for portal pages - Improved portal.js functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e9b44ec4be
commit
ec81af5cdd
18 changed files with 1248 additions and 17 deletions
|
|
@ -78,6 +78,7 @@ if (len(request._api_path)) {
|
|||
if (findNoCase("/api/orders/listForKDS.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/orders/updateStatus.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/orders/checkStatusUpdate.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/orders/getDetail.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
||||
if (findNoCase("/api/tasks/listPending.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/tasks/accept.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
|
@ -112,8 +113,17 @@ if (len(request._api_path)) {
|
|||
if (findNoCase("/api/admin/cleanupCategories.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/deleteOrphans.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/switchBeacons.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/randomizePrices.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/debugTemplateLinks.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/fixBigDeansCategories.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/checkBigDeans.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/updateBigDeans.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/debugBigDeansMenu.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/listTables.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/describeTable.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/setupBigDeansInfo.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/beaconStatus.cfm", request._api_path)) request._api_isPublic = true;
|
||||
if (findNoCase("/api/admin/updateBeaconMapping.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
||||
// Setup/Import endpoints
|
||||
if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;
|
||||
|
|
|
|||
60
api/admin/beaconStatus.cfm
Normal file
60
api/admin/beaconStatus.cfm
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
// Show all beacons with their current business/service point assignments
|
||||
q = queryExecute("
|
||||
SELECT
|
||||
b.BeaconID,
|
||||
b.BeaconUUID,
|
||||
b.BeaconName,
|
||||
lt.BusinessID,
|
||||
lt.ServicePointID,
|
||||
biz.BusinessName,
|
||||
sp.ServicePointName
|
||||
FROM Beacons b
|
||||
LEFT JOIN lt_Beacon_Businesses_ServicePoints lt ON lt.BeaconID = b.BeaconID
|
||||
LEFT JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||
WHERE b.BeaconIsActive = 1
|
||||
ORDER BY b.BeaconID
|
||||
", {}, { datasource: "payfrit" });
|
||||
|
||||
rows = [];
|
||||
for (row in q) {
|
||||
arrayAppend(rows, {
|
||||
"BeaconID": row.BeaconID,
|
||||
"BeaconUUID": row.BeaconUUID,
|
||||
"BeaconName": row.BeaconName ?: "",
|
||||
"BusinessID": row.BusinessID ?: 0,
|
||||
"BusinessName": row.BusinessName ?: "",
|
||||
"ServicePointID": row.ServicePointID ?: 0,
|
||||
"ServicePointName": row.ServicePointName ?: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Also get service points for reference
|
||||
spQuery = queryExecute("
|
||||
SELECT sp.ServicePointID, sp.ServicePointName, sp.ServicePointBusinessID, b.BusinessName
|
||||
FROM ServicePoints sp
|
||||
JOIN Businesses b ON b.BusinessID = sp.ServicePointBusinessID
|
||||
ORDER BY sp.ServicePointBusinessID, sp.ServicePointID
|
||||
", {}, { datasource: "payfrit" });
|
||||
|
||||
servicePoints = [];
|
||||
for (sp in spQuery) {
|
||||
arrayAppend(servicePoints, {
|
||||
"ServicePointID": sp.ServicePointID,
|
||||
"ServicePointName": sp.ServicePointName,
|
||||
"BusinessID": sp.ServicePointBusinessID,
|
||||
"BusinessName": sp.BusinessName
|
||||
});
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"BEACONS": rows,
|
||||
"SERVICE_POINTS": servicePoints
|
||||
}));
|
||||
</cfscript>
|
||||
33
api/admin/checkBigDeans.cfm
Normal file
33
api/admin/checkBigDeans.cfm
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
// Check Big Dean's owner
|
||||
q = queryExecute("
|
||||
SELECT b.BusinessID, b.BusinessName, b.BusinessUserID
|
||||
FROM Businesses b
|
||||
WHERE b.BusinessID = 27
|
||||
", {}, { datasource: "payfrit" });
|
||||
|
||||
// Get users
|
||||
users = queryExecute("
|
||||
SELECT *
|
||||
FROM Users
|
||||
ORDER BY UserID
|
||||
LIMIT 20
|
||||
", {}, { datasource: "payfrit" });
|
||||
|
||||
colNames = users.getColumnNames();
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"BigDeans": {
|
||||
"BusinessID": q.BusinessID,
|
||||
"BusinessName": q.BusinessName,
|
||||
"BusinessUserID": q.BusinessUserID
|
||||
},
|
||||
"UserColumns": colNames,
|
||||
"UserCount": users.recordCount
|
||||
}));
|
||||
</cfscript>
|
||||
53
api/admin/debugBigDeansMenu.cfm
Normal file
53
api/admin/debugBigDeansMenu.cfm
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
businessID = 27;
|
||||
|
||||
// Run the EXACT query from getForBuilder.cfm
|
||||
qCategories = queryExecute("
|
||||
SELECT DISTINCT
|
||||
p.ItemID as CategoryID,
|
||||
p.ItemName as CategoryName,
|
||||
p.ItemSortOrder
|
||||
FROM Items p
|
||||
INNER JOIN Items c ON c.ItemParentItemID = p.ItemID
|
||||
WHERE p.ItemBusinessID = :businessID
|
||||
AND p.ItemParentItemID = 0
|
||||
AND p.ItemIsActive = 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = p.ItemID
|
||||
)
|
||||
ORDER BY p.ItemSortOrder, p.ItemName
|
||||
", { businessID: businessID });
|
||||
|
||||
cats = [];
|
||||
for (c in qCategories) {
|
||||
arrayAppend(cats, {
|
||||
"CategoryID": c.CategoryID,
|
||||
"CategoryName": c.CategoryName
|
||||
});
|
||||
}
|
||||
|
||||
// Also check raw counts
|
||||
rawCount = queryExecute("
|
||||
SELECT COUNT(*) as cnt FROM Items
|
||||
WHERE ItemBusinessID = :bizId AND ItemParentItemID = 0 AND ItemIsActive = 1
|
||||
", { bizId: businessID });
|
||||
|
||||
childrenCount = queryExecute("
|
||||
SELECT COUNT(DISTINCT c.ItemParentItemID) as cnt
|
||||
FROM Items c
|
||||
INNER JOIN Items p ON p.ItemID = c.ItemParentItemID
|
||||
WHERE p.ItemBusinessID = :bizId AND p.ItemParentItemID = 0
|
||||
", { bizId: businessID });
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"CategoriesFromQuery": cats,
|
||||
"CategoryCount": arrayLen(cats),
|
||||
"TotalTopLevelItems": rawCount.cnt,
|
||||
"TopLevelItemsWithChildren": childrenCount.cnt
|
||||
}));
|
||||
</cfscript>
|
||||
54
api/admin/describeTable.cfm
Normal file
54
api/admin/describeTable.cfm
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
requestData = {};
|
||||
if (len(requestBody)) {
|
||||
requestData = deserializeJSON(requestBody);
|
||||
}
|
||||
|
||||
tableName = requestData.table ?: "";
|
||||
if (len(tableName) == 0) {
|
||||
writeOutput(serializeJSON({ "OK": false, "ERROR": "table parameter required" }));
|
||||
abort;
|
||||
}
|
||||
|
||||
// Get table structure
|
||||
cols = queryExecute("DESCRIBE #tableName#", {}, { datasource: "payfrit" });
|
||||
|
||||
columns = [];
|
||||
for (c in cols) {
|
||||
arrayAppend(columns, {
|
||||
"Field": c.Field,
|
||||
"Type": c.Type,
|
||||
"Null": c.Null,
|
||||
"Key": c.Key,
|
||||
"Default": isNull(c.Default) ? "NULL" : c.Default
|
||||
});
|
||||
}
|
||||
|
||||
// Get sample data
|
||||
sampleData = queryExecute("SELECT * FROM #tableName# LIMIT 5", {}, { datasource: "payfrit" });
|
||||
|
||||
samples = [];
|
||||
for (row in sampleData) {
|
||||
arrayAppend(samples, row);
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"TABLE": tableName,
|
||||
"COLUMNS": columns,
|
||||
"SAMPLE_DATA": sampleData,
|
||||
"ROW_COUNT": sampleData.recordCount
|
||||
}));
|
||||
} catch (any e) {
|
||||
writeOutput(serializeJSON({
|
||||
"OK": false,
|
||||
"ERROR": e.message
|
||||
}));
|
||||
}
|
||||
</cfscript>
|
||||
30
api/admin/listTables.cfm
Normal file
30
api/admin/listTables.cfm
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
try {
|
||||
// List all tables
|
||||
tables = queryExecute("SHOW TABLES", {}, { datasource: "payfrit" });
|
||||
|
||||
tableList = [];
|
||||
for (t in tables) {
|
||||
// Get the first column value (table name)
|
||||
for (col in t) {
|
||||
arrayAppend(tableList, t[col]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"TABLES": tableList,
|
||||
"COUNT": arrayLen(tableList)
|
||||
}));
|
||||
} catch (any e) {
|
||||
writeOutput(serializeJSON({
|
||||
"OK": false,
|
||||
"ERROR": e.message
|
||||
}));
|
||||
}
|
||||
</cfscript>
|
||||
179
api/admin/randomizePrices.cfm
Normal file
179
api/admin/randomizePrices.cfm
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
businessId = 27; // Big Dean's
|
||||
|
||||
// Categories are items with ItemParentItemID=0 AND ItemIsCollapsible=0
|
||||
// Modifier templates are items with ItemParentItemID=0 AND ItemIsCollapsible=1
|
||||
// Menu items are children of categories
|
||||
// Modifiers are children of menu items or modifier templates
|
||||
|
||||
// Get category IDs (NOT modifier templates)
|
||||
categoryIds = queryExecute("
|
||||
SELECT ItemID
|
||||
FROM Items
|
||||
WHERE ItemBusinessID = :bizId
|
||||
AND ItemParentItemID = 0
|
||||
AND ItemIsCollapsible = 0
|
||||
", { bizId: businessId }, { datasource: "payfrit" });
|
||||
|
||||
catIdList = "";
|
||||
for (cat in categoryIds) {
|
||||
catIdList = listAppend(catIdList, cat.ItemID);
|
||||
}
|
||||
|
||||
// Now get actual menu items (direct children of categories)
|
||||
// Exclude items that are template options (their parent is a collapsible modifier group)
|
||||
items = queryExecute("
|
||||
SELECT i.ItemID, i.ItemName
|
||||
FROM Items i
|
||||
WHERE i.ItemBusinessID = :bizId
|
||||
AND i.ItemParentItemID IN (#catIdList#)
|
||||
AND i.ItemIsCollapsible = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ItemTemplateLinks tl WHERE tl.TemplateItemID = i.ItemID
|
||||
)
|
||||
", { bizId: businessId }, { datasource: "payfrit" });
|
||||
|
||||
updated = [];
|
||||
|
||||
for (item in items) {
|
||||
itemName = lcase(item.ItemName);
|
||||
newPrice = 0;
|
||||
|
||||
// Drinks - $3-6
|
||||
if (findNoCase("beer", itemName) || findNoCase("ale", itemName) || findNoCase("lager", itemName) || findNoCase("ipa", itemName) || findNoCase("stout", itemName)) {
|
||||
newPrice = randRange(5, 9) - 0.01; // $4.99 - $8.99
|
||||
}
|
||||
else if (findNoCase("wine", itemName) || findNoCase("sangria", itemName)) {
|
||||
newPrice = randRange(8, 14) - 0.01; // $7.99 - $13.99
|
||||
}
|
||||
else if (findNoCase("soda", itemName) || findNoCase("coke", itemName) || findNoCase("pepsi", itemName) || findNoCase("sprite", itemName) || findNoCase("lemonade", itemName) || findNoCase("tea", itemName) || findNoCase("coffee", itemName)) {
|
||||
newPrice = randRange(3, 5) - 0.01; // $2.99 - $4.99
|
||||
}
|
||||
else if (findNoCase("margarita", itemName) || findNoCase("cocktail", itemName) || findNoCase("martini", itemName) || findNoCase("mojito", itemName)) {
|
||||
newPrice = randRange(10, 15) - 0.01; // $9.99 - $14.99
|
||||
}
|
||||
// Appetizers / Starters - $8-14
|
||||
else if (findNoCase("fries", itemName) || findNoCase("chips", itemName) || findNoCase("nachos", itemName) || findNoCase("wings", itemName) || findNoCase("calamari", itemName) || findNoCase("appetizer", itemName) || findNoCase("starter", itemName)) {
|
||||
newPrice = randRange(8, 14) - 0.01; // $7.99 - $13.99
|
||||
}
|
||||
// Salads - $10-16
|
||||
else if (findNoCase("salad", itemName)) {
|
||||
newPrice = randRange(11, 17) - 0.01; // $10.99 - $16.99
|
||||
}
|
||||
// Soups - $6-10
|
||||
else if (findNoCase("soup", itemName) || findNoCase("chowder", itemName)) {
|
||||
newPrice = randRange(7, 11) - 0.01; // $6.99 - $10.99
|
||||
}
|
||||
// Burgers - $14-19
|
||||
else if (findNoCase("burger", itemName)) {
|
||||
newPrice = randRange(15, 20) - 0.01; // $14.99 - $19.99
|
||||
}
|
||||
// Sandwiches - $12-17
|
||||
else if (findNoCase("sandwich", itemName) || findNoCase("wrap", itemName) || findNoCase("club", itemName) || findNoCase("blt", itemName)) {
|
||||
newPrice = randRange(13, 18) - 0.01; // $12.99 - $17.99
|
||||
}
|
||||
// Fish & Seafood - $16-28
|
||||
else if (findNoCase("fish", itemName) || findNoCase("salmon", itemName) || findNoCase("shrimp", itemName) || findNoCase("lobster", itemName) || findNoCase("crab", itemName) || findNoCase("scallop", itemName) || findNoCase("tuna", itemName) || findNoCase("halibut", itemName) || findNoCase("cod", itemName)) {
|
||||
newPrice = randRange(17, 29) - 0.01; // $16.99 - $28.99
|
||||
}
|
||||
// Tacos - $13-18
|
||||
else if (findNoCase("taco", itemName)) {
|
||||
newPrice = randRange(14, 19) - 0.01; // $13.99 - $18.99
|
||||
}
|
||||
// Kids meals - $8-12
|
||||
else if (findNoCase("kid", itemName) || findNoCase("child", itemName)) {
|
||||
newPrice = randRange(9, 13) - 0.01; // $8.99 - $12.99
|
||||
}
|
||||
// Desserts - $7-12
|
||||
else if (findNoCase("dessert", itemName) || findNoCase("cake", itemName) || findNoCase("pie", itemName) || findNoCase("sundae", itemName) || findNoCase("brownie", itemName) || findNoCase("cheesecake", itemName) || findNoCase("ice cream", itemName)) {
|
||||
newPrice = randRange(8, 13) - 0.01; // $7.99 - $12.99
|
||||
}
|
||||
// Default entrees - $14-22
|
||||
else {
|
||||
newPrice = randRange(15, 23) - 0.01; // $14.99 - $22.99
|
||||
}
|
||||
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemPrice = :price
|
||||
WHERE ItemID = :itemId
|
||||
", { price: newPrice, itemId: item.ItemID }, { datasource: "payfrit" });
|
||||
|
||||
arrayAppend(updated, {
|
||||
"ItemID": item.ItemID,
|
||||
"ItemName": item.ItemName,
|
||||
"NewPrice": newPrice
|
||||
});
|
||||
}
|
||||
|
||||
// Update modifier prices (children of menu items, NOT direct children of categories)
|
||||
// Modifiers are items whose parent is NOT a category (i.e., parent is a menu item or modifier group)
|
||||
modifiers = queryExecute("
|
||||
SELECT ItemID, ItemName
|
||||
FROM Items
|
||||
WHERE ItemBusinessID = :bizId
|
||||
AND ItemParentItemID > 0
|
||||
AND ItemParentItemID NOT IN (#catIdList#)
|
||||
", { bizId: businessId }, { datasource: "payfrit" });
|
||||
|
||||
for (mod in modifiers) {
|
||||
modName = lcase(mod.ItemName);
|
||||
modPrice = 0;
|
||||
|
||||
// Proteins are expensive add-ons
|
||||
if (findNoCase("bacon", modName) || findNoCase("avocado", modName) || findNoCase("guac", modName)) {
|
||||
modPrice = randRange(2, 4) - 0.01; // $1.99 - $3.99
|
||||
}
|
||||
else if (findNoCase("chicken", modName) || findNoCase("shrimp", modName) || findNoCase("steak", modName) || findNoCase("salmon", modName)) {
|
||||
modPrice = randRange(4, 7) - 0.01; // $3.99 - $6.99
|
||||
}
|
||||
else if (findNoCase("cheese", modName) || findNoCase("egg", modName)) {
|
||||
modPrice = randRange(1, 3) - 0.01; // $0.99 - $2.99
|
||||
}
|
||||
// Size upgrades
|
||||
else if (findNoCase("large", modName) || findNoCase("extra", modName) || findNoCase("double", modName)) {
|
||||
modPrice = randRange(2, 4) - 0.01; // $1.99 - $3.99
|
||||
}
|
||||
// Most modifiers (toppings, sauces, etc.) are free or cheap
|
||||
else {
|
||||
// 70% free, 30% small charge
|
||||
if (randRange(1, 10) <= 7) {
|
||||
modPrice = 0;
|
||||
} else {
|
||||
modPrice = randRange(1, 2) - 0.01; // $0.99 - $1.99
|
||||
}
|
||||
}
|
||||
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemPrice = :price
|
||||
WHERE ItemID = :itemId
|
||||
", { price: modPrice, itemId: mod.ItemID }, { datasource: "payfrit" });
|
||||
}
|
||||
|
||||
// Reset category prices to $0 (shouldn't have prices for reporting)
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemPrice = 0
|
||||
WHERE ItemBusinessID = :bizId
|
||||
AND ItemParentItemID = 0
|
||||
", { bizId: businessId }, { datasource: "payfrit" });
|
||||
|
||||
// Reset modifier group prices to $0 (only options have prices)
|
||||
queryExecute("
|
||||
UPDATE Items
|
||||
SET ItemPrice = 0
|
||||
WHERE ItemBusinessID = :bizId
|
||||
AND ItemIsCollapsible = 1
|
||||
", { bizId: businessId }, { datasource: "payfrit" });
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"MESSAGE": "Updated prices for #arrayLen(updated)# menu items and #modifiers.recordCount# modifiers. Reset category and group prices to $0.",
|
||||
"ITEMS": updated
|
||||
}));
|
||||
</cfscript>
|
||||
149
api/admin/setupBigDeansInfo.cfm
Normal file
149
api/admin/setupBigDeansInfo.cfm
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
businessId = 27;
|
||||
response = { "OK": false };
|
||||
|
||||
try {
|
||||
// Big Dean's actual info
|
||||
// Address: 1615 Ocean Front Walk, Santa Monica, CA 90401
|
||||
// Phone: (310) 393-2666
|
||||
// Hours: Mon-Thu: 11am-10pm, Fri-Sat: 11am-11pm, Sun: 11am-10pm
|
||||
|
||||
// Get California StateID
|
||||
qState = queryExecute("SELECT tt_StateID FROM tt_States WHERE tt_StateAbbreviation = 'CA' LIMIT 1");
|
||||
stateId = qState.recordCount > 0 ? qState.tt_StateID : 5; // Default to 5 if not found
|
||||
|
||||
// Check if Big Dean's already has an address
|
||||
existingAddr = queryExecute("
|
||||
SELECT AddressID FROM Addresses
|
||||
WHERE AddressBusinessID = :bizId AND AddressUserID = 0
|
||||
", { bizId: businessId });
|
||||
|
||||
if (existingAddr.recordCount == 0) {
|
||||
// Insert new address
|
||||
queryExecute("
|
||||
INSERT INTO Addresses (AddressUserID, AddressBusinessID, AddressTypeID, AddressLine1, AddressCity, AddressStateID, AddressZIPCode, AddressIsDeleted, AddressAddedOn)
|
||||
VALUES (0, :bizId, '2', :line1, :city, :stateId, :zip, 0, NOW())
|
||||
", {
|
||||
bizId: businessId,
|
||||
line1: "1615 Ocean Front Walk",
|
||||
city: "Santa Monica",
|
||||
stateId: stateId,
|
||||
zip: "90401"
|
||||
});
|
||||
response["ADDRESS_ACTION"] = "inserted";
|
||||
} else {
|
||||
// Update existing address
|
||||
queryExecute("
|
||||
UPDATE Addresses
|
||||
SET AddressLine1 = :line1, AddressCity = :city, AddressStateID = :stateId, AddressZIPCode = :zip
|
||||
WHERE AddressBusinessID = :bizId AND AddressUserID = 0
|
||||
", {
|
||||
bizId: businessId,
|
||||
line1: "1615 Ocean Front Walk",
|
||||
city: "Santa Monica",
|
||||
stateId: stateId,
|
||||
zip: "90401"
|
||||
});
|
||||
response["ADDRESS_ACTION"] = "updated";
|
||||
}
|
||||
|
||||
// Check existing hours for this business
|
||||
existingHours = queryExecute("
|
||||
SELECT COUNT(*) as cnt FROM Hours WHERE HoursBusinessID = :bizId
|
||||
", { bizId: businessId });
|
||||
|
||||
if (existingHours.cnt == 0) {
|
||||
// Insert hours for each day
|
||||
// Days: 1=Sunday, 2=Monday, 3=Tuesday, 4=Wednesday, 5=Thursday, 6=Friday, 7=Saturday
|
||||
// Mon-Thu: 11am-10pm (days 2-5)
|
||||
// Fri-Sat: 11am-11pm (days 6-7)
|
||||
// Sun: 11am-10pm (day 1)
|
||||
|
||||
// Sunday (1): 11am-10pm
|
||||
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 1, '11:00:00', '22:00:00')", { bizId: businessId });
|
||||
|
||||
// Monday (2): 11am-10pm
|
||||
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 2, '11:00:00', '22:00:00')", { bizId: businessId });
|
||||
|
||||
// Tuesday (3): 11am-10pm
|
||||
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 3, '11:00:00', '22:00:00')", { bizId: businessId });
|
||||
|
||||
// Wednesday (4): 11am-10pm
|
||||
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 4, '11:00:00', '22:00:00')", { bizId: businessId });
|
||||
|
||||
// Thursday (5): 11am-10pm
|
||||
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 5, '11:00:00', '22:00:00')", { bizId: businessId });
|
||||
|
||||
// Friday (6): 11am-11pm
|
||||
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 6, '11:00:00', '23:00:00')", { bizId: businessId });
|
||||
|
||||
// Saturday (7): 11am-11pm
|
||||
queryExecute("INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime) VALUES (:bizId, 7, '11:00:00', '23:00:00')", { bizId: businessId });
|
||||
|
||||
response["HOURS_ACTION"] = "inserted 7 days";
|
||||
} else {
|
||||
// Update existing hours
|
||||
// Mon-Thu: 11am-10pm
|
||||
queryExecute("UPDATE Hours SET HoursOpenTime = '11:00:00', HoursClosingTime = '22:00:00' WHERE HoursBusinessID = :bizId AND HoursDayID IN (1, 2, 3, 4, 5)", { bizId: businessId });
|
||||
// Fri-Sat: 11am-11pm
|
||||
queryExecute("UPDATE Hours SET HoursOpenTime = '11:00:00', HoursClosingTime = '23:00:00' WHERE HoursBusinessID = :bizId AND HoursDayID IN (6, 7)", { bizId: businessId });
|
||||
response["HOURS_ACTION"] = "updated";
|
||||
}
|
||||
|
||||
// Update phone on Businesses table (if column exists)
|
||||
try {
|
||||
queryExecute("UPDATE Businesses SET BusinessPhone = :phone WHERE BusinessID = :bizId", {
|
||||
phone: "(310) 393-2666",
|
||||
bizId: businessId
|
||||
});
|
||||
response["PHONE_ACTION"] = "updated";
|
||||
} catch (any e) {
|
||||
response["PHONE_ACTION"] = "column may not exist: " & e.message;
|
||||
}
|
||||
|
||||
// Verify the data
|
||||
address = queryExecute("
|
||||
SELECT a.*, s.tt_StateAbbreviation
|
||||
FROM Addresses a
|
||||
LEFT JOIN tt_States s ON s.tt_StateID = a.AddressStateID
|
||||
WHERE a.AddressBusinessID = :bizId AND a.AddressUserID = 0
|
||||
", { bizId: businessId });
|
||||
|
||||
hours = queryExecute("
|
||||
SELECT h.*, d.tt_DayName
|
||||
FROM Hours h
|
||||
JOIN tt_Days d ON d.tt_DayID = h.HoursDayID
|
||||
WHERE h.HoursBusinessID = :bizId
|
||||
ORDER BY h.HoursDayID
|
||||
", { bizId: businessId });
|
||||
|
||||
response["OK"] = true;
|
||||
response["BUSINESS_ID"] = businessId;
|
||||
response["ADDRESS"] = address.recordCount > 0 ? {
|
||||
"line1": address.AddressLine1,
|
||||
"city": address.AddressCity,
|
||||
"state": address.tt_StateAbbreviation,
|
||||
"zip": address.AddressZIPCode
|
||||
} : "not found";
|
||||
|
||||
hoursArr = [];
|
||||
for (h in hours) {
|
||||
arrayAppend(hoursArr, {
|
||||
"day": h.tt_DayName,
|
||||
"open": timeFormat(h.HoursOpenTime, "h:mm tt"),
|
||||
"close": timeFormat(h.HoursClosingTime, "h:mm tt")
|
||||
});
|
||||
}
|
||||
response["HOURS"] = hoursArr;
|
||||
|
||||
} catch (any e) {
|
||||
response["ERROR"] = e.message;
|
||||
response["DETAIL"] = e.detail;
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
<cfscript>
|
||||
// Switch all beacons from one business to another
|
||||
fromBiz = 27; // Big Dean's
|
||||
toBiz = 17; // In-N-Out
|
||||
fromBiz = 17; // In-N-Out
|
||||
toBiz = 27; // Big Dean's
|
||||
|
||||
queryExecute("
|
||||
UPDATE lt_Beacon_Businesses_ServicePoints
|
||||
|
|
|
|||
52
api/admin/updateBeaconMapping.cfm
Normal file
52
api/admin/updateBeaconMapping.cfm
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
// Update Beacon 2 to point to In-N-Out (BusinessID 17)
|
||||
beaconId = 2;
|
||||
newBusinessId = 17;
|
||||
|
||||
queryExecute("
|
||||
UPDATE lt_Beacon_Businesses_ServicePoints
|
||||
SET BusinessID = :newBizId
|
||||
WHERE BeaconID = :beaconId
|
||||
", { newBizId: newBusinessId, beaconId: beaconId }, { datasource: "payfrit" });
|
||||
|
||||
// Get current state
|
||||
q = queryExecute("
|
||||
SELECT
|
||||
b.BeaconID,
|
||||
b.BeaconUUID,
|
||||
b.BeaconName,
|
||||
lt.BusinessID,
|
||||
lt.ServicePointID,
|
||||
biz.BusinessName,
|
||||
sp.ServicePointName
|
||||
FROM Beacons b
|
||||
LEFT JOIN lt_Beacon_Businesses_ServicePoints lt ON lt.BeaconID = b.BeaconID
|
||||
LEFT JOIN Businesses biz ON biz.BusinessID = lt.BusinessID
|
||||
LEFT JOIN ServicePoints sp ON sp.ServicePointID = lt.ServicePointID
|
||||
WHERE b.BeaconIsActive = 1
|
||||
ORDER BY b.BeaconID
|
||||
", {}, { datasource: "payfrit" });
|
||||
|
||||
rows = [];
|
||||
for (row in q) {
|
||||
arrayAppend(rows, {
|
||||
"BeaconID": row.BeaconID,
|
||||
"BeaconUUID": row.BeaconUUID,
|
||||
"BeaconName": row.BeaconName ?: "",
|
||||
"BusinessID": row.BusinessID ?: 0,
|
||||
"BusinessName": row.BusinessName ?: "",
|
||||
"ServicePointID": row.ServicePointID ?: 0,
|
||||
"ServicePointName": row.ServicePointName ?: ""
|
||||
});
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"MESSAGE": "Updated beacon #beaconId# to BusinessID #newBusinessId#",
|
||||
"BEACONS": rows
|
||||
}));
|
||||
</cfscript>
|
||||
85
api/admin/updateBigDeans.cfm
Normal file
85
api/admin/updateBigDeans.cfm
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
// Update Big Dean's business info
|
||||
businessId = 27;
|
||||
|
||||
// Big Dean's actual address and hours
|
||||
address = "1615 Ocean Front Walk, Santa Monica, CA 90401";
|
||||
phone = "(310) 393-2666";
|
||||
hours = "Mon-Thu: 11am-10pm, Fri-Sat: 11am-11pm, Sun: 11am-10pm";
|
||||
|
||||
try {
|
||||
// First get column names from INFORMATION_SCHEMA
|
||||
cols = queryExecute("
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'payfrit' AND TABLE_NAME = 'Businesses'
|
||||
ORDER BY ORDINAL_POSITION
|
||||
");
|
||||
|
||||
colNames = [];
|
||||
for (c in cols) {
|
||||
arrayAppend(colNames, c.COLUMN_NAME);
|
||||
}
|
||||
|
||||
// Check if we have the columns we need
|
||||
hasAddress = arrayFindNoCase(colNames, "BusinessAddress") > 0;
|
||||
hasPhone = arrayFindNoCase(colNames, "BusinessPhone") > 0;
|
||||
hasHours = arrayFindNoCase(colNames, "BusinessHours") > 0;
|
||||
|
||||
// Add columns if missing
|
||||
if (!hasAddress) {
|
||||
queryExecute("ALTER TABLE Businesses ADD COLUMN BusinessAddress VARCHAR(255)");
|
||||
}
|
||||
if (!hasPhone) {
|
||||
queryExecute("ALTER TABLE Businesses ADD COLUMN BusinessPhone VARCHAR(50)");
|
||||
}
|
||||
if (!hasHours) {
|
||||
queryExecute("ALTER TABLE Businesses ADD COLUMN BusinessHours VARCHAR(255)");
|
||||
}
|
||||
|
||||
// Update with new info
|
||||
queryExecute("
|
||||
UPDATE Businesses
|
||||
SET BusinessAddress = :address,
|
||||
BusinessPhone = :phone,
|
||||
BusinessHours = :hours
|
||||
WHERE BusinessID = :bizId
|
||||
", {
|
||||
address: address,
|
||||
phone: phone,
|
||||
hours: hours,
|
||||
bizId: businessId
|
||||
});
|
||||
|
||||
// Get updated record
|
||||
updated = queryExecute("
|
||||
SELECT BusinessID, BusinessName, BusinessAddress, BusinessPhone, BusinessHours
|
||||
FROM Businesses
|
||||
WHERE BusinessID = :bizId
|
||||
", { bizId: businessId });
|
||||
|
||||
writeOutput(serializeJSON({
|
||||
"OK": true,
|
||||
"MESSAGE": "Updated Big Dean's info",
|
||||
"COLUMNS_EXISTED": { "address": hasAddress, "phone": hasPhone, "hours": hasHours },
|
||||
"BUSINESS": {
|
||||
"BusinessID": updated.BusinessID,
|
||||
"BusinessName": updated.BusinessName,
|
||||
"BusinessAddress": updated.BusinessAddress,
|
||||
"BusinessPhone": updated.BusinessPhone,
|
||||
"BusinessHours": updated.BusinessHours
|
||||
}
|
||||
}));
|
||||
|
||||
} catch (any e) {
|
||||
writeOutput(serializeJSON({
|
||||
"OK": false,
|
||||
"ERROR": e.message,
|
||||
"DETAIL": e.detail
|
||||
}));
|
||||
}
|
||||
</cfscript>
|
||||
|
|
@ -32,11 +32,12 @@ try {
|
|||
abort;
|
||||
}
|
||||
|
||||
// Get business details (only columns that exist)
|
||||
// Get business details
|
||||
q = queryExecute("
|
||||
SELECT
|
||||
BusinessID,
|
||||
BusinessName,
|
||||
BusinessPhone,
|
||||
BusinessStripeAccountID,
|
||||
BusinessStripeOnboardingComplete
|
||||
FROM Businesses
|
||||
|
|
@ -49,10 +50,74 @@ try {
|
|||
abort;
|
||||
}
|
||||
|
||||
// Get address from Addresses table
|
||||
qAddr = queryExecute("
|
||||
SELECT a.AddressLine1, a.AddressLine2, a.AddressCity, a.AddressZIPCode, s.tt_StateAbbreviation
|
||||
FROM Addresses a
|
||||
LEFT JOIN tt_States s ON s.tt_StateID = a.AddressStateID
|
||||
WHERE a.AddressBusinessID = :businessID AND a.AddressUserID = 0 AND a.AddressIsDeleted = 0
|
||||
LIMIT 1
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
||||
addressStr = "";
|
||||
if (qAddr.recordCount > 0) {
|
||||
addressParts = [];
|
||||
if (len(qAddr.AddressLine1)) arrayAppend(addressParts, qAddr.AddressLine1);
|
||||
if (len(qAddr.AddressLine2)) arrayAppend(addressParts, qAddr.AddressLine2);
|
||||
cityStateZip = [];
|
||||
if (len(qAddr.AddressCity)) arrayAppend(cityStateZip, qAddr.AddressCity);
|
||||
if (len(qAddr.tt_StateAbbreviation)) arrayAppend(cityStateZip, qAddr.tt_StateAbbreviation);
|
||||
if (len(qAddr.AddressZIPCode)) arrayAppend(cityStateZip, qAddr.AddressZIPCode);
|
||||
if (arrayLen(cityStateZip) > 0) arrayAppend(addressParts, arrayToList(cityStateZip, ", "));
|
||||
addressStr = arrayToList(addressParts, ", ");
|
||||
}
|
||||
|
||||
// Get hours from Hours table
|
||||
qHours = queryExecute("
|
||||
SELECT h.HoursDayID, h.HoursOpenTime, h.HoursClosingTime, d.tt_DayAbbrev
|
||||
FROM Hours h
|
||||
JOIN tt_Days d ON d.tt_DayID = h.HoursDayID
|
||||
WHERE h.HoursBusinessID = :businessID
|
||||
ORDER BY h.HoursDayID
|
||||
", { businessID: businessID }, { datasource: "payfrit" });
|
||||
|
||||
hoursArr = [];
|
||||
hoursStr = "";
|
||||
if (qHours.recordCount > 0) {
|
||||
for (h in qHours) {
|
||||
arrayAppend(hoursArr, {
|
||||
"day": h.tt_DayAbbrev,
|
||||
"dayId": h.HoursDayID,
|
||||
"open": timeFormat(h.HoursOpenTime, "h:mm tt"),
|
||||
"close": timeFormat(h.HoursClosingTime, "h:mm tt")
|
||||
});
|
||||
}
|
||||
// Build readable hours string (group similar days)
|
||||
// Mon-Thu: 11am-10pm, Fri-Sat: 11am-11pm, Sun: 11am-10pm
|
||||
hourGroups = {};
|
||||
for (h in hoursArr) {
|
||||
key = h.open & "-" & h.close;
|
||||
if (!structKeyExists(hourGroups, key)) {
|
||||
hourGroups[key] = [];
|
||||
}
|
||||
arrayAppend(hourGroups[key], h.day);
|
||||
}
|
||||
hourStrParts = [];
|
||||
for (key in hourGroups) {
|
||||
days = hourGroups[key];
|
||||
arrayAppend(hourStrParts, arrayToList(days, ",") & ": " & key);
|
||||
}
|
||||
hoursStr = arrayToList(hourStrParts, ", ");
|
||||
}
|
||||
|
||||
// Build business object
|
||||
business = {
|
||||
"BusinessID": q.BusinessID,
|
||||
"BusinessName": q.BusinessName,
|
||||
"BusinessAddress": addressStr,
|
||||
"BusinessPhone": q.BusinessPhone,
|
||||
"BusinessHours": hoursStr,
|
||||
"BusinessHoursDetail": hoursArr,
|
||||
"StripeConnected": (len(q.BusinessStripeAccountID) > 0 && q.BusinessStripeOnboardingComplete == 1)
|
||||
};
|
||||
|
||||
|
|
|
|||
172
api/orders/getDetail.cfm
Normal file
172
api/orders/getDetail.cfm
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
<cfheader name="Cache-Control" value="no-store">
|
||||
|
||||
<cfscript>
|
||||
/**
|
||||
* Get Order Detail
|
||||
* Returns full order info including line items and customer details
|
||||
*
|
||||
* GET: ?OrderID=123
|
||||
* POST: { OrderID: 123 }
|
||||
*/
|
||||
|
||||
response = { "OK": false };
|
||||
|
||||
try {
|
||||
// Get OrderID from request
|
||||
orderID = 0;
|
||||
|
||||
// Check URL params
|
||||
if (structKeyExists(url, "OrderID")) {
|
||||
orderID = val(url.OrderID);
|
||||
}
|
||||
|
||||
// Check POST body
|
||||
if (orderID == 0) {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
if (len(requestBody)) {
|
||||
requestData = deserializeJSON(requestBody);
|
||||
if (structKeyExists(requestData, "OrderID")) {
|
||||
orderID = val(requestData.OrderID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (orderID == 0) {
|
||||
response["ERROR"] = "missing_order_id";
|
||||
response["MESSAGE"] = "OrderID is required";
|
||||
writeOutput(serializeJSON(response));
|
||||
abort;
|
||||
}
|
||||
|
||||
// Get order details
|
||||
qOrder = queryExecute("
|
||||
SELECT
|
||||
o.OrderID,
|
||||
o.OrderBusinessID,
|
||||
o.OrderUserID,
|
||||
o.OrderServicePointID,
|
||||
o.OrderStatusID,
|
||||
o.OrderRemarks,
|
||||
o.OrderAddedOn,
|
||||
o.OrderLastEditedOn,
|
||||
o.OrderTipAmount,
|
||||
u.UserFirstName,
|
||||
u.UserLastName,
|
||||
u.UserContactNumber,
|
||||
u.UserEmailAddress,
|
||||
sp.ServicePointName,
|
||||
sp.ServicePointTypeID
|
||||
FROM Orders o
|
||||
LEFT JOIN Users u ON u.UserID = o.OrderUserID
|
||||
LEFT JOIN ServicePoints sp ON sp.ServicePointID = o.OrderServicePointID
|
||||
WHERE o.OrderID = :orderID
|
||||
", { orderID: orderID });
|
||||
|
||||
if (qOrder.recordCount == 0) {
|
||||
response["ERROR"] = "order_not_found";
|
||||
response["MESSAGE"] = "Order not found";
|
||||
writeOutput(serializeJSON(response));
|
||||
abort;
|
||||
}
|
||||
|
||||
// Get line items
|
||||
qItems = queryExecute("
|
||||
SELECT
|
||||
oli.OrderLineItemID,
|
||||
oli.OrderLineItemItemID,
|
||||
oli.OrderLineItemParentOrderLineItemID,
|
||||
oli.OrderLineItemQuantity,
|
||||
oli.OrderLineItemPrice,
|
||||
oli.OrderLineItemRemark,
|
||||
i.ItemName,
|
||||
i.ItemPrice
|
||||
FROM OrderLineItems oli
|
||||
INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID
|
||||
WHERE oli.OrderLineItemOrderID = :orderID
|
||||
ORDER BY oli.OrderLineItemID
|
||||
", { orderID: orderID });
|
||||
|
||||
// Build line items array with parent-child structure
|
||||
lineItems = [];
|
||||
itemsById = {};
|
||||
|
||||
// First pass: create all items
|
||||
for (row in qItems) {
|
||||
item = {
|
||||
"LineItemID": row.OrderLineItemID,
|
||||
"ItemID": row.OrderLineItemItemID,
|
||||
"ParentLineItemID": row.OrderLineItemParentOrderLineItemID,
|
||||
"ItemName": row.ItemName,
|
||||
"Quantity": row.OrderLineItemQuantity,
|
||||
"UnitPrice": row.OrderLineItemPrice,
|
||||
"Remarks": row.OrderLineItemRemark,
|
||||
"Modifiers": []
|
||||
};
|
||||
itemsById[row.OrderLineItemID] = item;
|
||||
}
|
||||
|
||||
// Second pass: build hierarchy
|
||||
for (row in qItems) {
|
||||
item = itemsById[row.OrderLineItemID];
|
||||
parentID = row.OrderLineItemParentOrderLineItemID;
|
||||
|
||||
if (parentID > 0 && structKeyExists(itemsById, parentID)) {
|
||||
// This is a modifier - add to parent
|
||||
arrayAppend(itemsById[parentID].Modifiers, item);
|
||||
} else {
|
||||
// This is a top-level item
|
||||
arrayAppend(lineItems, item);
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
order = {
|
||||
"OrderID": qOrder.OrderID,
|
||||
"BusinessID": qOrder.OrderBusinessID,
|
||||
"Status": qOrder.OrderStatusID,
|
||||
"StatusText": getStatusText(qOrder.OrderStatusID),
|
||||
"Tip": qOrder.OrderTipAmount,
|
||||
"Notes": qOrder.OrderRemarks,
|
||||
"CreatedOn": dateTimeFormat(qOrder.OrderAddedOn, "yyyy-mm-dd HH:nn:ss"),
|
||||
"UpdatedOn": len(qOrder.OrderLastEditedOn) ? dateTimeFormat(qOrder.OrderLastEditedOn, "yyyy-mm-dd HH:nn:ss") : "",
|
||||
"Customer": {
|
||||
"UserID": qOrder.OrderUserID,
|
||||
"FirstName": qOrder.UserFirstName,
|
||||
"LastName": qOrder.UserLastName,
|
||||
"Phone": qOrder.UserContactNumber,
|
||||
"Email": qOrder.UserEmailAddress
|
||||
},
|
||||
"ServicePoint": {
|
||||
"ServicePointID": qOrder.OrderServicePointID,
|
||||
"Name": qOrder.ServicePointName,
|
||||
"TypeID": qOrder.ServicePointTypeID
|
||||
},
|
||||
"LineItems": lineItems
|
||||
};
|
||||
|
||||
response["OK"] = true;
|
||||
response["ORDER"] = order;
|
||||
|
||||
} catch (any e) {
|
||||
response["ERROR"] = "server_error";
|
||||
response["MESSAGE"] = e.message;
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
|
||||
// Helper function
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 0: return "Cart";
|
||||
case 1: return "Submitted";
|
||||
case 2: return "In Progress";
|
||||
case 3: return "Ready";
|
||||
case 4: return "Completed";
|
||||
case 5: return "Cancelled";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
</cfscript>
|
||||
|
|
@ -58,7 +58,8 @@
|
|||
sp.ServicePointTypeID,
|
||||
u.UserID as CustomerUserID,
|
||||
u.UserFirstName,
|
||||
u.UserLastName
|
||||
u.UserLastName,
|
||||
u.UserContactNumber
|
||||
FROM Tasks t
|
||||
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
|
||||
LEFT JOIN Orders o ON o.OrderID = t.TaskOrderID
|
||||
|
|
@ -98,11 +99,27 @@
|
|||
"CustomerUserID": qTask.CustomerUserID ?: 0,
|
||||
"CustomerFirstName": qTask.UserFirstName ?: "",
|
||||
"CustomerLastName": qTask.UserLastName ?: "",
|
||||
"CustomerPhone": "",
|
||||
"CustomerPhone": qTask.UserContactNumber ?: "",
|
||||
"CustomerPhotoUrl": qTask.CustomerUserID GT 0 ? "https://biz.payfrit.com/uploads/users/" & qTask.CustomerUserID & ".jpg" : "",
|
||||
"BeaconUUID": "",
|
||||
"LineItems": []
|
||||
}>
|
||||
|
||||
<!--- Get beacon UUID for the service point (for auto-completion on Works app) --->
|
||||
<cfif val(qTask.OrderServicePointID) GT 0>
|
||||
<cfset qBeacon = queryExecute("
|
||||
SELECT b.BeaconUUID
|
||||
FROM lt_Beacon_Businesses_ServicePoints lt
|
||||
INNER JOIN Beacons b ON b.BeaconID = lt.BeaconID
|
||||
WHERE lt.ServicePointID = ?
|
||||
AND b.BeaconIsActive = 1
|
||||
LIMIT 1
|
||||
", [ { value = qTask.OrderServicePointID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
|
||||
<cfif qBeacon.recordCount GT 0>
|
||||
<cfset result.BeaconUUID = qBeacon.BeaconUUID>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<!--- Get order line items if there's an order --->
|
||||
<cfif qTask.OrderID GT 0>
|
||||
<cfset qLineItems = queryExecute("
|
||||
|
|
@ -134,7 +151,7 @@
|
|||
"ItemPrice": qLineItems.OrderLineItemPrice,
|
||||
"Quantity": qLineItems.OrderLineItemQuantity,
|
||||
"Remark": qLineItems.OrderLineItemRemark,
|
||||
"IsModifier": qLineItems.ItemParentItemID GT 0
|
||||
"IsModifier": qLineItems.OrderLineItemParentOrderLineItemID GT 0
|
||||
})>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
|
|
|||
|
|
@ -717,7 +717,7 @@
|
|||
<!-- Toolbar -->
|
||||
<div class="builder-toolbar">
|
||||
<div class="toolbar-group">
|
||||
<a href="/portal/#menu" class="toolbar-btn" title="Back to Portal" style="text-decoration: none;">
|
||||
<a href="/portal/index.html#menu" class="toolbar-btn" title="Back to Portal" style="text-decoration: none;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
|
|
@ -2429,23 +2429,32 @@
|
|||
// Load menu from API
|
||||
async loadMenu() {
|
||||
try {
|
||||
console.log('[MenuBuilder] Loading menu for BusinessID:', this.config.businessId);
|
||||
console.log('[MenuBuilder] API URL:', `${this.config.apiBaseUrl}/menu/getForBuilder.cfm`);
|
||||
const response = await fetch(`${this.config.apiBaseUrl}/menu/getForBuilder.cfm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ BusinessID: this.config.businessId })
|
||||
});
|
||||
console.log('[MenuBuilder] Response status:', response.status);
|
||||
const data = await response.json();
|
||||
console.log('[MenuBuilder] Response data:', data);
|
||||
console.log('[MenuBuilder] CATEGORY_COUNT:', data.CATEGORY_COUNT);
|
||||
console.log('[MenuBuilder] ITEM_COUNT:', data.ITEM_COUNT);
|
||||
if (data.OK && data.MENU) {
|
||||
this.menu = data.MENU;
|
||||
console.log('[MenuBuilder] Menu categories:', this.menu.categories?.length || 0);
|
||||
// Store templates from API
|
||||
if (data.TEMPLATES) {
|
||||
this.templates = data.TEMPLATES;
|
||||
this.renderTemplateLibrary();
|
||||
}
|
||||
this.render();
|
||||
} else {
|
||||
console.log('[MenuBuilder] No MENU in response or OK=false');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[MenuBuilder] No existing menu or API not available');
|
||||
console.error('[MenuBuilder] Error loading menu:', err);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -983,3 +983,141 @@ body {
|
|||
padding: 0.875rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Order Detail Modal */
|
||||
.order-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.order-detail-section {
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.order-detail-section:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.order-detail-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.order-detail-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.order-detail-sub {
|
||||
font-size: 14px;
|
||||
color: var(--gray-500);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.order-items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-detail-item {
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.order-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.order-item-qty {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.order-item-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.order-item-price {
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.order-item-modifiers {
|
||||
margin-top: 8px;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.order-item-modifier {
|
||||
font-size: 13px;
|
||||
color: var(--gray-600);
|
||||
padding: 2px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modifier-price {
|
||||
color: var(--gray-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.order-item-remarks {
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
color: var(--gray-500);
|
||||
margin-top: 8px;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.order-detail-notes {
|
||||
background: var(--gray-50);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.order-detail-totals {
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.order-total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.order-total-row.total {
|
||||
border-top: 1px solid var(--gray-300);
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.order-detail-footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.order-detail-time {
|
||||
font-size: 13px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
|
|
|||
141
portal/portal.js
141
portal/portal.js
|
|
@ -58,6 +58,9 @@ const Portal = {
|
|||
|
||||
if (!token || !savedBusiness) {
|
||||
// Not logged in - redirect to login
|
||||
localStorage.removeItem('payfrit_portal_token');
|
||||
localStorage.removeItem('payfrit_portal_userid');
|
||||
localStorage.removeItem('payfrit_portal_business');
|
||||
window.location.href = BASE_PATH + '/portal/login.html';
|
||||
return;
|
||||
}
|
||||
|
|
@ -742,17 +745,139 @@ const Portal = {
|
|||
}
|
||||
},
|
||||
|
||||
// Logout
|
||||
logout() {
|
||||
if (confirm('Are you sure you want to logout?')) {
|
||||
window.location.href = '/index.cfm?mode=logout';
|
||||
// View order
|
||||
async viewOrder(orderId) {
|
||||
this.toast('Loading order...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.config.apiBaseUrl}/orders/getDetail.cfm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ OrderID: orderId })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.OK && data.ORDER) {
|
||||
this.showOrderDetailModal(data.ORDER);
|
||||
} else {
|
||||
this.toast(data.ERROR || 'Failed to load order', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Portal] Error loading order:', err);
|
||||
this.toast('Error loading order details', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// View order
|
||||
viewOrder(orderId) {
|
||||
this.toast(`Viewing order #${orderId}`, 'info');
|
||||
// TODO: Implement order detail view
|
||||
// Show order detail modal
|
||||
showOrderDetailModal(order) {
|
||||
document.getElementById('modalTitle').textContent = `Order #${order.OrderID}`;
|
||||
|
||||
const customerName = [order.Customer.FirstName, order.Customer.LastName].filter(Boolean).join(' ') || 'Guest';
|
||||
const servicePoint = order.ServicePoint.Name || 'Not assigned';
|
||||
|
||||
// Build line items HTML
|
||||
const lineItemsHtml = order.LineItems.map(item => {
|
||||
const modifiersHtml = item.Modifiers && item.Modifiers.length > 0
|
||||
? `<div class="order-item-modifiers">
|
||||
${item.Modifiers.map(mod => `
|
||||
<div class="order-item-modifier">
|
||||
+ ${mod.ItemName}
|
||||
${mod.UnitPrice > 0 ? `<span class="modifier-price">+$${mod.UnitPrice.toFixed(2)}</span>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const remarksHtml = item.Remarks
|
||||
? `<div class="order-item-remarks">"${item.Remarks}"</div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="order-detail-item">
|
||||
<div class="order-item-header">
|
||||
<span class="order-item-qty">${item.Quantity}x</span>
|
||||
<span class="order-item-name">${item.ItemName}</span>
|
||||
<span class="order-item-price">$${(item.UnitPrice * item.Quantity).toFixed(2)}</span>
|
||||
</div>
|
||||
${modifiersHtml}
|
||||
${remarksHtml}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('modalBody').innerHTML = `
|
||||
<div class="order-detail">
|
||||
<div class="order-detail-section">
|
||||
<div class="order-detail-label">Status</div>
|
||||
<span class="status-badge ${this.getStatusClass(order.Status)}">${order.StatusText}</span>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-section">
|
||||
<div class="order-detail-label">Customer</div>
|
||||
<div class="order-detail-value">${customerName}</div>
|
||||
${order.Customer.Phone ? `<div class="order-detail-sub">${order.Customer.Phone}</div>` : ''}
|
||||
${order.Customer.Email ? `<div class="order-detail-sub">${order.Customer.Email}</div>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="order-detail-section">
|
||||
<div class="order-detail-label">Service Point</div>
|
||||
<div class="order-detail-value">${servicePoint}</div>
|
||||
${order.ServicePoint.Type ? `<div class="order-detail-sub">${order.ServicePoint.Type}</div>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="order-detail-section">
|
||||
<div class="order-detail-label">Items</div>
|
||||
<div class="order-items-list">
|
||||
${lineItemsHtml || '<div class="empty-state">No items</div>'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${order.Notes ? `
|
||||
<div class="order-detail-section">
|
||||
<div class="order-detail-label">Notes</div>
|
||||
<div class="order-detail-notes">${order.Notes}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="order-detail-totals">
|
||||
<div class="order-total-row">
|
||||
<span>Subtotal</span>
|
||||
<span>$${order.Subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="order-total-row">
|
||||
<span>Tax</span>
|
||||
<span>$${order.Tax.toFixed(2)}</span>
|
||||
</div>
|
||||
${order.Tip > 0 ? `
|
||||
<div class="order-total-row">
|
||||
<span>Tip</span>
|
||||
<span>$${order.Tip.toFixed(2)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="order-total-row total">
|
||||
<span>Total</span>
|
||||
<span>$${order.Total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-footer">
|
||||
<div class="order-detail-time">Placed: ${this.formatDateTime(order.CreatedOn)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.showModal();
|
||||
},
|
||||
|
||||
// Format date time
|
||||
formatDateTime(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString([], {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
// Edit item
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@
|
|||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-title">
|
||||
<a href="/portal/#menu" style="color: var(--text-muted); text-decoration: none;">
|
||||
<a href="/portal/index.html#menu" style="color: var(--text-muted); text-decoration: none;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue