Complete port of all 163 API endpoints from Lucee/CFML to PHP 8.3. Shared helpers in api/helpers.php (DB, auth, request/response, security). PDO prepared statements throughout. Same JSON response shapes as CFML.
489 lines
20 KiB
PHP
489 lines
20 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../helpers.php';
|
|
runAuth();
|
|
|
|
/**
|
|
* Get Menu Items for Customer Apps
|
|
*
|
|
* POST body:
|
|
* {
|
|
* "BusinessID": 37,
|
|
* "OrderTypeID": 0, // optional: 1=Dine-In, 2=Takeaway, 3=Delivery
|
|
* "MenuID": 0 // optional: filter to specific menu
|
|
* }
|
|
*/
|
|
|
|
$data = readJsonBody();
|
|
$businessID = (int) ($data['BusinessID'] ?? 0);
|
|
$orderTypeID = (int) ($data['OrderTypeID'] ?? 0);
|
|
$requestedMenuID = (int) ($data['MenuID'] ?? 0);
|
|
|
|
if ($businessID <= 0) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'missing_businessid', 'MESSAGE' => 'BusinessID is required.', 'DETAIL' => '']);
|
|
}
|
|
|
|
// Get business timezone for schedule filtering
|
|
$businessTimezone = 'America/Los_Angeles';
|
|
try {
|
|
$qTz = queryOne("SELECT Timezone FROM Businesses WHERE ID = ?", [$businessID]);
|
|
if ($qTz && !empty($qTz['Timezone'])) {
|
|
$businessTimezone = $qTz['Timezone'];
|
|
}
|
|
} catch (Exception $e) {
|
|
// Column might not exist yet, use default
|
|
}
|
|
|
|
$currentTime = getTimeInZone($businessTimezone);
|
|
$currentDayID = getDayInZone($businessTimezone);
|
|
|
|
$menuList = [];
|
|
|
|
try {
|
|
// Check if new schema is active (BusinessID column exists and has data)
|
|
$newSchemaActive = false;
|
|
try {
|
|
$qCheck = queryOne("SELECT COUNT(*) as cnt FROM Items WHERE BusinessID = ? AND BusinessID > 0", [$businessID]);
|
|
$newSchemaActive = $qCheck && (int) $qCheck['cnt'] > 0;
|
|
} catch (Exception $e) {
|
|
$newSchemaActive = false;
|
|
}
|
|
|
|
$rows = [];
|
|
$qCategories = null;
|
|
|
|
if ($newSchemaActive) {
|
|
// Check if Categories table has data for this business
|
|
$hasCategoriesData = false;
|
|
try {
|
|
$qCatCheck = queryOne("SELECT COUNT(*) as cnt FROM Categories WHERE BusinessID = ?", [$businessID]);
|
|
$hasCategoriesData = $qCatCheck && (int) $qCatCheck['cnt'] > 0;
|
|
} catch (Exception $e) {
|
|
$hasCategoriesData = false;
|
|
}
|
|
|
|
if ($hasCategoriesData) {
|
|
// Get active menus
|
|
$activeMenuIds = '';
|
|
try {
|
|
$qAllMenus = queryTimed("
|
|
SELECT ID, Name FROM Menus
|
|
WHERE BusinessID = ? AND IsActive = 1
|
|
ORDER BY SortOrder, Name
|
|
", [$businessID]);
|
|
|
|
if ($requestedMenuID > 0) {
|
|
$activeMenuIds = (string) $requestedMenuID;
|
|
} else {
|
|
$ids = array_column($qAllMenus, 'ID');
|
|
$activeMenuIds = implode(',', $ids);
|
|
}
|
|
|
|
foreach ($qAllMenus as $m) {
|
|
$menuList[] = ['MenuID' => (int) $m['ID'], 'Name' => $m['Name']];
|
|
}
|
|
} catch (Exception $e) {
|
|
// Menus table might not exist yet
|
|
}
|
|
|
|
// Build category query with schedule/channel/menu filtering
|
|
$catParams = [$businessID, $orderTypeID, $orderTypeID, $currentTime, $currentTime, $currentDayID];
|
|
$menuClause = '';
|
|
if (strlen($activeMenuIds) > 0) {
|
|
// Build safe IN clause for menu IDs
|
|
$menuIdArr = array_map('intval', explode(',', $activeMenuIds));
|
|
$menuPlaceholders = implode(',', $menuIdArr);
|
|
$menuClause = "OR MenuID IN ($menuPlaceholders)";
|
|
}
|
|
|
|
$qCategories = queryTimed("
|
|
SELECT
|
|
ID, Name, SortOrder, OrderTypes, ParentCategoryID,
|
|
ScheduleStart, ScheduleEnd, ScheduleDays, MenuID
|
|
FROM Categories
|
|
WHERE BusinessID = ?
|
|
AND (? = 0 OR FIND_IN_SET(?, OrderTypes) > 0)
|
|
AND (
|
|
ScheduleStart IS NULL
|
|
OR ScheduleEnd IS NULL
|
|
OR (TIME(?) >= ScheduleStart AND TIME(?) <= ScheduleEnd)
|
|
)
|
|
AND (
|
|
ScheduleDays IS NULL
|
|
OR ScheduleDays = ''
|
|
OR FIND_IN_SET(?, ScheduleDays) > 0
|
|
)
|
|
AND (
|
|
MenuID IS NULL
|
|
OR MenuID = 0
|
|
$menuClause
|
|
)
|
|
ORDER BY SortOrder
|
|
", $catParams);
|
|
|
|
// Get visible category IDs
|
|
$visibleCategoryIds = array_column($qCategories, 'ID');
|
|
$visibleStr = count($visibleCategoryIds) > 0
|
|
? implode(',', array_map('intval', $visibleCategoryIds))
|
|
: '0';
|
|
|
|
// Get menu items for visible categories
|
|
$q = queryTimed("
|
|
SELECT
|
|
i.ID,
|
|
i.CategoryID,
|
|
c.Name AS CategoryName,
|
|
i.Name AS ItemName,
|
|
i.Description,
|
|
i.ParentItemID,
|
|
i.Price,
|
|
i.IsActive,
|
|
i.IsCheckedByDefault,
|
|
i.RequiresChildSelection,
|
|
i.MaxNumSelectionReq,
|
|
i.IsCollapsible,
|
|
i.SortOrder,
|
|
i.StationID,
|
|
s.Name AS StationName,
|
|
s.Color,
|
|
c.MenuID
|
|
FROM Items i
|
|
LEFT JOIN Categories c ON c.ID = i.CategoryID
|
|
LEFT JOIN Stations s ON s.ID = i.StationID
|
|
WHERE i.BusinessID = ?
|
|
AND i.IsActive = 1
|
|
AND (i.CategoryID IN ($visibleStr) OR (i.CategoryID = 0 AND i.ParentItemID > 0))
|
|
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID)
|
|
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl2 WHERE tl2.TemplateItemID = i.ParentItemID)
|
|
AND NOT (i.ParentItemID = 0 AND i.CategoryID = 0 AND i.Price = 0)
|
|
ORDER BY COALESCE(c.SortOrder, 999), i.SortOrder, i.ID
|
|
", [$businessID]);
|
|
|
|
} else {
|
|
// Fallback: Derive categories from parent Items
|
|
$q = queryTimed("
|
|
SELECT
|
|
i.ID,
|
|
CASE
|
|
WHEN i.ParentItemID = 0 AND i.IsCollapsible = 0 THEN i.ID
|
|
ELSE COALESCE(
|
|
(SELECT cat.ID FROM Items cat
|
|
WHERE cat.ID = i.ParentItemID
|
|
AND cat.ParentItemID = 0
|
|
AND cat.IsCollapsible = 0),
|
|
0
|
|
)
|
|
END as CategoryID,
|
|
CASE
|
|
WHEN i.ParentItemID = 0 AND i.IsCollapsible = 0 THEN i.Name
|
|
ELSE COALESCE(
|
|
(SELECT cat.Name FROM Items cat
|
|
WHERE cat.ID = i.ParentItemID
|
|
AND cat.ParentItemID = 0
|
|
AND cat.IsCollapsible = 0),
|
|
''
|
|
)
|
|
END as CategoryName,
|
|
i.Name AS ItemName,
|
|
i.Description,
|
|
i.ParentItemID,
|
|
i.Price,
|
|
i.IsActive,
|
|
i.IsCheckedByDefault,
|
|
i.RequiresChildSelection,
|
|
i.MaxNumSelectionReq,
|
|
i.IsCollapsible,
|
|
i.SortOrder,
|
|
i.StationID,
|
|
s.Name AS StationName,
|
|
s.Color
|
|
FROM Items i
|
|
LEFT JOIN Stations s ON s.ID = i.StationID
|
|
WHERE i.BusinessID = ?
|
|
AND i.IsActive = 1
|
|
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl WHERE tl.TemplateItemID = i.ID)
|
|
AND NOT EXISTS (SELECT 1 FROM lt_ItemID_TemplateItemID tl2 WHERE tl2.TemplateItemID = i.ParentItemID)
|
|
AND (
|
|
i.ParentItemID > 0
|
|
OR (i.ParentItemID = 0 AND i.IsCollapsible = 0)
|
|
)
|
|
ORDER BY i.ParentItemID, i.SortOrder, i.ID
|
|
", [$businessID]);
|
|
}
|
|
} else {
|
|
// OLD SCHEMA: Use Categories table
|
|
$q = queryTimed("
|
|
SELECT
|
|
i.ID,
|
|
i.CategoryID,
|
|
c.Name AS CategoryName,
|
|
i.Name AS ItemName,
|
|
i.Description,
|
|
i.ParentItemID,
|
|
i.Price,
|
|
i.IsActive,
|
|
i.IsCheckedByDefault,
|
|
i.RequiresChildSelection,
|
|
i.MaxNumSelectionReq,
|
|
i.IsCollapsible,
|
|
i.SortOrder,
|
|
i.StationID,
|
|
s.Name AS StationName,
|
|
s.Color
|
|
FROM Items i
|
|
INNER JOIN Categories c ON c.ID = i.CategoryID
|
|
LEFT JOIN Stations s ON s.ID = i.StationID
|
|
WHERE c.BusinessID = ?
|
|
ORDER BY i.ParentItemID, i.SortOrder, i.ID
|
|
", [$businessID]);
|
|
}
|
|
|
|
// Build set of category IDs that have items
|
|
$categoriesWithItems = [];
|
|
foreach ($q as $row) {
|
|
if ((int) $row['CategoryID'] > 0) {
|
|
$categoriesWithItems[(int) $row['CategoryID']] = true;
|
|
}
|
|
}
|
|
|
|
// Build category ID set for parent remapping
|
|
$categoryIdSet = [];
|
|
if ($qCategories !== null) {
|
|
// Mark parent categories of subcategories that have items
|
|
foreach ($qCategories as $cat) {
|
|
if ((int) ($cat['ParentCategoryID'] ?? 0) > 0 && isset($categoriesWithItems[(int) $cat['ID']])) {
|
|
$categoriesWithItems[(int) $cat['ParentCategoryID']] = true;
|
|
}
|
|
}
|
|
foreach ($qCategories as $cat) {
|
|
$categoryIdSet[(int) $cat['ID']] = true;
|
|
}
|
|
|
|
// Add category headers as virtual parent items
|
|
foreach ($qCategories as $cat) {
|
|
if (isset($categoriesWithItems[(int) $cat['ID']])) {
|
|
$rows[] = [
|
|
'ItemID' => (int) $cat['ID'],
|
|
'CategoryID' => (int) $cat['ID'],
|
|
'Name' => $cat['Name'],
|
|
'Description' => '',
|
|
'ParentItemID' => 0,
|
|
'ParentCategoryID' => (int) ($cat['ParentCategoryID'] ?? 0),
|
|
'Price' => 0,
|
|
'IsActive' => 1,
|
|
'IsCheckedByDefault' => 0,
|
|
'RequiresChildSelection' => 0,
|
|
'MaxNumSelectionReq' => 0,
|
|
'IsCollapsible' => 0,
|
|
'SortOrder' => (int) $cat['SortOrder'],
|
|
'MenuID' => (int) ($cat['MenuID'] ?? 0),
|
|
'StationID' => '',
|
|
'ItemName' => '',
|
|
'ItemColor' => '',
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process items
|
|
foreach ($q as $row) {
|
|
$effectiveParentID = (int) $row['ParentItemID'];
|
|
|
|
if ($newSchemaActive && $qCategories !== null && (int) $row['CategoryID'] > 0) {
|
|
if ($effectiveParentID === 0) {
|
|
$effectiveParentID = (int) $row['CategoryID'];
|
|
} elseif (isset($categoryIdSet[$effectiveParentID])) {
|
|
// Parent IS a category ID — correct
|
|
} elseif (!isset($categoryIdSet[$effectiveParentID])) {
|
|
$effectiveParentID = (int) $row['CategoryID'];
|
|
}
|
|
}
|
|
|
|
$itemMenuID = (int) ($row['MenuID'] ?? 0);
|
|
$itemName = $row['ItemName'] ?? $row['Name'] ?? '';
|
|
$catName = $row['CategoryName'] ?? $row['Name'] ?? '';
|
|
|
|
$rows[] = [
|
|
'ItemID' => (int) $row['ID'],
|
|
'CategoryID' => (int) $row['CategoryID'],
|
|
'Name' => strlen(trim($itemName)) > 0 ? $itemName : $catName,
|
|
'Description' => $row['Description'] ?? '',
|
|
'ParentItemID' => $effectiveParentID,
|
|
'Price' => $row['Price'],
|
|
'IsActive' => (int) $row['IsActive'],
|
|
'IsCheckedByDefault' => (int) $row['IsCheckedByDefault'],
|
|
'RequiresChildSelection' => (int) $row['RequiresChildSelection'],
|
|
'MaxNumSelectionReq' => (int) $row['MaxNumSelectionReq'],
|
|
'IsCollapsible' => (int) $row['IsCollapsible'],
|
|
'SortOrder' => (int) $row['SortOrder'],
|
|
'MenuID' => $itemMenuID,
|
|
'StationID' => !empty($row['StationID']) ? $row['StationID'] : '',
|
|
'ItemName' => strlen(trim($catName)) > 0 ? $catName : '',
|
|
'ItemColor' => !empty($row['Color']) ? $row['Color'] : '',
|
|
];
|
|
}
|
|
|
|
// Add template-linked modifiers as virtual children (unified schema only)
|
|
if ($newSchemaActive) {
|
|
$qTemplateLinks = queryTimed("
|
|
SELECT
|
|
tl.ItemID as MenuItemID,
|
|
tmpl.ID as TemplateItemID,
|
|
tmpl.Name as TemplateName,
|
|
tmpl.Description as TemplateDescription,
|
|
tmpl.RequiresChildSelection as TemplateRequired,
|
|
tmpl.MaxNumSelectionReq as TemplateMaxSelections,
|
|
tmpl.IsCollapsible as TemplateIsCollapsible,
|
|
tl.SortOrder as TemplateSortOrder
|
|
FROM lt_ItemID_TemplateItemID tl
|
|
INNER JOIN Items tmpl ON tmpl.ID = tl.TemplateItemID AND tmpl.IsActive = 1
|
|
INNER JOIN Items menuItem ON menuItem.ID = tl.ItemID
|
|
WHERE menuItem.BusinessID = ?
|
|
AND menuItem.IsActive = 1
|
|
ORDER BY tl.ItemID, tl.SortOrder
|
|
", [$businessID]);
|
|
|
|
$qTemplateOptions = queryTimed("
|
|
SELECT DISTINCT
|
|
opt.ID as OptionItemID,
|
|
opt.ParentItemID as TemplateItemID,
|
|
opt.Name as OptionName,
|
|
opt.Description as OptionDescription,
|
|
opt.Price as OptionPrice,
|
|
opt.IsCheckedByDefault as OptionIsDefault,
|
|
opt.SortOrder as OptionSortOrder
|
|
FROM Items opt
|
|
INNER JOIN lt_ItemID_TemplateItemID tl ON tl.TemplateItemID = opt.ParentItemID
|
|
INNER JOIN Items menuItem ON menuItem.ID = tl.ItemID
|
|
WHERE menuItem.BusinessID = ?
|
|
AND menuItem.IsActive = 1
|
|
AND opt.IsActive = 1
|
|
ORDER BY opt.ParentItemID, opt.SortOrder
|
|
", [$businessID]);
|
|
|
|
// Build template options map: templateID -> [options]
|
|
$templateOptionsMap = [];
|
|
foreach ($qTemplateOptions as $opt) {
|
|
$tid = (int) $opt['TemplateItemID'];
|
|
$templateOptionsMap[$tid][] = [
|
|
'ItemID' => (int) $opt['OptionItemID'],
|
|
'Name' => $opt['OptionName'],
|
|
'Description' => $opt['OptionDescription'] ?? '',
|
|
'Price' => $opt['OptionPrice'],
|
|
'IsCheckedByDefault' => (int) $opt['OptionIsDefault'],
|
|
'SortOrder' => (int) $opt['OptionSortOrder'],
|
|
];
|
|
}
|
|
|
|
// Add templates and options as virtual children
|
|
$addedTemplates = [];
|
|
foreach ($qTemplateLinks as $link) {
|
|
$menuItemID = (int) $link['MenuItemID'];
|
|
$templateID = (int) $link['TemplateItemID'];
|
|
$linkKey = "{$menuItemID}_{$templateID}";
|
|
|
|
if (isset($addedTemplates[$linkKey])) continue;
|
|
$addedTemplates[$linkKey] = true;
|
|
|
|
$virtualTemplateID = $menuItemID * 100000 + $templateID;
|
|
|
|
// Template as modifier group
|
|
$rows[] = [
|
|
'ItemID' => $virtualTemplateID,
|
|
'CategoryID' => 0,
|
|
'Name' => $link['TemplateName'],
|
|
'Description' => $link['TemplateDescription'] ?? '',
|
|
'ParentItemID' => $menuItemID,
|
|
'Price' => 0,
|
|
'IsActive' => 1,
|
|
'IsCheckedByDefault' => 0,
|
|
'RequiresChildSelection' => (int) $link['TemplateRequired'],
|
|
'MaxNumSelectionReq' => (int) $link['TemplateMaxSelections'],
|
|
'IsCollapsible' => (int) $link['TemplateIsCollapsible'],
|
|
'SortOrder' => (int) $link['TemplateSortOrder'],
|
|
'StationID' => '',
|
|
'ItemName' => '',
|
|
'ItemColor' => '',
|
|
];
|
|
|
|
// Template options
|
|
if (isset($templateOptionsMap[$templateID])) {
|
|
foreach ($templateOptionsMap[$templateID] as $opt) {
|
|
$virtualOptionID = $menuItemID * 100000 + $opt['ItemID'];
|
|
$rows[] = [
|
|
'ItemID' => $virtualOptionID,
|
|
'CategoryID' => 0,
|
|
'Name' => $opt['Name'],
|
|
'Description' => $opt['Description'],
|
|
'ParentItemID' => $virtualTemplateID,
|
|
'Price' => $opt['Price'],
|
|
'IsActive' => 1,
|
|
'IsCheckedByDefault' => $opt['IsCheckedByDefault'],
|
|
'RequiresChildSelection' => 0,
|
|
'MaxNumSelectionReq' => 0,
|
|
'IsCollapsible' => 0,
|
|
'SortOrder' => $opt['SortOrder'],
|
|
'StationID' => '',
|
|
'ItemName' => '',
|
|
'ItemColor' => '',
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get brand color, tax rate, payfrit fee, header image
|
|
$qBrand = queryOne("
|
|
SELECT BrandColor, BrandColorLight, TaxRate, PayfritFee,
|
|
HeaderImageExtension, SessionEnabled, OrderTypes
|
|
FROM Businesses WHERE ID = ?
|
|
", [$businessID]);
|
|
|
|
if (!$qBrand) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'business_not_found']);
|
|
}
|
|
|
|
if (!is_numeric($qBrand['TaxRate'])) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'business_tax_rate_not_configured']);
|
|
}
|
|
if (!is_numeric($qBrand['PayfritFee']) || (float) $qBrand['PayfritFee'] <= 0) {
|
|
apiAbort(['OK' => false, 'ERROR' => 'business_payfrit_fee_not_configured']);
|
|
}
|
|
|
|
$brandColor = '';
|
|
if (!empty($qBrand['BrandColor'])) {
|
|
$brandColor = $qBrand['BrandColor'][0] === '#' ? $qBrand['BrandColor'] : '#' . $qBrand['BrandColor'];
|
|
}
|
|
$brandColorLight = '';
|
|
if (!empty($qBrand['BrandColorLight'])) {
|
|
$brandColorLight = $qBrand['BrandColorLight'][0] === '#' ? $qBrand['BrandColorLight'] : '#' . $qBrand['BrandColorLight'];
|
|
}
|
|
$headerImageUrl = '';
|
|
if (!empty($qBrand['HeaderImageExtension'])) {
|
|
$headerImageUrl = "/uploads/headers/{$businessID}.{$qBrand['HeaderImageExtension']}";
|
|
}
|
|
|
|
jsonResponse([
|
|
'OK' => true,
|
|
'ERROR' => '',
|
|
'Items' => $rows,
|
|
'COUNT' => count($rows),
|
|
'SCHEMA' => $newSchemaActive ? 'unified' : 'legacy',
|
|
'BRANDCOLOR' => $brandColor,
|
|
'BRANDCOLORLIGHT' => $brandColorLight,
|
|
'HEADERIMAGEURL' => $headerImageUrl,
|
|
'TAXRATE' => (float) $qBrand['TaxRate'],
|
|
'PAYFRITFEE' => (float) $qBrand['PayfritFee'],
|
|
'SESSIONENABLED' => (int) ($qBrand['SessionEnabled'] ?? 0),
|
|
'Menus' => $menuList,
|
|
'SelectedMenuID' => $requestedMenuID,
|
|
'ORDERTYPES' => !empty($qBrand['OrderTypes']) ? $qBrand['OrderTypes'] : '1',
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
jsonResponse([
|
|
'OK' => false,
|
|
'ERROR' => 'server_error',
|
|
'MESSAGE' => 'DB error loading items',
|
|
'DETAIL' => $e->getMessage(),
|
|
]);
|
|
}
|