diff --git a/api/Application.cfm b/api/Application.cfm index e8ba379..e545bc5 100644 --- a/api/Application.cfm +++ b/api/Application.cfm @@ -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; diff --git a/api/admin/beaconStatus.cfm b/api/admin/beaconStatus.cfm new file mode 100644 index 0000000..1eab4df --- /dev/null +++ b/api/admin/beaconStatus.cfm @@ -0,0 +1,60 @@ + + + + + +// 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 +})); + diff --git a/api/admin/checkBigDeans.cfm b/api/admin/checkBigDeans.cfm new file mode 100644 index 0000000..5262e7c --- /dev/null +++ b/api/admin/checkBigDeans.cfm @@ -0,0 +1,33 @@ + + + + + +// 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 +})); + diff --git a/api/admin/debugBigDeansMenu.cfm b/api/admin/debugBigDeansMenu.cfm new file mode 100644 index 0000000..749cfe2 --- /dev/null +++ b/api/admin/debugBigDeansMenu.cfm @@ -0,0 +1,53 @@ + + + + + +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 +})); + diff --git a/api/admin/describeTable.cfm b/api/admin/describeTable.cfm new file mode 100644 index 0000000..823c6cb --- /dev/null +++ b/api/admin/describeTable.cfm @@ -0,0 +1,54 @@ + + + + + +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 + })); +} + diff --git a/api/admin/listTables.cfm b/api/admin/listTables.cfm new file mode 100644 index 0000000..4daa9dd --- /dev/null +++ b/api/admin/listTables.cfm @@ -0,0 +1,30 @@ + + + + + +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 + })); +} + diff --git a/api/admin/randomizePrices.cfm b/api/admin/randomizePrices.cfm new file mode 100644 index 0000000..93a5895 --- /dev/null +++ b/api/admin/randomizePrices.cfm @@ -0,0 +1,179 @@ + + + + + +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 +})); + diff --git a/api/admin/setupBigDeansInfo.cfm b/api/admin/setupBigDeansInfo.cfm new file mode 100644 index 0000000..ac83a0e --- /dev/null +++ b/api/admin/setupBigDeansInfo.cfm @@ -0,0 +1,149 @@ + + + + + +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)); + diff --git a/api/admin/switchBeacons.cfm b/api/admin/switchBeacons.cfm index e5ed2a8..40808b3 100644 --- a/api/admin/switchBeacons.cfm +++ b/api/admin/switchBeacons.cfm @@ -4,8 +4,8 @@ // 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 diff --git a/api/admin/updateBeaconMapping.cfm b/api/admin/updateBeaconMapping.cfm new file mode 100644 index 0000000..6669685 --- /dev/null +++ b/api/admin/updateBeaconMapping.cfm @@ -0,0 +1,52 @@ + + + + + +// 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 +})); + diff --git a/api/admin/updateBigDeans.cfm b/api/admin/updateBigDeans.cfm new file mode 100644 index 0000000..fcb672a --- /dev/null +++ b/api/admin/updateBigDeans.cfm @@ -0,0 +1,85 @@ + + + + + +// 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 + })); +} + diff --git a/api/businesses/get.cfm b/api/businesses/get.cfm index 4c63f1a..7b7886d 100644 --- a/api/businesses/get.cfm +++ b/api/businesses/get.cfm @@ -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) }; diff --git a/api/orders/getDetail.cfm b/api/orders/getDetail.cfm new file mode 100644 index 0000000..81725b9 --- /dev/null +++ b/api/orders/getDetail.cfm @@ -0,0 +1,172 @@ + + + + + + +/** + * 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"; + } +} + diff --git a/api/tasks/getDetails.cfm b/api/tasks/getDetails.cfm index b6e855c..bbc33f1 100644 --- a/api/tasks/getDetails.cfm +++ b/api/tasks/getDetails.cfm @@ -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": [] }> + + + + + + + + diff --git a/portal/menu-builder.html b/portal/menu-builder.html index b14f799..fbb3f64 100644 --- a/portal/menu-builder.html +++ b/portal/menu-builder.html @@ -717,7 +717,7 @@
- + @@ -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); } }, diff --git a/portal/portal.css b/portal/portal.css index 4ce7033..631e048 100644 --- a/portal/portal.css +++ b/portal/portal.css @@ -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); +} diff --git a/portal/portal.js b/portal/portal.js index 2195830..5ee5d5e 100644 --- a/portal/portal.js +++ b/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 + ? `
+ ${item.Modifiers.map(mod => ` +
+ + ${mod.ItemName} + ${mod.UnitPrice > 0 ? `+$${mod.UnitPrice.toFixed(2)}` : ''} +
+ `).join('')} +
` + : ''; + + const remarksHtml = item.Remarks + ? `
"${item.Remarks}"
` + : ''; + + return ` +
+
+ ${item.Quantity}x + ${item.ItemName} + $${(item.UnitPrice * item.Quantity).toFixed(2)} +
+ ${modifiersHtml} + ${remarksHtml} +
+ `; + }).join(''); + + document.getElementById('modalBody').innerHTML = ` +
+
+
Status
+ ${order.StatusText} +
+ +
+
Customer
+
${customerName}
+ ${order.Customer.Phone ? `
${order.Customer.Phone}
` : ''} + ${order.Customer.Email ? `
${order.Customer.Email}
` : ''} +
+ +
+
Service Point
+
${servicePoint}
+ ${order.ServicePoint.Type ? `
${order.ServicePoint.Type}
` : ''} +
+ +
+
Items
+
+ ${lineItemsHtml || '
No items
'} +
+
+ + ${order.Notes ? ` +
+
Notes
+
${order.Notes}
+
+ ` : ''} + +
+
+ Subtotal + $${order.Subtotal.toFixed(2)} +
+
+ Tax + $${order.Tax.toFixed(2)} +
+ ${order.Tip > 0 ? ` +
+ Tip + $${order.Tip.toFixed(2)} +
+ ` : ''} +
+ Total + $${order.Total.toFixed(2)} +
+
+ + +
+ `; + 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 diff --git a/portal/station-assignment.html b/portal/station-assignment.html index 7ea36aa..37e4d90 100644 --- a/portal/station-assignment.html +++ b/portal/station-assignment.html @@ -312,7 +312,7 @@