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' => '']); }