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:
John Mizerek 2026-01-07 11:45:21 -08:00
parent e9b44ec4be
commit ec81af5cdd
18 changed files with 1248 additions and 17 deletions

View file

@ -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/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/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/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/listPending.cfm", request._api_path)) request._api_isPublic = true;
if (findNoCase("/api/tasks/accept.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/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/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/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/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/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 // Setup/Import endpoints
if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/setup/importBusiness.cfm", request._api_path)) request._api_isPublic = true;

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View file

@ -4,8 +4,8 @@
<cfscript> <cfscript>
// Switch all beacons from one business to another // Switch all beacons from one business to another
fromBiz = 27; // Big Dean's fromBiz = 17; // In-N-Out
toBiz = 17; // In-N-Out toBiz = 27; // Big Dean's
queryExecute(" queryExecute("
UPDATE lt_Beacon_Businesses_ServicePoints UPDATE lt_Beacon_Businesses_ServicePoints

View 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>

View 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>

View file

@ -32,11 +32,12 @@ try {
abort; abort;
} }
// Get business details (only columns that exist) // Get business details
q = queryExecute(" q = queryExecute("
SELECT SELECT
BusinessID, BusinessID,
BusinessName, BusinessName,
BusinessPhone,
BusinessStripeAccountID, BusinessStripeAccountID,
BusinessStripeOnboardingComplete BusinessStripeOnboardingComplete
FROM Businesses FROM Businesses
@ -49,10 +50,74 @@ try {
abort; 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 // Build business object
business = { business = {
"BusinessID": q.BusinessID, "BusinessID": q.BusinessID,
"BusinessName": q.BusinessName, "BusinessName": q.BusinessName,
"BusinessAddress": addressStr,
"BusinessPhone": q.BusinessPhone,
"BusinessHours": hoursStr,
"BusinessHoursDetail": hoursArr,
"StripeConnected": (len(q.BusinessStripeAccountID) > 0 && q.BusinessStripeOnboardingComplete == 1) "StripeConnected": (len(q.BusinessStripeAccountID) > 0 && q.BusinessStripeOnboardingComplete == 1)
}; };

172
api/orders/getDetail.cfm Normal file
View 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>

View file

@ -58,7 +58,8 @@
sp.ServicePointTypeID, sp.ServicePointTypeID,
u.UserID as CustomerUserID, u.UserID as CustomerUserID,
u.UserFirstName, u.UserFirstName,
u.UserLastName u.UserLastName,
u.UserContactNumber
FROM Tasks t FROM Tasks t
LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID
LEFT JOIN Orders o ON o.OrderID = t.TaskOrderID LEFT JOIN Orders o ON o.OrderID = t.TaskOrderID
@ -98,11 +99,27 @@
"CustomerUserID": qTask.CustomerUserID ?: 0, "CustomerUserID": qTask.CustomerUserID ?: 0,
"CustomerFirstName": qTask.UserFirstName ?: "", "CustomerFirstName": qTask.UserFirstName ?: "",
"CustomerLastName": qTask.UserLastName ?: "", "CustomerLastName": qTask.UserLastName ?: "",
"CustomerPhone": "", "CustomerPhone": qTask.UserContactNumber ?: "",
"CustomerPhotoUrl": qTask.CustomerUserID GT 0 ? "https://biz.payfrit.com/uploads/users/" & qTask.CustomerUserID & ".jpg" : "", "CustomerPhotoUrl": qTask.CustomerUserID GT 0 ? "https://biz.payfrit.com/uploads/users/" & qTask.CustomerUserID & ".jpg" : "",
"BeaconUUID": "",
"LineItems": [] "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 ---> <!--- Get order line items if there's an order --->
<cfif qTask.OrderID GT 0> <cfif qTask.OrderID GT 0>
<cfset qLineItems = queryExecute(" <cfset qLineItems = queryExecute("
@ -134,7 +151,7 @@
"ItemPrice": qLineItems.OrderLineItemPrice, "ItemPrice": qLineItems.OrderLineItemPrice,
"Quantity": qLineItems.OrderLineItemQuantity, "Quantity": qLineItems.OrderLineItemQuantity,
"Remark": qLineItems.OrderLineItemRemark, "Remark": qLineItems.OrderLineItemRemark,
"IsModifier": qLineItems.ItemParentItemID GT 0 "IsModifier": qLineItems.OrderLineItemParentOrderLineItemID GT 0
})> })>
</cfloop> </cfloop>
</cfif> </cfif>

View file

@ -717,7 +717,7 @@
<!-- Toolbar --> <!-- Toolbar -->
<div class="builder-toolbar"> <div class="builder-toolbar">
<div class="toolbar-group"> <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"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7"/> <path d="M19 12H5M12 19l-7-7 7-7"/>
</svg> </svg>
@ -2429,23 +2429,32 @@
// Load menu from API // Load menu from API
async loadMenu() { async loadMenu() {
try { 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`, { const response = await fetch(`${this.config.apiBaseUrl}/menu/getForBuilder.cfm`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ BusinessID: this.config.businessId }) body: JSON.stringify({ BusinessID: this.config.businessId })
}); });
console.log('[MenuBuilder] Response status:', response.status);
const data = await response.json(); 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) { if (data.OK && data.MENU) {
this.menu = data.MENU; this.menu = data.MENU;
console.log('[MenuBuilder] Menu categories:', this.menu.categories?.length || 0);
// Store templates from API // Store templates from API
if (data.TEMPLATES) { if (data.TEMPLATES) {
this.templates = data.TEMPLATES; this.templates = data.TEMPLATES;
this.renderTemplateLibrary(); this.renderTemplateLibrary();
} }
this.render(); this.render();
} else {
console.log('[MenuBuilder] No MENU in response or OK=false');
} }
} catch (err) { } catch (err) {
console.log('[MenuBuilder] No existing menu or API not available'); console.error('[MenuBuilder] Error loading menu:', err);
} }
}, },

View file

@ -983,3 +983,141 @@ body {
padding: 0.875rem 2rem; padding: 0.875rem 2rem;
font-size: 1.1rem; 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);
}

View file

@ -58,6 +58,9 @@ const Portal = {
if (!token || !savedBusiness) { if (!token || !savedBusiness) {
// Not logged in - redirect to login // 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'; window.location.href = BASE_PATH + '/portal/login.html';
return; return;
} }
@ -742,17 +745,139 @@ const Portal = {
} }
}, },
// Logout // View order
logout() { async viewOrder(orderId) {
if (confirm('Are you sure you want to logout?')) { this.toast('Loading order...', 'info');
window.location.href = '/index.cfm?mode=logout';
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 // Show order detail modal
viewOrder(orderId) { showOrderDetailModal(order) {
this.toast(`Viewing order #${orderId}`, 'info'); document.getElementById('modalTitle').textContent = `Order #${order.OrderID}`;
// TODO: Implement order detail view
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 // Edit item

View file

@ -312,7 +312,7 @@
<!-- Toolbar --> <!-- Toolbar -->
<div class="toolbar"> <div class="toolbar">
<div class="toolbar-title"> <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"> <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"/> <path d="M19 12H5M12 19l-7-7 7-7"/>
</svg> </svg>