MySQL PDO returns DECIMAL/FLOAT columns as strings, causing json_encode
to emit them as JSON strings ("9.99") instead of numbers (9.99).
Android's Gson Map parsing fails the as? Number cast on strings,
defaulting to 0.0. Cast Price to (float) before building the response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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' => (float) $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' => (float) $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(),
|
|
]);
|
|
}
|