diff --git a/api/Application.cfm b/api/Application.cfm index 7ed0ece..2f98649 100644 --- a/api/Application.cfm +++ b/api/Application.cfm @@ -82,10 +82,26 @@ if (len(request._api_path)) { 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/listMine.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/tasks/complete.cfm", request._api_path)) request._api_isPublic = true; + + // Worker app endpoints + if (findNoCase("/api/workers/myBusinesses.cfm", request._api_path)) request._api_isPublic = true; // Portal endpoints if (findNoCase("/api/portal/stats.cfm", request._api_path)) request._api_isPublic = true; + // Menu builder endpoints + if (findNoCase("/api/menu/getForBuilder.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/menu/saveFromBuilder.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/tasks/create.cfm", request._api_path)) request._api_isPublic = true; + + // Admin endpoints + if (findNoCase("/api/admin/resetTestData.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/admin/debugTasks.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/admin/testTaskInsert.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/admin/debugBusinesses.cfm", request._api_path)) request._api_isPublic = true; + // Stripe endpoints if (findNoCase("/api/stripe/onboard.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/stripe/status.cfm", request._api_path)) request._api_isPublic = true; diff --git a/api/admin/debugBusinesses.cfm b/api/admin/debugBusinesses.cfm new file mode 100644 index 0000000..7f7e302 --- /dev/null +++ b/api/admin/debugBusinesses.cfm @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/admin/testTaskInsert.cfm b/api/admin/testTaskInsert.cfm new file mode 100644 index 0000000..f3ee706 --- /dev/null +++ b/api/admin/testTaskInsert.cfm @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + +#serializeJSON(result)# diff --git a/api/import/crimson_menu.cfm b/api/import/crimson_menu.cfm new file mode 100644 index 0000000..8812b63 --- /dev/null +++ b/api/import/crimson_menu.cfm @@ -0,0 +1,508 @@ + + + + + +// Import Crimson Mediterranean Cookhouse menu +// This script creates the business, categories, and all items + +response = { "OK": false, "steps": [], "errors": [] }; + +try { + // Step 1: Create the business + response.steps.append("Creating business record..."); + + // Check if business already exists by name + qCheck = queryExecute(" + SELECT BusinessID FROM Businesses WHERE BusinessName = :name + ", { name: "Crimson Mediterranean Cookhouse" }, { datasource: "payfrit" }); + + if (qCheck.recordCount > 0) { + BusinessID = qCheck.BusinessID; + response.steps.append("Business already exists with ID: " & BusinessID); + } else { + queryExecute(" + INSERT INTO Businesses (BusinessName, BusinessOwnerUserID) + VALUES (:name, :ownerID) + ", { + name: "Crimson Mediterranean Cookhouse", + ownerID: 2 // UserID 2 (John) + }, { datasource: "payfrit" }); + + qNew = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" }); + BusinessID = qNew.id; + response.steps.append("Created business with ID: " & BusinessID); + } + + // Step 2: Create categories + response.steps.append("Creating categories..."); + + categories = [ + { id: "grill", name: "From the Grill", order: 1 }, + { id: "wraps", name: "Wraps", order: 2 }, + { id: "salads", name: "Salads", order: 3 }, + { id: "sides", name: "Sides", order: 4 }, + { id: "hot_coffee", name: "Hot Coffee", order: 5 }, + { id: "iced_coffee", name: "Iced Coffee", order: 6 }, + { id: "hot_tea", name: "Hot Teas", order: 7 }, + { id: "iced_tea", name: "Iced Teas", order: 8 }, + { id: "beverages", name: "Beverages", order: 9 } + ]; + + categoryMap = {}; // Maps category string ID to CategoryID + + for (cat in categories) { + // Check if exists + qCat = queryExecute(" + SELECT CategoryID FROM Categories + WHERE CategoryBusinessID = :bizID AND CategoryName = :name + ", { bizID: BusinessID, name: cat.name }, { datasource: "payfrit" }); + + if (qCat.recordCount > 0) { + categoryMap[cat.id] = qCat.CategoryID; + } else { + queryExecute(" + INSERT INTO Categories (CategoryBusinessID, CategoryName, CategorySortOrder) + VALUES (:bizID, :name, :sortOrder) + ", { + bizID: BusinessID, + name: cat.name, + sortOrder: cat.order + }, { datasource: "payfrit" }); + + qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" }); + categoryMap[cat.id] = qNewCat.id; + } + } + + response.steps.append("Categories created/found: " & structKeyList(categoryMap)); + + // Step 3: Create modifier groups and their options + response.steps.append("Creating modifier groups..."); + + // We'll store modifier IDs for linking + modifierMap = {}; // Maps modifier string ID to ItemID + + // Helper function to create a modifier group + function createModifierGroup(categoryID, parentItemID, groupName, groupID, options, required, maxSelect) { + // Check if group exists + var qMod = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = :parentID + ", { catID: categoryID, name: groupName, parentID: parentItemID }, { datasource: "payfrit" }); + + var groupItemID = 0; + if (qMod.recordCount > 0) { + groupItemID = qMod.ItemID; + } else { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemParentItemID, ItemPrice, ItemIsActive, + ItemRequiresChildSelection, ItemMaxNumSelectionReq, ItemIsCollapsible, ItemSortOrder) + VALUES (:catID, :name, :parentID, 0, 1, :required, :maxSelect, 1, :sortOrder) + ", { + catID: categoryID, + name: groupName, + parentID: parentItemID, + required: required ? 1 : 0, + maxSelect: maxSelect, + sortOrder: 100 + }, { datasource: "payfrit" }); + + var qNewMod = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" }); + groupItemID = qNewMod.id; + } + + // Create options + var optionOrder = 1; + for (var opt in options) { + var qOpt = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = :parentID + ", { catID: categoryID, name: opt.name, parentID: groupItemID }, { datasource: "payfrit" }); + + if (qOpt.recordCount == 0) { + var isDefault = (optionOrder == 1 && required) ? 1 : 0; + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemParentItemID, ItemPrice, ItemIsActive, + ItemIsCheckedByDefault, ItemSortOrder) + VALUES (:catID, :name, :parentID, :price, 1, :isDefault, :sortOrder) + ", { + catID: categoryID, + name: opt.name, + parentID: groupItemID, + price: opt.price_adjustment ?: 0, + isDefault: isDefault, + sortOrder: optionOrder + }, { datasource: "payfrit" }); + } + optionOrder++; + } + + return groupItemID; + } + + // Step 4: Create menu items + response.steps.append("Creating menu items..."); + + // === FROM THE GRILL === + grillCatID = categoryMap["grill"]; + grillItems = [ + { name: "Chicken Kabob (White Meat)", desc: "Served with rice & choice of sides", price: 22.00, hasSides: true }, + { name: "Chicken Kabob (Dark Meat)", desc: "Served with rice & choice of sides", price: 22.00, hasSides: true }, + { name: "Filet Mignon Kabob", desc: "Served with rice & choice of sides", price: 23.00, hasSides: true }, + { name: "Ground Sirloin Kabob", desc: "Two skewers of charbroiled seasoned ground sirloin with rice & choice of sides", price: 21.00, hasSides: true }, + { name: "Grilled Salmon", desc: "Served with rice & choice of sides", price: 23.00, hasSides: true }, + { name: "Falafel Veggie Plate", desc: "3 falafels drizzled with tahini, served with rice, choice of salad & sides", price: 20.00, hasSides: true }, + { name: "Chicken Shawarma Plate", desc: "Dark meat served with rice, lebanese turnips, tahini & choice of sides", price: 22.00, hasSides: true } + ]; + + itemOrder = 1; + for (item in grillItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: grillCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemRequiresChildSelection, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :reqChild, :sortOrder) + ", { + catID: grillCatID, + name: item.name, + desc: item.desc, + price: item.price, + reqChild: item.hasSides ? 1 : 0, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === WRAPS === + wrapsCatID = categoryMap["wraps"]; + wrapItems = [ + { name: "Chicken Wrap", desc: "White or dark meat wrapped with lettuce, cucumbers, tomatoes, hummus, garlic sauce & rose sauce", price: 19.50 }, + { name: "BBQ Chicken Wrap", desc: "White meat wrapped with lettuce, corn, black beans, tomatoes, scallions, tortilla strips, monterey jack cheese with herb ranch & bbq sauce", price: 19.50 }, + { name: "Filet Mignon Wrap", desc: "Wrapped with romaine lettuce, cucumbers, tomatoes, hummus, garlic sauce & rose sauce", price: 19.50 }, + { name: "Ground Sirloin Wrap", desc: "Wrapped with romaine lettuce, cucumbers, tomatoes, hummus, garlic sauce & rose sauce", price: 18.50 }, + { name: "Hummus Veggie Wrap", desc: "Wrapped with rice, lettuce, cucumbers, tomatoes, lebanese turnip, garlic sauce & rose sauce", price: 16.50 }, + { name: "Falafel Wrap", desc: "Wrapped with lettuce, cucumbers, tomatoes, lebanese turnips, tahini & garlic sauce", price: 17.50 }, + { name: "Spicy Falafel Wrap", desc: "Wrapped with lettuce, cucumbers, tomatoes, lebanese turnips, spicy tahini & garlic sauce", price: 17.50 }, + { name: "Sesame Falafel Wrap", desc: "Wrapped with romaine lettuce, broccoli, corn, red onions, cucumbers & rose sauce with sesame dressing", price: 17.50 }, + { name: "Crimson Chicken Wrap", desc: "White meat wrapped with lettuce, monterey jack cheese & tortilla strips with spicy herb ranch dressing", price: 19.50 }, + { name: "Kale Chicken Caesar Wrap", desc: "White meat with kale, romaine lettuce & shaved parmesan cheese with caesar dressing", price: 19.50 }, + { name: "Chicken Shawarma Wrap", desc: "Dark meat wrapped with lettuce, hummus, cucumbers, tomatoes, lebanese turnips, tahini & garlic sauce", price: 19.50 } + ]; + + itemOrder = 1; + for (item in wrapItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: wrapsCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: wrapsCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === SALADS === + saladsCatID = categoryMap["salads"]; + saladItems = [ + { name: "Mediterranean Salad", desc: "Romaine lettuce, cucumbers, tomatoes, red onions, kalamata olives, garbanzo beans topped with feta cheese with balsamic vinaigrette", price: 19.50 }, + { name: "Cran-Ginger Salad", desc: "Mixed greens, tomatoes, broccoli, cucumbers, topped with strawberries, sliced almonds & dried cranberries in ginger dressing", price: 19.50 }, + { name: "Kale Salad", desc: "Red cabbage & carrots tossed in balsamic vinaigrette dressing", price: 19.50 }, + { name: "Asian Salad", desc: "Romaine lettuce, red cabbage, mandarin oranges, green onions, shredded carrots & asian noodles topped with sliced almonds in sesame dressing", price: 19.50 }, + { name: "BBQ Ranch Salad", desc: "Romaine lettuce, corn, black beans, tomatoes, scallions, fried onions & tortilla strips with monterey jack cheese in herb ranch & bbq sauce", price: 19.50 }, + { name: "Kale Caesar Salad", desc: "Kale, romaine lettuce & shaved parmesan cheese tossed in caesar dressing", price: 19.50 }, + { name: "Topolino Salad", desc: "Romaine lettuce, red onions, feta cheese, cranberries, sliced almonds & raisins in ranch vinaigrette dressing", price: 19.50 }, + { name: "Crimson Salad", desc: "Kale, romaine lettuce, avocado, corn, black beans, tomatoes, scallions, fried onions & tortilla strips with monterey jack cheese in spicy herb ranch", price: 19.50 } + ]; + + itemOrder = 1; + for (item in saladItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: saladsCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: saladsCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === SIDES === + sidesCatID = categoryMap["sides"]; + sideItems = [ + { name: "Tomato Soup", desc: "Made fresh daily", price: 5.00 }, + { name: "Lentil Soup", desc: "Red lentils, onions, jalapeños & ginger", price: 5.00 }, + { name: "Tzatziki", desc: "Yogurt tossed with cucumbers & dried mint", price: 5.00 }, + { name: "Dolma", desc: "Grape leaves stuffed with rice & rose sauce", price: 5.00 }, + { name: "Kale Salad Side", desc: "Red cabbage & carrots in balsamic vinaigrette", price: 5.00 }, + { name: "Mini Greek Salad", desc: "Cucumbers, tomatoes, red onions & feta cheese with lemon vinaigrette", price: 5.00 }, + { name: "Mixed Green Salad", desc: "Cucumbers, tomatoes & shredded carrots with balsamic vinaigrette", price: 5.00 }, + { name: "Mini Broccoli Salad", desc: "Cucumbers, corn & red cabbage with sesame dressing", price: 5.00 }, + { name: "Shirazi Salad", desc: "Cucumbers, tomatoes, parsley with lemon vinaigrette", price: 5.00 }, + { name: "Babaganoush", desc: "Grilled eggplant, garlic, tahini, lemon juice, olive oil & sumac", price: 5.00 }, + { name: "Hummus", desc: "Chickpeas, garlic, lemon juice, & tahini topped with olive oil & paprika", price: 5.00 }, + { name: "Grilled Vegetables", desc: "Seasoned vegetables grilled to order", price: 5.00 }, + { name: "Sweet Potato Fries", desc: "Fried in non-GMO cooking oil", price: 5.00 }, + { name: "French Fries", desc: "Fried in non-GMO cooking oil", price: 5.00 } + ]; + + itemOrder = 1; + for (item in sideItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: sidesCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: sidesCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === HOT COFFEE === + hotCoffeeCatID = categoryMap["hot_coffee"]; + hotCoffeeItems = [ + { name: "Coffee", desc: "Fresh brewed Intelligentsia coffee", price: 4.00 }, + { name: "Espresso", desc: "Single shot espresso", price: 3.50 }, + { name: "Americano", desc: "Espresso with hot water", price: 4.50 }, + { name: "Cappuccino", desc: "Espresso with steamed milk and foam", price: 5.00 }, + { name: "Latte", desc: "Espresso with steamed milk", price: 5.25 }, + { name: "Macchiato", desc: "Espresso marked with foam", price: 4.50 }, + { name: "Vanilla Latte", desc: "Espresso with steamed milk and vanilla", price: 5.50 }, + { name: "Caramel Latte", desc: "Espresso with steamed milk and caramel", price: 5.50 }, + { name: "Hazelnut Latte", desc: "Espresso with steamed milk and hazelnut", price: 5.50 }, + { name: "Honey Vanilla Latte", desc: "Espresso with steamed milk, honey and vanilla", price: 5.75 }, + { name: "Mocha Latte", desc: "Espresso with steamed milk and chocolate", price: 5.50 }, + { name: "Mocha Mint", desc: "Espresso with steamed milk, chocolate and mint", price: 5.75 }, + { name: "Hot Chocolate", desc: "Rich hot chocolate", price: 4.50 } + ]; + + itemOrder = 1; + for (item in hotCoffeeItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: hotCoffeeCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: hotCoffeeCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === ICED COFFEE === + icedCoffeeCatID = categoryMap["iced_coffee"]; + icedCoffeeItems = [ + { name: "Iced Coffee", desc: "Cold brewed Intelligentsia coffee over ice", price: 4.25 }, + { name: "Iced Espresso", desc: "Espresso over ice", price: 3.50 }, + { name: "Iced Americano", desc: "Espresso with cold water over ice", price: 4.75 }, + { name: "Iced Latte", desc: "Espresso with cold milk over ice", price: 5.25 }, + { name: "Iced Vanilla Latte", desc: "Espresso with cold milk and vanilla over ice", price: 5.75 }, + { name: "Iced Caramel Macchiato", desc: "Espresso with cold milk and caramel over ice", price: 5.75 }, + { name: "Iced Honey Vanilla Latte", desc: "Espresso with cold milk, honey and vanilla over ice", price: 6.00 }, + { name: "Iced Mocha", desc: "Espresso with cold milk and chocolate over ice", price: 5.75 }, + { name: "Iced Mocha Mint", desc: "Espresso with cold milk, chocolate and mint over ice", price: 6.00 }, + { name: "Angeleno", desc: "House specialty iced coffee drink", price: 6.00 } + ]; + + itemOrder = 1; + for (item in icedCoffeeItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: icedCoffeeCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: icedCoffeeCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === HOT TEAS === + hotTeaCatID = categoryMap["hot_tea"]; + hotTeaItems = [ + { name: "Organic Chamomile", desc: "Soothing organic chamomile tea", price: 4.00 }, + { name: "Organic Earl Grey", desc: "Classic bergamot-infused black tea", price: 4.00 }, + { name: "Organic Hot Chai Tea", desc: "Spiced organic chai tea", price: 4.00 }, + { name: "Organic House Chai Tea Latte", desc: "Spiced chai with steamed milk", price: 5.00 }, + { name: "Organic Jasmine Green", desc: "Delicate jasmine-scented green tea", price: 4.00 }, + { name: "Organic Mint Melange", desc: "Refreshing mint tea blend", price: 4.00 }, + { name: "Matcha Green Tea Latte", desc: "Japanese matcha with steamed milk", price: 5.75 }, + { name: "Persian Latte", desc: "Traditional Persian-style tea latte", price: 5.50 } + ]; + + itemOrder = 1; + for (item in hotTeaItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: hotTeaCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: hotTeaCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === ICED TEAS === + icedTeaCatID = categoryMap["iced_tea"]; + icedTeaItems = [ + { name: "Organic Black Iced Tea", desc: "Fresh brewed organic black tea over ice", price: 4.00 }, + { name: "Organic Tropical Green Iced Tea", desc: "Tropical-infused green tea over ice", price: 4.00 }, + { name: "Organic Hibiscus Berry Iced Tea", desc: "Hibiscus and berry tea blend over ice", price: 4.00 }, + { name: "Organic Hibiscus Berry Tea Palmer", desc: "Hibiscus berry tea with lemonade", price: 4.00 }, + { name: "Organic Black Tea Palmer", desc: "Black tea with lemonade", price: 4.00 }, + { name: "Organic Tropical Green Tea Palmer", desc: "Tropical green tea with lemonade", price: 4.00 }, + { name: "Iced Organic House Chai Tea Latte", desc: "Spiced chai with cold milk over ice", price: 5.25 }, + { name: "Iced Persian Latte", desc: "Persian-style tea latte over ice", price: 5.75 }, + { name: "Iced Matcha Green Tea Latte", desc: "Japanese matcha with cold milk over ice", price: 6.00 } + ]; + + itemOrder = 1; + for (item in icedTeaItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: icedTeaCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: icedTeaCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // === BEVERAGES === + bevCatID = categoryMap["beverages"]; + bevItems = [ + { name: "Coke", desc: "Coca-Cola", price: 3.00 }, + { name: "Diet Coke", desc: "Diet Coca-Cola", price: 3.00 }, + { name: "Sprite", desc: "Lemon-lime soda", price: 3.00 }, + { name: "Sparkling Water", desc: "Carbonated mineral water", price: 3.00 }, + { name: "Orange Pellegrino", desc: "San Pellegrino Aranciata", price: 2.50 }, + { name: "Bottled Water", desc: "Still bottled water", price: 3.00 }, + { name: "Lemonade", desc: "Fresh lemonade", price: 3.00 }, + { name: "Strawberry Lemonade", desc: "Fresh lemonade with strawberry", price: 3.00 }, + { name: "Milk", desc: "Cold milk", price: 3.00 }, + { name: "Orange Juice", desc: "Fresh orange juice", price: 3.50 } + ]; + + itemOrder = 1; + for (item in bevItems) { + qItem = queryExecute(" + SELECT ItemID FROM Items + WHERE ItemCategoryID = :catID AND ItemName = :name AND ItemParentItemID = 0 + ", { catID: bevCatID, name: item.name }, { datasource: "payfrit" }); + + if (qItem.recordCount == 0) { + queryExecute(" + INSERT INTO Items (ItemCategoryID, ItemName, ItemDescription, ItemParentItemID, ItemPrice, + ItemIsActive, ItemSortOrder) + VALUES (:catID, :name, :desc, 0, :price, 1, :sortOrder) + ", { + catID: bevCatID, + name: item.name, + desc: item.desc, + price: item.price, + sortOrder: itemOrder + }, { datasource: "payfrit" }); + } + itemOrder++; + } + + // Count total items + qCount = queryExecute(" + SELECT COUNT(*) as cnt FROM Items i + INNER JOIN Categories c ON c.CategoryID = i.ItemCategoryID + WHERE c.CategoryBusinessID = :bizID + ", { bizID: BusinessID }, { datasource: "payfrit" }); + + response.OK = true; + response.BusinessID = BusinessID; + response.totalItems = qCount.cnt; + response.steps.append("Import complete! BusinessID: " & BusinessID & ", Total items: " & qCount.cnt); + +} catch (any e) { + response.errors.append(e.message); + response.errors.append(e.detail); +} + +writeOutput(serializeJSON(response)); + diff --git a/api/menu/getForBuilder.cfm b/api/menu/getForBuilder.cfm new file mode 100644 index 0000000..8f9bcc5 --- /dev/null +++ b/api/menu/getForBuilder.cfm @@ -0,0 +1,108 @@ + +// Get menu data formatted for the builder UI +// Input: BusinessID +// Output: { OK: true, MENU: { categories: [...] } } + +param name="form.BusinessID" default="0"; +param name="url.BusinessID" default="#form.BusinessID#"; + +businessID = val(url.BusinessID); + +response = { "OK": false }; + +try { + if (businessID == 0) { + // Try to get from request body + requestBody = toString(getHttpRequestData().content); + if (len(requestBody)) { + jsonData = deserializeJSON(requestBody); + businessID = val(jsonData.BusinessID ?: 0); + } + } + + if (businessID == 0) { + throw("BusinessID is required"); + } + + // Get categories + categories = queryExecute(" + SELECT CategoryID, CategoryName, CategoryDescription, CategorySortOrder + FROM Categories + WHERE CategoryBusinessID = :businessID + ORDER BY CategorySortOrder, CategoryName + ", { businessID: businessID }); + + menuCategories = []; + + for (cat in categories) { + // Get items for this category (without Tasks join which may not exist) + items = queryExecute(" + SELECT i.ItemID, i.ItemName, i.ItemDescription, i.ItemPrice, + i.ItemIsActive, i.ItemSortOrder + FROM Items i + WHERE i.ItemCategoryID = :categoryID + AND i.ItemParentItemID = 0 + ORDER BY i.ItemSortOrder, i.ItemName + ", { categoryID: cat.CategoryID }); + + categoryItems = []; + + for (item in items) { + // Get modifiers for this item + modifiers = queryExecute(" + SELECT ItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder + FROM Items + WHERE ItemParentItemID = :itemID + ORDER BY ItemSortOrder, ItemName + ", { itemID: item.ItemID }); + + itemModifiers = []; + for (mod in modifiers) { + arrayAppend(itemModifiers, { + "id": mod.ItemID, + "name": mod.ItemName, + "price": mod.ItemPrice, + "isDefault": mod.ItemIsCheckedByDefault == 1, + "sortOrder": mod.ItemSortOrder + }); + } + + arrayAppend(categoryItems, { + "id": item.ItemID, + "name": item.ItemName, + "description": item.ItemDescription ?: "", + "price": item.ItemPrice, + "imageUrl": "", + "photoTaskId": "", + "modifiers": itemModifiers, + "sortOrder": item.ItemSortOrder + }); + } + + arrayAppend(menuCategories, { + "id": cat.CategoryID, + "name": cat.CategoryName, + "description": cat.CategoryDescription ?: "", + "sortOrder": cat.CategorySortOrder, + "items": categoryItems + }); + } + + response = { + "OK": true, + "MENU": { + "categories": menuCategories + } + }; + +} catch (any e) { + response = { + "OK": false, + "ERROR": e.message, + "DETAIL": e.detail ?: "" + }; +} + +cfheader(name="Content-Type", value="application/json"); +writeOutput(serializeJSON(response)); + diff --git a/api/menu/saveFromBuilder.cfm b/api/menu/saveFromBuilder.cfm new file mode 100644 index 0000000..d45aca4 --- /dev/null +++ b/api/menu/saveFromBuilder.cfm @@ -0,0 +1,158 @@ + +// Save menu data from the builder UI +// Input: BusinessID, Menu (JSON structure) +// Output: { OK: true } + +response = { "OK": false }; + +try { + requestBody = toString(getHttpRequestData().content); + if (!len(requestBody)) { + throw("Request body is required"); + } + + jsonData = deserializeJSON(requestBody); + businessID = val(jsonData.BusinessID ?: 0); + menu = jsonData.Menu ?: {}; + + if (businessID == 0) { + throw("BusinessID is required"); + } + + if (!structKeyExists(menu, "categories") || !isArray(menu.categories)) { + throw("Menu categories are required"); + } + + // Process each category + for (cat in menu.categories) { + categoryID = 0; + + // Check if it's an existing category (numeric ID) or new (temp_ prefix) + if (isNumeric(cat.id)) { + categoryID = val(cat.id); + // Update existing category + queryExecute(" + UPDATE Categories + SET CategoryName = :name, + CategoryDescription = :description, + CategorySortOrder = :sortOrder + WHERE CategoryID = :categoryID + ", { + categoryID: categoryID, + name: cat.name, + description: cat.description ?: "", + sortOrder: val(cat.sortOrder ?: 0) + }); + } else { + // Insert new category + queryExecute(" + INSERT INTO Categories (CategoryBusinessID, CategoryName, CategoryDescription, CategorySortOrder) + VALUES (:businessID, :name, :description, :sortOrder) + ", { + businessID: businessID, + name: cat.name, + description: cat.description ?: "", + sortOrder: val(cat.sortOrder ?: 0) + }); + + // Get the new category ID + result = queryExecute("SELECT LAST_INSERT_ID() as newID"); + categoryID = result.newID; + } + + // Process items in this category + if (structKeyExists(cat, "items") && isArray(cat.items)) { + for (item in cat.items) { + itemID = 0; + + if (isNumeric(item.id)) { + itemID = val(item.id); + // Update existing item (without ImageURL which may not exist) + queryExecute(" + UPDATE Items + SET ItemName = :name, + ItemDescription = :description, + ItemPrice = :price, + ItemCategoryID = :categoryID, + ItemSortOrder = :sortOrder + WHERE ItemID = :itemID + ", { + itemID: itemID, + name: item.name, + description: item.description ?: "", + price: val(item.price ?: 0), + categoryID: categoryID, + sortOrder: val(item.sortOrder ?: 0) + }); + } else { + // Insert new item (without ImageURL which may not exist) + queryExecute(" + INSERT INTO Items (ItemBusinessID, ItemCategoryID, ItemName, ItemDescription, ItemPrice, ItemSortOrder, ItemIsActive, ItemParentItemID) + VALUES (:businessID, :categoryID, :name, :description, :price, :sortOrder, 1, 0) + ", { + businessID: businessID, + categoryID: categoryID, + name: item.name, + description: item.description ?: "", + price: val(item.price ?: 0), + sortOrder: val(item.sortOrder ?: 0) + }); + + result = queryExecute("SELECT LAST_INSERT_ID() as newID"); + itemID = result.newID; + } + + // Process modifiers for this item + if (structKeyExists(item, "modifiers") && isArray(item.modifiers)) { + for (mod in item.modifiers) { + if (isNumeric(mod.id)) { + // Update existing modifier + queryExecute(" + UPDATE Items + SET ItemName = :name, + ItemPrice = :price, + ItemIsCheckedByDefault = :isDefault, + ItemSortOrder = :sortOrder + WHERE ItemID = :modID + ", { + modID: val(mod.id), + name: mod.name, + price: val(mod.price ?: 0), + isDefault: (mod.isDefault ?: false) ? 1 : 0, + sortOrder: val(mod.sortOrder ?: 0) + }); + } else { + // Insert new modifier + queryExecute(" + INSERT INTO Items (ItemBusinessID, ItemCategoryID, ItemParentItemID, ItemName, ItemPrice, ItemIsCheckedByDefault, ItemSortOrder, ItemIsActive) + VALUES (:businessID, :categoryID, :parentID, :name, :price, :isDefault, :sortOrder, 1) + ", { + businessID: businessID, + categoryID: categoryID, + parentID: itemID, + name: mod.name, + price: val(mod.price ?: 0), + isDefault: (mod.isDefault ?: false) ? 1 : 0, + sortOrder: val(mod.sortOrder ?: 0) + }); + } + } + } + } + } + } + + response = { "OK": true }; + +} catch (any e) { + response = { + "OK": false, + "ERROR": e.message, + "DETAIL": e.detail ?: "", + "TYPE": e.type ?: "" + }; +} + +cfheader(name="Content-Type", value="application/json"); +writeOutput(serializeJSON(response)); + diff --git a/api/orders/listForKDS.cfm b/api/orders/listForKDS.cfm index 9a52f25..4236d17 100644 --- a/api/orders/listForKDS.cfm +++ b/api/orders/listForKDS.cfm @@ -89,7 +89,8 @@ oli.OrderLineItemRemark, oli.OrderLineItemIsDeleted, i.ItemName, - i.ItemParentItemID + i.ItemParentItemID, + i.ItemIsCheckedByDefault FROM OrderLineItems oli INNER JOIN Items i ON i.ItemID = oli.OrderLineItemItemID WHERE oli.OrderLineItemOrderID = ? @@ -107,7 +108,8 @@ "OrderLineItemQuantity": qLineItems.OrderLineItemQuantity, "OrderLineItemRemark": qLineItems.OrderLineItemRemark, "ItemName": qLineItems.ItemName, - "ItemParentItemID": qLineItems.ItemParentItemID + "ItemParentItemID": qLineItems.ItemParentItemID, + "ItemIsCheckedByDefault": qLineItems.ItemIsCheckedByDefault })> diff --git a/api/orders/submit.cfm b/api/orders/submit.cfm index 1274cf8..51b4d1e 100644 --- a/api/orders/submit.cfm +++ b/api/orders/submit.cfm @@ -140,9 +140,11 @@ - + @@ -47,6 +49,8 @@ + + + + + + + + + + + + + + + + + + + diff --git a/api/tasks/accept.cfm b/api/tasks/accept.cfm index 4fe7e71..cc1928e 100644 --- a/api/tasks/accept.cfm +++ b/api/tasks/accept.cfm @@ -28,7 +28,6 @@ - @@ -36,9 +35,9 @@ - + @@ -47,27 +46,26 @@ - - + + - + @@ -75,7 +73,7 @@ diff --git a/api/tasks/complete.cfm b/api/tasks/complete.cfm new file mode 100644 index 0000000..ff7dbca --- /dev/null +++ b/api/tasks/complete.cfm @@ -0,0 +1,98 @@ + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/tasks/create.cfm b/api/tasks/create.cfm new file mode 100644 index 0000000..b3e1c7a --- /dev/null +++ b/api/tasks/create.cfm @@ -0,0 +1,98 @@ + +// Create a task (e.g., photo task for menu item) +// Input: BusinessID, ItemID, TaskType, Instructions, PYTReward +// Output: { OK: true, TASK_ID: ... } + +response = { "OK": false }; + +try { + requestBody = toString(getHttpRequestData().content); + if (!len(requestBody)) { + throw("Request body is required"); + } + + jsonData = deserializeJSON(requestBody); + businessID = val(jsonData.BusinessID ?: 0); + itemID = val(jsonData.ItemID ?: 0); + taskType = jsonData.TaskType ?: "employee_photo"; + instructions = jsonData.Instructions ?: ""; + pytReward = val(jsonData.PYTReward ?: 0); + + if (businessID == 0) { + throw("BusinessID is required"); + } + + // Get item info if itemID provided + itemName = ""; + if (itemID > 0) { + itemQuery = queryExecute(" + SELECT ItemName FROM Items WHERE ItemID = :itemID + ", { itemID: itemID }); + if (itemQuery.recordCount) { + itemName = itemQuery.ItemName; + } + } + + // Create task description + taskDescription = ""; + switch(taskType) { + case "employee_photo": + taskDescription = "Take a photo of: " & itemName; + break; + case "user_photo": + taskDescription = "Submit a photo of " & itemName & " to earn " & pytReward & " PYT"; + break; + default: + taskDescription = instructions; + } + + // Insert task + queryExecute(" + INSERT INTO Tasks ( + TaskBusinessID, + TaskItemID, + TaskType, + TaskDescription, + TaskInstructions, + TaskPYTReward, + TaskStatus, + TaskCreatedOn + ) VALUES ( + :businessID, + :itemID, + :taskType, + :description, + :instructions, + :pytReward, + 'pending', + NOW() + ) + ", { + businessID: businessID, + itemID: itemID, + taskType: taskType, + description: taskDescription, + instructions: instructions, + pytReward: pytReward + }); + + // Get the new task ID + result = queryExecute("SELECT LAST_INSERT_ID() as newID"); + taskID = result.newID; + + response = { + "OK": true, + "TASK_ID": taskID, + "MESSAGE": "Task created successfully" + }; + +} catch (any e) { + response = { + "OK": false, + "ERROR": e.message + }; +} + +cfheader(name="Content-Type", value="application/json"); +writeOutput(serializeJSON(response)); + diff --git a/api/tasks/listMine.cfm b/api/tasks/listMine.cfm new file mode 100644 index 0000000..28fa5fe --- /dev/null +++ b/api/tasks/listMine.cfm @@ -0,0 +1,129 @@ + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = DATE_SUB(CURDATE(), INTERVAL 7 DAY)")> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/tasks/listPending.cfm b/api/tasks/listPending.cfm index 738c8db..c094d11 100644 --- a/api/tasks/listPending.cfm +++ b/api/tasks/listPending.cfm @@ -35,8 +35,8 @@ - - + + @@ -52,35 +52,41 @@ t.TaskID, t.TaskBusinessID, t.TaskCategoryID, - t.TaskTitle, - t.TaskDetails, - t.TaskCreatedOn, - t.TaskStatusID, - t.TaskSourceType, - t.TaskSourceID, + t.TaskOrderID, + t.TaskTypeID, + t.TaskAddedOn, + t.TaskClaimedByUserID, tc.TaskCategoryName, tc.TaskCategoryColor FROM Tasks t LEFT JOIN TaskCategories tc ON tc.TaskCategoryID = t.TaskCategoryID WHERE #whereSQL# - ORDER BY t.TaskCreatedOn ASC + ORDER BY t.TaskAddedOn ASC ", params, { datasource = "payfrit" })> + + + + + + + + diff --git a/api/tasks/setup.cfm b/api/tasks/setup.cfm new file mode 100644 index 0000000..7142665 --- /dev/null +++ b/api/tasks/setup.cfm @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + {"OK": true, "MESSAGE": "Setup complete"} + + + {"OK": false, "ERROR": "#cfcatch.message#"} + + diff --git a/api/workers/myBusinesses.cfm b/api/workers/myBusinesses.cfm new file mode 100644 index 0000000..1915381 --- /dev/null +++ b/api/workers/myBusinesses.cfm @@ -0,0 +1,81 @@ + + + + + + + #serializeJSON(arguments.payload)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kds/kds.js b/kds/kds.js index f0d1528..d95d121 100644 --- a/kds/kds.js +++ b/kds/kds.js @@ -261,6 +261,12 @@ function renderAllModifiers(modifiers, allItems) { function collectLeafModifiers(mods) { mods.forEach(mod => { + // Skip default items - they weren't explicitly chosen by the customer + if (mod.ItemIsCheckedByDefault === 1 || mod.ItemIsCheckedByDefault === true || mod.ItemIsCheckedByDefault === "1") { + console.log(`[renderAllModifiers] Skipping default item: ${mod.ItemName}`); + return; + } + const children = allItems.filter(item => item.OrderLineItemParentOrderLineItemID === mod.OrderLineItemID); if (children.length === 0) { // This is a leaf - no children diff --git a/portal/menu-builder.html b/portal/menu-builder.html new file mode 100644 index 0000000..25f2a81 --- /dev/null +++ b/portal/menu-builder.html @@ -0,0 +1,2252 @@ + + + + + + Menu Builder - Payfrit + + + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ +
+

