Upload endpoints were saving files to PHP's DOCUMENT_ROOT instead of the Lucee webroot where the Android app loads them from. Also fix verifyLoginOTP and verifyOTP to accept both UUID/OTP and uuid/otp keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
403 lines
15 KiB
PHP
403 lines
15 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 = [];
|
|
$webroot = isDev()
|
|
? '/opt/lucee/tomcat/webapps/ROOT'
|
|
: '/var/www/biz.payfrit.com';
|
|
$uploadsDir = $webroot . '/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' => '']);
|
|
}
|