payfrit-api/api/menu/getForBuilder.php
John Mizerek 28d86ba6e5 Fix production webroot path — both servers use /opt/lucee/tomcat/webapps/ROOT
Added luceeWebroot() helper to avoid repeating the path. The previous
fix incorrectly used /var/www/biz.payfrit.com for production, but both
dev and biz use the same Lucee webroot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:19:10 -07:00

400 lines
14 KiB
PHP

<?php
require_once __DIR__ . '/../helpers.php';
runAuth();
/**
* Get Menu for Builder
* Returns categories and items in structured format for the menu builder UI
*/
/**
* Recursively build nested options tree from flat rows.
*/
function buildOptionsTree(array $allOptions, int $parentId): array {
$result = [];
foreach ($allOptions as $opt) {
if ((int) $opt['ParentItemID'] !== $parentId) continue;
$children = buildOptionsTree($allOptions, (int) $opt['ItemID']);
$result[] = [
'id' => 'opt_' . $opt['ItemID'],
'dbId' => (int) $opt['ItemID'],
'name' => $opt['Name'],
'price' => $opt['Price'],
'isDefault' => (int) $opt['IsDefault'] === 1,
'sortOrder' => (int) $opt['SortOrder'],
'requiresSelection' => (int) ($opt['RequiresSelection'] ?? 0) === 1,
'maxSelections' => (int) ($opt['MaxSelections'] ?? 0),
'options' => $children,
];
}
usort($result, fn($a, $b) => $a['sortOrder'] - $b['sortOrder']);
return $result;
}
$data = readJsonBody();
$businessID = (int) ($data['BusinessID'] ?? 0);
if ($businessID === 0) {
jsonResponse(['OK' => false, 'ERROR' => 'missing_business_id', 'MESSAGE' => 'BusinessID is required']);
}
$menuID = (int) ($data['MenuID'] ?? 0);
try {
// Get business default menu and timezone
$defaultMenuID = 0;
$businessTimezone = 'America/Los_Angeles';
try {
$qBiz = queryOne("SELECT DefaultMenuID, Timezone FROM Businesses WHERE ID = ?", [$businessID]);
if ($qBiz) {
if (!empty($qBiz['DefaultMenuID'])) $defaultMenuID = (int) $qBiz['DefaultMenuID'];
if (!empty($qBiz['Timezone'])) $businessTimezone = $qBiz['Timezone'];
}
} catch (Exception $e) {}
// Get all menus for this business
$allMenus = [];
try {
$qMenus = queryTimed("
SELECT ID, Name, Description, DaysActive, StartTime, EndTime, SortOrder
FROM Menus
WHERE BusinessID = ? AND IsActive = 1
ORDER BY SortOrder, Name
", [$businessID]);
foreach ($qMenus as $m) {
$allMenus[] = [
'MenuID' => (int) $m['ID'],
'MenuName' => $m['Name'],
'MenuDescription' => $m['Description'] ?? '',
'MenuDaysActive' => (int) $m['DaysActive'],
'MenuStartTime' => !empty($m['StartTime']) ? (new DateTime($m['StartTime']))->format('H:i') : '',
'MenuEndTime' => !empty($m['EndTime']) ? (new DateTime($m['EndTime']))->format('H:i') : '',
'SortOrder' => (int) $m['SortOrder'],
];
}
// Auto-select menu based on current time when no specific menu requested
if ($menuID === 0 && count($qMenus) > 1) {
$currentTime = substr(getTimeInZone($businessTimezone), 0, 5); // "HH:mm"
$currentDay = getDayInZone($businessTimezone); // 1=Sun, 2=Mon, ... 7=Sat
$dayBit = pow(2, $currentDay - 1);
$activeMenuIds = [];
foreach ($qMenus as $m) {
if (((int) $m['DaysActive'] & $dayBit) === 0) continue;
$hasStart = !empty($m['StartTime']);
$hasEnd = !empty($m['EndTime']);
if ($hasStart && $hasEnd) {
$startT = (new DateTime($m['StartTime']))->format('H:i');
$endT = (new DateTime($m['EndTime']))->format('H:i');
if ($currentTime >= $startT && $currentTime <= $endT) {
$activeMenuIds[] = (int) $m['ID'];
}
} else {
$activeMenuIds[] = (int) $m['ID'];
}
}
if (count($activeMenuIds) === 1) {
$menuID = $activeMenuIds[0];
} elseif (count($activeMenuIds) > 1 && $defaultMenuID > 0 && in_array($defaultMenuID, $activeMenuIds)) {
$menuID = $defaultMenuID;
}
}
} catch (Exception $e) {
// Menus table might not exist yet
}
// Check if Categories table has data
$hasCategoriesData = false;
try {
$qCatCheck = queryOne("SELECT 1 as x FROM Categories WHERE BusinessID = ? LIMIT 1", [$businessID]);
$hasCategoriesData = $qCatCheck !== null;
} catch (Exception $e) {}
if ($hasCategoriesData) {
// Use Categories table
$catParams = [$businessID];
$menuFilter = '';
if ($menuID > 0) {
$menuFilter = ' AND MenuID = ?';
$catParams[] = $menuID;
}
$qCatRows = queryTimed("
SELECT ID, Name, ParentCategoryID, SortOrder, MenuID
FROM Categories
WHERE BusinessID = ? $menuFilter
ORDER BY SortOrder, Name
", $catParams);
$qItemRows = queryTimed("
SELECT
i.ID, i.CategoryID as CategoryItemID, i.Name, i.Description,
i.Price, i.SortOrder, i.IsActive
FROM Items i
WHERE i.BusinessID = ?
AND i.IsActive = 1
AND i.CategoryID > 0
ORDER BY i.SortOrder, i.Name
", [$businessID]);
$qDirectModifiers = queryTimed("
SELECT
m.ID as ItemID, m.ParentItemID, m.Name, m.Price,
m.IsCheckedByDefault as IsDefault, m.SortOrder,
m.RequiresChildSelection as RequiresSelection,
m.MaxNumSelectionReq as MaxSelections
FROM Items m
WHERE m.BusinessID = ?
AND m.IsActive = 1
AND m.ParentItemID > 0
AND (m.CategoryID = 0 OR m.CategoryID IS NULL)
ORDER BY m.SortOrder, m.Name
", [$businessID]);
} else {
// Unified schema: Categories are Items at ParentID=0
$qCatRows = queryTimed("
SELECT DISTINCT
p.ID, p.Name, 0 AS ParentCategoryID, p.SortOrder, 0 AS MenuID
FROM Items p
INNER JOIN Items c ON c.ParentItemID = p.ID
WHERE p.BusinessID = ?
AND p.ParentItemID = 0
AND p.IsActive = 1
AND NOT EXISTS (
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = p.ID
)
ORDER BY p.SortOrder, p.Name
", [$businessID]);
$qItemRows = queryTimed("
SELECT
i.ID, i.ParentItemID as CategoryItemID, i.Name, i.Description,
i.Price, i.SortOrder, i.IsActive
FROM Items i
INNER JOIN Items cat ON cat.ID = i.ParentItemID
WHERE i.BusinessID = ?
AND i.IsActive = 1
AND cat.ParentItemID = 0
AND NOT EXISTS (
SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = cat.ID
)
ORDER BY i.SortOrder, i.Name
", [$businessID]);
$qDirectModifiers = queryTimed("
SELECT
m.ID as ItemID, m.ParentItemID, m.Name, m.Price,
m.IsCheckedByDefault as IsDefault, m.SortOrder,
m.RequiresChildSelection as RequiresSelection,
m.MaxNumSelectionReq as MaxSelections
FROM Items m
WHERE m.BusinessID = ?
AND m.IsActive = 1
AND m.ParentItemID > 0
ORDER BY m.SortOrder, m.Name
", [$businessID]);
}
// Collect menu item IDs
$menuItemIds = array_column($qItemRows, 'ID');
// Get template links for this business's menu items
$qTemplateLinkRows = [];
if (count($menuItemIds) > 0) {
$placeholders = implode(',', array_fill(0, count($menuItemIds), '?'));
$qTemplateLinkRows = queryTimed("
SELECT tl.ItemID as ParentItemID, tl.TemplateItemID, tl.SortOrder
FROM lt_ItemID_TemplateItemID tl
WHERE tl.ItemID IN ($placeholders)
ORDER BY tl.ItemID, tl.SortOrder
", $menuItemIds);
}
// Get templates for this business
$qTemplateRows = queryTimed("
SELECT DISTINCT
t.ID as ItemID, t.Name, t.Price,
t.IsCheckedByDefault as IsDefault, t.SortOrder,
t.RequiresChildSelection as RequiresSelection,
t.MaxNumSelectionReq as MaxSelections
FROM Items t
WHERE t.BusinessID = ?
AND (t.CategoryID = 0 OR t.CategoryID IS NULL)
AND t.ParentItemID = 0
AND t.IsActive = 1
ORDER BY t.SortOrder, t.Name
", [$businessID]);
$templateIds = array_column($qTemplateRows, 'ItemID');
// Get template children
$qTemplateChildRows = [];
if (count($templateIds) > 0) {
$placeholders = implode(',', array_fill(0, count($templateIds), '?'));
$qTemplateChildRows = queryTimed("
SELECT
c.ID as ItemID, c.ParentItemID, c.Name, c.Price,
c.IsCheckedByDefault as IsDefault, c.SortOrder,
c.RequiresChildSelection as RequiresSelection,
c.MaxNumSelectionReq as MaxSelections
FROM Items c
WHERE c.ParentItemID IN ($placeholders) AND c.IsActive = 1
ORDER BY c.SortOrder, c.Name
", $templateIds);
}
// Build templates lookup with options
$templatesById = [];
foreach ($qTemplateRows as $t) {
$tid = (int) $t['ItemID'];
$options = buildOptionsTree($qTemplateChildRows, $tid);
$templatesById[$tid] = [
'id' => 'mod_' . $tid,
'dbId' => $tid,
'name' => $t['Name'],
'price' => $t['Price'],
'isDefault' => (int) $t['IsDefault'] === 1,
'sortOrder' => (int) $t['SortOrder'],
'isTemplate' => true,
'requiresSelection' => (int) ($t['RequiresSelection'] ?? 0) === 1,
'maxSelections' => (int) ($t['MaxSelections'] ?? 0),
'options' => $options,
];
}
// Build template links lookup by parent ItemID
$templateLinksByItem = [];
foreach ($qTemplateLinkRows as $link) {
$parentID = (int) $link['ParentItemID'];
$templateID = (int) $link['TemplateItemID'];
if (isset($templatesById[$templateID])) {
$tmpl = $templatesById[$templateID]; // copy
$tmpl['sortOrder'] = (int) $link['SortOrder'];
$templateLinksByItem[$parentID][] = $tmpl;
}
}
// Build direct modifiers by item
$directModsByItem = [];
foreach ($menuItemIds as $itemId) {
$options = buildOptionsTree($qDirectModifiers, (int) $itemId);
if (count($options) > 0) {
$directModsByItem[(int) $itemId] = $options;
}
}
// Build items lookup by CategoryID
$itemsByCategory = [];
$uploadsDir = luceeWebroot() . '/uploads/items';
foreach ($qItemRows as $item) {
$catID = (int) $item['CategoryItemID'];
$itemID = (int) $item['ID'];
// Get template-linked modifiers
$itemModifiers = isset($templateLinksByItem[$itemID]) ? $templateLinksByItem[$itemID] : [];
// Add direct modifiers
if (isset($directModsByItem[$itemID])) {
$itemModifiers = array_merge($itemModifiers, $directModsByItem[$itemID]);
}
// Sort modifiers by sortOrder
usort($itemModifiers, fn($a, $b) => $a['sortOrder'] - $b['sortOrder']);
// Check for existing item photo
$itemImageUrl = null;
foreach (['jpg', 'jpeg', 'png', 'gif', 'webp'] as $ext) {
if (file_exists("$uploadsDir/{$itemID}.{$ext}")) {
$itemImageUrl = "/uploads/items/{$itemID}.{$ext}";
break;
}
}
$itemsByCategory[$catID][] = [
'id' => 'item_' . $itemID,
'dbId' => $itemID,
'name' => $item['Name'],
'description' => $item['Description'] ?? '',
'price' => $item['Price'],
'imageUrl' => $itemImageUrl,
'photoTaskId' => null,
'modifiers' => $itemModifiers,
'sortOrder' => (int) $item['SortOrder'],
];
}
// Build categories array
$categories = [];
$catIndex = 0;
foreach ($qCatRows as $cat) {
$catID = (int) $cat['ID'];
$catItems = $itemsByCategory[$catID] ?? [];
$catStruct = [
'id' => 'cat_' . $catID,
'dbId' => $catID,
'name' => $cat['Name'],
'description' => '',
'sortOrder' => $catIndex,
'items' => $catItems,
];
if ($hasCategoriesData) {
$catStruct['menuId'] = (int) ($cat['MenuID'] ?? 0);
$catStruct['parentCategoryId'] = (int) ($cat['ParentCategoryID'] ?? 0);
$catStruct['parentCategoryDbId'] = (int) ($cat['ParentCategoryID'] ?? 0);
}
$categories[] = $catStruct;
$catIndex++;
}
// Build template library
$templateLibrary = array_values($templatesById);
// Get brand colors
$brandColor = '';
$brandColorLight = '';
try {
$qBrand = queryOne("SELECT BrandColor, BrandColorLight FROM Businesses WHERE ID = ?", [$businessID]);
if ($qBrand) {
if (!empty($qBrand['BrandColor'])) {
$brandColor = $qBrand['BrandColor'][0] === '#' ? $qBrand['BrandColor'] : '#' . $qBrand['BrandColor'];
}
if (!empty($qBrand['BrandColorLight'])) {
$brandColorLight = $qBrand['BrandColorLight'][0] === '#' ? $qBrand['BrandColorLight'] : '#' . $qBrand['BrandColorLight'];
}
}
} catch (Exception $e) {}
$totalItems = 0;
foreach ($categories as $cat) {
$totalItems += count($cat['items']);
}
jsonResponse([
'OK' => true,
'MENU' => ['categories' => $categories],
'MENUS' => $allMenus,
'SELECTED_MENU_ID' => $menuID,
'DEFAULT_MENU_ID' => $defaultMenuID,
'TEMPLATES' => $templateLibrary,
'BRANDCOLOR' => $brandColor,
'BRANDCOLORLIGHT' => $brandColorLight,
'CATEGORY_COUNT' => count($categories),
'TEMPLATE_COUNT' => count($templateLibrary),
'MENU_COUNT' => count($allMenus),
'SCHEMA' => $hasCategoriesData ? 'legacy' : 'unified',
'ITEM_COUNT' => $totalItems,
]);
} catch (Exception $e) {
jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage(), 'DETAIL' => '']);
}