Components

+
+
+
📁
+
+
Category
+
Group of menu items
+
+
+
+
🍽️
+
+
Menu Item
+
Orderable product
+
+
+
+
⚙️
+
+
Modifier
+
Add-on or option
+
+
+
+
📋
+
+
Modifier Group
+
Group of options
+
+
+
+ +

Templates

+
+
+
📏
+
+
Size Options
+
Small, Medium, Large
+
+
+
+
🥩
+
+
Protein Options
+
Chicken, Beef, Veggie
+
+
+
+
🌡️
+
+
Temperature
+
Hot, Iced, Blended
+
+
+
+ +

Quick Stats

+
+
+ Categories: + 0 +
+
+ Items: + 0 +
+
+ Photos Missing: + 0 +
+
+
+ + +
+
+

+ Menu Builder + +

+
+ + +
+ + +
+
+

Properties

+
+ +
+

+ Select an item to edit its properties +

+
+
+
+
+ + +
+
+ + + + + Edit + Enter +
+
+ + + + + Clone + Ctrl+D +
+
+ + + + + + Create Photo Task +
+
+
+ + + + Move Up + Alt+Up +
+
+ + + + Move Down + Alt+Down +
+
+
+ + + + Delete + Del +
+
+ + + + + +
+ + + + diff --git a/portal/portal.js b/portal/portal.js index 044fd64..41dd3b8 100644 --- a/portal/portal.js +++ b/portal/portal.js @@ -310,25 +310,40 @@ const Portal = { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }, - // Load menu - redirect to full menu editor + // Load menu - show menu builder options async loadMenu() { console.log('[Portal] Loading menu...'); - // Show link to full menu editor + // Show links to menu tools const container = document.getElementById('menuGrid'); container.innerHTML = ` -