payfrit-api/api/menu/saveFromBuilder.php
John Mizerek 1f81d98c52 Initial PHP API migration from CFML
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.
2026-03-14 14:26:59 -07:00

258 lines
11 KiB
PHP

<?php
require_once __DIR__ . '/../helpers.php';
runAuth();
/**
* Save menu data from the builder UI
*
* POST body: { "BusinessID": 37, "Menu": { "categories": [...] } }
*/
// Track which templates we've already saved options for
$savedTemplates = [];
/**
* Recursively save options/modifiers at any depth.
*/
function saveOptionsRecursive(array $options, int $parentID, int $businessID): void {
$optSortOrder = 0;
foreach ($options as $opt) {
$optDbId = (int) ($opt['dbId'] ?? 0);
$requiresSelection = (!empty($opt['requiresSelection'])) ? 1 : 0;
$maxSelections = (int) ($opt['maxSelections'] ?? 0);
$isDefault = (!empty($opt['isDefault'])) ? 1 : 0;
$optionID = 0;
if ($optDbId > 0) {
$optionID = $optDbId;
queryTimed("
UPDATE Items
SET Name = ?, Price = ?, IsCheckedByDefault = ?, SortOrder = ?,
RequiresChildSelection = ?, MaxNumSelectionReq = ?, ParentItemID = ?
WHERE ID = ?
", [
$opt['name'], (float) ($opt['price'] ?? 0), $isDefault, $optSortOrder,
$requiresSelection, $maxSelections, $parentID, $optDbId,
]);
} else {
queryTimed("
INSERT INTO Items (
BusinessID, ParentItemID, Name, Price,
IsCheckedByDefault, SortOrder, IsActive, AddedOn,
RequiresChildSelection, MaxNumSelectionReq, CategoryID
) VALUES (?, ?, ?, ?, ?, ?, 1, NOW(), ?, ?, 0)
", [
$businessID, $parentID, $opt['name'], (float) ($opt['price'] ?? 0),
$isDefault, $optSortOrder, $requiresSelection, $maxSelections,
]);
$optionID = (int) lastInsertId();
}
if (!empty($opt['options']) && is_array($opt['options'])) {
saveOptionsRecursive($opt['options'], $optionID, $businessID);
}
$optSortOrder++;
}
}
$data = readJsonBody();
$businessID = (int) ($data['BusinessID'] ?? 0);
$menu = $data['Menu'] ?? [];
if ($businessID === 0) {
jsonResponse(['OK' => false, 'ERROR' => 'BusinessID is required']);
}
if (!isset($menu['categories']) || !is_array($menu['categories'])) {
jsonResponse(['OK' => false, 'ERROR' => 'Menu categories are required']);
}
try {
// Determine schema
$hasCategoriesData = false;
try {
$qCatCheck = queryOne("SELECT 1 as x FROM Categories WHERE BusinessID = ? LIMIT 1", [$businessID]);
$hasCategoriesData = $qCatCheck !== null;
} catch (Exception $e) {}
$newSchemaActive = !$hasCategoriesData;
$db = getDb();
$db->beginTransaction();
// Track JS category id -> DB categoryID for subcategory parent resolution
$jsCatIdToDbId = [];
$catSortOrder = 0;
foreach ($menu['categories'] as $cat) {
$categoryID = 0;
$categoryDbId = (int) ($cat['dbId'] ?? 0);
$categoryMenuId = (int) ($cat['menuId'] ?? 0);
// Resolve parentCategoryID
$parentCategoryID = 0;
if (!empty($cat['parentCategoryDbId']) && (int) $cat['parentCategoryDbId'] > 0) {
$parentCategoryID = (int) $cat['parentCategoryDbId'];
} elseif (!empty($cat['parentCategoryId']) && $cat['parentCategoryId'] !== '0') {
if (isset($jsCatIdToDbId[$cat['parentCategoryId']])) {
$parentCategoryID = $jsCatIdToDbId[$cat['parentCategoryId']];
}
}
if ($newSchemaActive) {
if ($categoryDbId > 0) {
$categoryID = $categoryDbId;
queryTimed("
UPDATE Items SET Name = ?, SortOrder = ?
WHERE ID = ? AND BusinessID = ?
", [$cat['name'], $catSortOrder, $categoryID, $businessID]);
} else {
queryTimed("
INSERT INTO Items (BusinessID, Name, Description, ParentItemID, Price, IsActive, SortOrder, AddedOn, CategoryID)
VALUES (?, ?, '', 0, 0, 1, ?, NOW(), 0)
", [$businessID, $cat['name'], $catSortOrder]);
$categoryID = (int) lastInsertId();
}
} else {
if ($categoryDbId > 0) {
$categoryID = $categoryDbId;
queryTimed("
UPDATE Categories
SET Name = ?, SortOrder = ?, MenuID = NULLIF(?, 0), ParentCategoryID = ?
WHERE ID = ?
", [$cat['name'], $catSortOrder, $categoryMenuId, $parentCategoryID, $categoryID]);
} else {
queryTimed("
INSERT INTO Categories (BusinessID, MenuID, Name, SortOrder, ParentCategoryID, AddedOn)
VALUES (?, NULLIF(?, 0), ?, ?, ?, NOW())
", [$businessID, $categoryMenuId, $cat['name'], $catSortOrder, $parentCategoryID]);
$categoryID = (int) lastInsertId();
}
}
// Track JS id -> DB id
if (!empty($cat['id'])) {
$jsCatIdToDbId[$cat['id']] = $categoryID;
}
// Process items
if (!empty($cat['items']) && is_array($cat['items'])) {
$itemSortOrder = 0;
foreach ($cat['items'] as $item) {
$itemID = 0;
$itemDbId = (int) ($item['dbId'] ?? 0);
if ($itemDbId > 0) {
$itemID = $itemDbId;
if ($newSchemaActive) {
queryTimed("
UPDATE Items SET Name = ?, Description = ?, Price = ?, ParentItemID = ?, SortOrder = ?
WHERE ID = ?
", [$item['name'], $item['description'] ?? '', (float) ($item['price'] ?? 0), $categoryID, $itemSortOrder, $itemID]);
} else {
queryTimed("
UPDATE Items SET Name = ?, Description = ?, Price = ?, CategoryID = ?, SortOrder = ?
WHERE ID = ?
", [$item['name'], $item['description'] ?? '', (float) ($item['price'] ?? 0), $categoryID, $itemSortOrder, $itemID]);
}
} else {
if ($newSchemaActive) {
queryTimed("
INSERT INTO Items (BusinessID, ParentItemID, Name, Description, Price, SortOrder, IsActive, AddedOn, CategoryID)
VALUES (?, ?, ?, ?, ?, ?, 1, NOW(), 0)
", [$businessID, $categoryID, $item['name'], $item['description'] ?? '', (float) ($item['price'] ?? 0), $itemSortOrder]);
} else {
queryTimed("
INSERT INTO Items (BusinessID, CategoryID, Name, Description, Price, SortOrder, IsActive, ParentItemID, AddedOn)
VALUES (?, ?, ?, ?, ?, ?, 1, 0, NOW())
", [$businessID, $categoryID, $item['name'], $item['description'] ?? '', (float) ($item['price'] ?? 0), $itemSortOrder]);
}
$itemID = (int) lastInsertId();
}
// Handle modifiers
if (!empty($item['modifiers']) && is_array($item['modifiers'])) {
// Clear existing template links for this item
queryTimed("DELETE FROM lt_ItemID_TemplateItemID WHERE ItemID = ?", [$itemID]);
$modSortOrder = 0;
foreach ($item['modifiers'] as $mod) {
$modDbId = (int) ($mod['dbId'] ?? 0);
$requiresSelection = (!empty($mod['requiresSelection'])) ? 1 : 0;
$maxSelections = (int) ($mod['maxSelections'] ?? 0);
if (!empty($mod['isTemplate']) && $modDbId > 0) {
// Template reference — create link
queryTimed("
INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE SortOrder = ?
", [$itemID, $modDbId, $modSortOrder, $modSortOrder]);
// Update template name and selection rules
queryTimed("
UPDATE Items SET Name = ?, RequiresChildSelection = ?, MaxNumSelectionReq = ?
WHERE ID = ?
", [$mod['name'], $requiresSelection, $maxSelections, $modDbId]);
// Save template options only once
if (!isset($savedTemplates[$modDbId])) {
$savedTemplates[$modDbId] = true;
if (!empty($mod['options']) && is_array($mod['options'])) {
saveOptionsRecursive($mod['options'], $modDbId, $businessID);
}
}
} elseif ($modDbId > 0) {
// Direct modifier — update
$isDefault = (!empty($mod['isDefault'])) ? 1 : 0;
queryTimed("
UPDATE Items
SET Name = ?, Price = ?, IsCheckedByDefault = ?, SortOrder = ?,
RequiresChildSelection = ?, MaxNumSelectionReq = ?, ParentItemID = ?
WHERE ID = ?
", [
$mod['name'], (float) ($mod['price'] ?? 0), $isDefault, $modSortOrder,
$requiresSelection, $maxSelections, $itemID, $modDbId,
]);
if (!empty($mod['options']) && is_array($mod['options'])) {
saveOptionsRecursive($mod['options'], $modDbId, $businessID);
}
} else {
// New direct modifier — insert
$isDefault = (!empty($mod['isDefault'])) ? 1 : 0;
queryTimed("
INSERT INTO Items (
BusinessID, ParentItemID, Name, Price,
IsCheckedByDefault, SortOrder, IsActive, AddedOn,
RequiresChildSelection, MaxNumSelectionReq, CategoryID
) VALUES (?, ?, ?, ?, ?, ?, 1, NOW(), ?, ?, 0)
", [
$businessID, $itemID, $mod['name'], (float) ($mod['price'] ?? 0),
$isDefault, $modSortOrder, $requiresSelection, $maxSelections,
]);
$newModID = (int) lastInsertId();
if (!empty($mod['options']) && is_array($mod['options'])) {
saveOptionsRecursive($mod['options'], $newModID, $businessID);
}
}
$modSortOrder++;
}
}
$itemSortOrder++;
}
}
$catSortOrder++;
}
$db->commit();
jsonResponse(['OK' => true, 'SCHEMA' => $newSchemaActive ? 'unified' : 'legacy']);
} catch (Exception $e) {
try { $db->rollBack(); } catch (Exception $re) {}
jsonResponse(['OK' => false, 'ERROR' => $e->getMessage(), 'DETAIL' => '', 'TYPE' => '']);
}