false, 'steps' => [], 'errors' => []]; $itemsDir = uploadsRoot() . '/items'; /** * Resize image maintaining aspect ratio, fitting within maxSize box. */ function resizeToFit($img, int $maxSize) { $w = imagesx($img); $h = imagesy($img); if ($w <= $maxSize && $h <= $maxSize) return $img; if ($w > $h) { $newW = $maxSize; $newH = (int)($h * ($maxSize / $w)); } else { $newH = $maxSize; $newW = (int)($w * ($maxSize / $h)); } $dst = imagecreatetruecolor($newW, $newH); imagecopyresampled($dst, $img, 0, 0, 0, 0, $newW, $newH, $w, $h); imagedestroy($img); return $dst; } /** * Create square thumbnail (center crop). */ function createSquareThumb($img, int $size) { $w = imagesx($img); $h = imagesy($img); // Resize so smallest dimension equals size if ($w > $h) { $newH = $size; $newW = (int)($w * ($size / $h)); } else { $newW = $size; $newH = (int)($h * ($size / $w)); } $resized = imagecreatetruecolor($newW, $newH); imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h); // Center crop to square $w2 = imagesx($resized); $h2 = imagesy($resized); $x = ($w2 > $h2) ? (int)(($w2 - $h2) / 2) : 0; $y = ($h2 > $w2) ? (int)(($h2 - $w2) / 2) : 0; $cropSize = min($w2, $h2); $thumb = imagecreatetruecolor($size, $size); imagecopyresampled($thumb, $resized, 0, 0, $x, $y, $size, $size, $cropSize, $cropSize); imagedestroy($resized); return $thumb; } /** * Download and save item image from URL in three sizes. */ function downloadItemImage(int $itemID, string $imageUrl, string $itemsDir): bool { try { $imageUrl = trim($imageUrl); if (empty($imageUrl)) return false; if (str_starts_with($imageUrl, '//')) { $imageUrl = 'https:' . $imageUrl; } elseif ($imageUrl[0] === '/') { return false; // Can't resolve relative URL } // Upsize DoorDash CDN thumbnails if (stripos($imageUrl, 'cdn4dd.com/p/') !== false && stripos($imageUrl, 'width=150') !== false) { $imageUrl = str_ireplace('width=150', 'width=600', $imageUrl); $imageUrl = str_ireplace('height=150', 'height=600', $imageUrl); } $ch = curl_init($imageUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTPHEADER => [ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ], ]); $content = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); if ($httpCode !== 200 || $content === false) return false; if (stripos($contentType, 'image') === false) return false; // Write to temp file $tempFile = "$itemsDir/temp_{$itemID}_" . uniqid() . '.tmp'; file_put_contents($tempFile, $content); $img = imagecreatefromstring($content); if (!$img) { @unlink($tempFile); return false; } // Thumbnail (128x128 square) $thumbImg = imagecreatefromstring($content); $thumb = createSquareThumb($thumbImg, 128); imagejpeg($thumb, "$itemsDir/{$itemID}_thumb.jpg", 85); imagedestroy($thumb); // Medium (400px max) $medImg = imagecreatefromstring($content); $medium = resizeToFit($medImg, 400); imagejpeg($medium, "$itemsDir/{$itemID}_medium.jpg", 85); imagedestroy($medium); // Full (1200px max) $full = resizeToFit($img, 1200); imagejpeg($full, "$itemsDir/{$itemID}.jpg", 90); imagedestroy($full); @unlink($tempFile); return true; } catch (Exception $e) { if (isset($tempFile) && file_exists($tempFile)) @unlink($tempFile); return false; } } try { $raw = file_get_contents('php://input'); if (empty($raw)) throw new Exception('No request body provided'); // Clean control characters $raw = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $raw); $data = json_decode($raw, true); if ($data === null) { $response['errors'][] = 'JSON parse failed: ' . json_last_error_msg(); jsonResponse($response); } $businessId = (int)($data['businessId'] ?? 0); $userId = (int)($data['userId'] ?? 0); $providedMenuId = (int)($data['menuId'] ?? 0); $wizardData = $data['data'] ?? []; $biz = $wizardData['business'] ?? []; // If no businessId, create a new business if ($businessId === 0) { $response['steps'][] = 'No businessId provided - creating new business'; $bizName = is_string($biz['name'] ?? null) ? $biz['name'] : ''; if (empty($bizName)) throw new Exception('Business name is required to create new business'); if ($userId === 0) throw new Exception('userId is required to create new business'); $bizPhone = is_string($biz['phone'] ?? null) ? trim($biz['phone']) : ''; $bizTaxRate = is_numeric($biz['taxRatePercent'] ?? null) ? (float)$biz['taxRatePercent'] / 100 : 0; // Brand colors (6-digit hex without #) $bizBrandColor = is_string($biz['brandColor'] ?? null) ? trim($biz['brandColor']) : ''; $bizBrandColor = ltrim($bizBrandColor, '#'); if (!preg_match('/^[0-9A-Fa-f]{6}$/', $bizBrandColor)) $bizBrandColor = ''; $bizBrandColorLight = is_string($biz['brandColorLight'] ?? null) ? trim($biz['brandColorLight']) : ''; $bizBrandColorLight = ltrim($bizBrandColorLight, '#'); if (!preg_match('/^[0-9A-Fa-f]{6}$/', $bizBrandColorLight)) $bizBrandColorLight = ''; // Address fields $addressLine1 = is_string($biz['addressLine1'] ?? null) ? trim($biz['addressLine1']) : ''; $city = is_string($biz['city'] ?? null) ? trim($biz['city']) : ''; $state = is_string($biz['state'] ?? null) ? trim($biz['state']) : ''; $zip = is_string($biz['zip'] ?? null) ? trim($biz['zip']) : ''; $city = preg_replace('/[,.\s]+$/', '', $city); // Look up state ID $stateID = 0; $response['steps'][] = "State value received: '$state' (len: " . strlen($state) . ')'; if (strlen($state)) { $qState = queryOne("SELECT ID FROM tt_States WHERE Abbreviation = ?", [strtoupper($state)]); $response['steps'][] = "State lookup for '" . strtoupper($state) . "' found " . ($qState ? '1' : '0') . ' records'; if ($qState) { $stateID = (int)$qState['ID']; $response['steps'][] = "Using stateID: $stateID"; } } // Lat/lng from Toast $bizLat = is_numeric($biz['latitude'] ?? null) ? (float)$biz['latitude'] : 0; $bizLng = is_numeric($biz['longitude'] ?? null) ? (float)$biz['longitude'] : 0; // Create address $addrParams = [ strlen($addressLine1) ? $addressLine1 : 'Address pending', strlen($city) ? $city : '', $stateID > 0 ? $stateID : null, strlen($zip) ? $zip : '', $userId, 2, // AddressTypeID $bizLat != 0 ? $bizLat : null, $bizLng != 0 ? $bizLng : null, ]; queryTimed( "INSERT INTO Addresses (Line1, City, StateID, ZIPCode, UserID, AddressTypeID, Latitude, Longitude, AddedOn) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())", $addrParams ); $addressId = (int)lastInsertId(); $response['steps'][] = "Created address record (ID: $addressId)"; // Community meal type $communityMealType = (int)($wizardData['communityMealType'] ?? 1); if ($communityMealType < 1 || $communityMealType > 2) $communityMealType = 1; // Create business queryTimed( "INSERT INTO Businesses (Name, Phone, UserID, AddressID, DeliveryZIPCodes, CommunityMealType, TaxRate, BrandColor, BrandColorLight, AddedOn) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())", [ $bizName, $bizPhone, $userId, $addressId, strlen($zip) ? $zip : '', $communityMealType, $bizTaxRate, strlen($bizBrandColor) ? $bizBrandColor : null, strlen($bizBrandColorLight) ? $bizBrandColorLight : null, ] ); $businessId = (int)lastInsertId(); $response['steps'][] = "Created new business: $bizName (ID: $businessId)"; // Link address to business queryTimed("UPDATE Addresses SET BusinessID = ? WHERE ID = ?", [$businessId, $addressId]); $response['steps'][] = 'Linked address to business'; // Default task types $defaultTaskTypes = [ ['name' => 'Call Staff', 'icon' => 'notifications', 'color' => '#9C27B0', 'description' => 'Request staff assistance'], ['name' => 'Chat With Staff', 'icon' => 'chat', 'color' => '#2196F3', 'description' => 'Open a chat conversation'], ['name' => 'Pay With Cash', 'icon' => 'payments', 'color' => '#4CAF50', 'description' => 'Request to pay with cash'], ['name' => 'Deliver to Table', 'icon' => 'restaurant', 'color' => '#FF9800', 'description' => 'Deliver completed order to table'], ['name' => 'Order Ready for Pickup', 'icon' => 'shopping_bag', 'color' => '#00BCD4', 'description' => 'Notify customer their order is ready'], ['name' => 'Deliver to Address', 'icon' => 'local_shipping', 'color' => '#795548', 'description' => 'Deliver order to customer address'], ]; foreach ($defaultTaskTypes as $sortOrder => $tt) { queryTimed( "INSERT INTO tt_TaskTypes (Name, Description, Icon, Color, BusinessID, SortOrder) VALUES (?, ?, ?, ?, ?, ?)", [$tt['name'], $tt['description'], $tt['icon'], $tt['color'], $businessId, $sortOrder + 1] ); } $response['steps'][] = 'Created 6 default task types'; // Default task categories $defaultTaskCategories = [ ['name' => 'Service Point', 'color' => '#F44336'], ['name' => 'Kitchen', 'color' => '#FF9800'], ['name' => 'Bar', 'color' => '#9C27B0'], ['name' => 'Cleaning', 'color' => '#4CAF50'], ['name' => 'Management', 'color' => '#2196F3'], ['name' => 'Delivery', 'color' => '#00BCD4'], ['name' => 'General', 'color' => '#607D8B'], ]; foreach ($defaultTaskCategories as $tc) { queryTimed( "INSERT INTO TaskCategories (BusinessID, Name, Color) VALUES (?, ?, ?)", [$businessId, $tc['name'], $tc['color']] ); } $response['steps'][] = 'Created 7 default task categories'; // Default kitchen station queryTimed( "INSERT INTO Stations (BusinessID, Name, Color, SortOrder) VALUES (?, 'Kitchen', '#FF9800', 1)", [$businessId] ); $response['steps'][] = 'Created default Kitchen station'; // Save business hours if (!empty($biz['hoursSchedule']) && is_array($biz['hoursSchedule'])) { $hoursSchedule = $biz['hoursSchedule']; $response['steps'][] = 'Processing ' . count($hoursSchedule) . ' days of hours'; foreach ($hoursSchedule as $dayData) { if (!is_array($dayData)) continue; $dayID = (int)($dayData['dayId'] ?? 0); $openTime = is_string($dayData['open'] ?? null) ? $dayData['open'] : '09:00'; $closeTime = is_string($dayData['close'] ?? null) ? $dayData['close'] : '17:00'; if (strlen($openTime) === 5) $openTime .= ':00'; if (strlen($closeTime) === 5) $closeTime .= ':00'; queryTimed( "INSERT INTO Hours (BusinessID, DayID, OpenTime, ClosingTime) VALUES (?, ?, ?, ?)", [$businessId, $dayID, $openTime, $closeTime] ); } $response['steps'][] = 'Created ' . count($hoursSchedule) . ' hours records'; } } else { // Verify existing business $qBiz = queryOne("SELECT ID, Name AS BusinessName FROM Businesses WHERE ID = ?", [$businessId]); if (!$qBiz) throw new Exception("Business not found: $businessId"); $response['steps'][] = 'Found existing business: ' . $qBiz['BusinessName']; } // Build modifier template map $modTemplates = $wizardData['modifiers'] ?? []; $templateMap = []; $response['steps'][] = 'Processing ' . count($modTemplates) . ' modifier templates...'; foreach ($modTemplates as $i => $tmpl) { $tmplName = is_string($tmpl['name'] ?? null) ? $tmpl['name'] : ''; if (empty($tmplName)) { $response['steps'][] = "Warning: Skipping modifier template with no name at index $i"; continue; } $required = !empty($tmpl['required']); $options = is_array($tmpl['options'] ?? null) ? $tmpl['options'] : []; $tmplType = is_string($tmpl['type'] ?? null) ? $tmpl['type'] : 'select'; $maxSel = ($tmplType === 'checkbox') ? 0 : 1; $response['steps'][] = "Template '$tmplName' has " . count($options) . ' options (type: ' . (is_array($options) ? 'array' : 'other') . ')'; // Check if template exists $qTmpl = queryOne( "SELECT ID FROM Items WHERE BusinessID = ? AND Name = ? AND ParentItemID = 0 AND CategoryID = 0", [$businessId, $tmplName] ); if ($qTmpl) { $templateItemID = (int)$qTmpl['ID']; $response['steps'][] = "Template exists: $tmplName (ID: $templateItemID)"; } else { queryTimed( "INSERT INTO Items (BusinessID, Name, ParentItemID, CategoryID, Price, IsActive, RequiresChildSelection, MaxNumSelectionReq, SortOrder) VALUES (?, ?, 0, 0, 0, 1, ?, ?, 0)", [$businessId, $tmplName, $required ? 1 : 0, $maxSel] ); $templateItemID = (int)lastInsertId(); $response['steps'][] = "Created template: $tmplName (ID: $templateItemID)"; } $templateMap[$tmplName] = $templateItemID; // Create/update template options $optionOrder = 1; foreach ($options as $opt) { if (!is_array($opt)) continue; if (!is_string($opt['name'] ?? null) || empty($opt['name'])) continue; $optName = $opt['name']; $optPrice = is_numeric($opt['price'] ?? null) ? (float)$opt['price'] : 0; $optSelected = !empty($opt['selected']) ? 1 : 0; $qOpt = queryOne( "SELECT ID FROM Items WHERE BusinessID = ? AND Name = ? AND ParentItemID = ?", [$businessId, $optName, $templateItemID] ); if (!$qOpt) { queryTimed( "INSERT INTO Items (BusinessID, Name, ParentItemID, CategoryID, Price, IsActive, IsCheckedByDefault, SortOrder) VALUES (?, ?, ?, 0, ?, 1, ?, ?)", [$businessId, $optName, $templateItemID, $optPrice, $optSelected, $optionOrder] ); } $optionOrder++; } } // Determine menu(s) to create $selectedMenus = $wizardData['selectedMenus'] ?? []; $isMultiMenu = !empty($selectedMenus) && count($selectedMenus) > 1; // Build list of menus to process $menusToProcess = []; if ($providedMenuId > 0) { $qName = queryOne("SELECT Name FROM Menus WHERE ID = ?", [$providedMenuId]); $menusToProcess[] = ['id' => $providedMenuId, 'name' => $qName ? $qName['Name'] : "Menu $providedMenuId"]; $response['steps'][] = "Using provided menu: " . $menusToProcess[0]['name'] . " (ID: $providedMenuId)"; } elseif ($isMultiMenu) { $menuOrder = 0; foreach ($selectedMenus as $smName) { $smName = trim($smName); if (empty($smName)) continue; $qMenu = queryOne( "SELECT ID FROM Menus WHERE BusinessID = ? AND Name = ? AND IsActive = 1", [$businessId, $smName] ); if ($qMenu) { $menusToProcess[] = ['id' => (int)$qMenu['ID'], 'name' => $smName]; $response['steps'][] = "Using existing menu: $smName (ID: {$qMenu['ID']})"; } else { queryTimed( "INSERT INTO Menus (BusinessID, Name, DaysActive, StartTime, EndTime, SortOrder, IsActive, AddedOn) VALUES (?, ?, 127, NULL, NULL, ?, 1, NOW())", [$businessId, $smName, $menuOrder] ); $newMenuId = (int)lastInsertId(); $menusToProcess[] = ['id' => $newMenuId, 'name' => $smName]; $response['steps'][] = "Created menu: $smName (ID: $newMenuId)"; } $menuOrder++; } } else { $menuName = is_string($wizardData['menuName'] ?? null) && strlen(trim($wizardData['menuName'] ?? '')) ? trim($wizardData['menuName']) : 'Main Menu'; $menuStartTime = is_string($wizardData['menuStartTime'] ?? null) ? trim($wizardData['menuStartTime']) : ''; $menuEndTime = is_string($wizardData['menuEndTime'] ?? null) ? trim($wizardData['menuEndTime']) : ''; if (strlen($menuStartTime) === 5) $menuStartTime .= ':00'; if (strlen($menuEndTime) === 5) $menuEndTime .= ':00'; // Validate menu hours against business hours if (strlen($menuStartTime) && strlen($menuEndTime)) { $qHours = queryOne( "SELECT MIN(OpenTime) AS earliestOpen, MAX(ClosingTime) AS latestClose FROM Hours WHERE BusinessID = ?", [$businessId] ); if ($qHours && $qHours['earliestOpen'] !== null && $qHours['latestClose'] !== null) { $earliestOpen = substr($qHours['earliestOpen'], 0, 8); $latestClose = substr($qHours['latestClose'], 0, 8); if ($menuStartTime < $earliestOpen || $menuEndTime > $latestClose) { throw new Exception("Menu hours ($menuStartTime - $menuEndTime) must be within business operating hours ($earliestOpen - $latestClose)"); } $response['steps'][] = "Validated menu hours against business hours ($earliestOpen - $latestClose)"; } } $qMenu = queryOne( "SELECT ID FROM Menus WHERE BusinessID = ? AND Name = ? AND IsActive = 1", [$businessId, $menuName] ); if ($qMenu) { $menuID = (int)$qMenu['ID']; if (strlen($menuStartTime) && strlen($menuEndTime)) { queryTimed("UPDATE Menus SET StartTime = ?, EndTime = ? WHERE ID = ?", [$menuStartTime, $menuEndTime, $menuID]); $response['steps'][] = "Updated existing menu: $menuName (ID: $menuID) with hours $menuStartTime - $menuEndTime"; } else { $response['steps'][] = "Using existing menu: $menuName (ID: $menuID)"; } } else { queryTimed( "INSERT INTO Menus (BusinessID, Name, DaysActive, StartTime, EndTime, SortOrder, IsActive, AddedOn) VALUES (?, ?, 127, ?, ?, 0, 1, NOW())", [$businessId, $menuName, strlen($menuStartTime) ? $menuStartTime : null, strlen($menuEndTime) ? $menuEndTime : null] ); $menuID = (int)lastInsertId(); $timeInfo = (strlen($menuStartTime) && strlen($menuEndTime)) ? " ($menuStartTime - $menuEndTime)" : ' (all day)'; $response['steps'][] = "Created menu: $menuName$timeInfo (ID: $menuID)"; } $menusToProcess[] = ['id' => $menuID, 'name' => $menuName]; } // For single menu, use the first (only) entry if (!$isMultiMenu) { $menuID = $menusToProcess[0]['id']; $menuName = $menusToProcess[0]['name']; } // Build category map $categories = $wizardData['categories'] ?? []; $categoryMap = []; $response['steps'][] = 'Processing ' . count($categories) . ' categories...'; // For multi-menu: build a map of menu name → menu ID $menuNameToId = []; foreach ($menusToProcess as $mp) { $menuNameToId[$mp['name']] = $mp['id']; } // For multi-menu: figure out which categories belong to which menu // by looking at which items reference them $allItems = $wizardData['items'] ?? []; $categoryToMenu = []; if ($isMultiMenu) { foreach ($allItems as $item) { $itemMenu = trim($item['menu'] ?? ''); $itemCat = trim($item['category'] ?? ''); if (strlen($itemMenu) && strlen($itemCat) && isset($menuNameToId[$itemMenu])) { $categoryToMenu[$itemCat] = $menuNameToId[$itemMenu]; } } } // First pass: top-level categories $catOrder = 1; foreach ($categories as $c => $cat) { $catName = is_string($cat['name'] ?? null) ? $cat['name'] : ''; if (empty($catName)) { $response['steps'][] = "Warning: Skipping category with no name at index $c"; continue; } // Skip subcategories in first pass if (!empty($cat['parentCategoryName'])) continue; // Determine which menu this category belongs to $catMenuID = $isMultiMenu ? ($categoryToMenu[$catName] ?? $menusToProcess[0]['id']) : $menuID; $qCat = queryOne( "SELECT ID FROM Categories WHERE BusinessID = ? AND Name = ? AND MenuID = ?", [$businessId, $catName, $catMenuID] ); if ($qCat) { $categoryID = (int)$qCat['ID']; $response['steps'][] = "Category exists: $catName (ID: $categoryID)"; } else { queryTimed( "INSERT INTO Categories (BusinessID, MenuID, Name, SortOrder) VALUES (?, ?, ?, ?)", [$businessId, $catMenuID, $catName, $catOrder] ); $categoryID = (int)lastInsertId(); $catMenuName = array_column($menusToProcess, 'name', 'id')[$catMenuID] ?? 'unknown'; $response['steps'][] = "Created category: $catName in menu $catMenuName (ID: $categoryID)"; } $categoryMap[$catName] = $categoryID; $catOrder++; } // Second pass: subcategories foreach ($categories as $cat) { $catName = is_string($cat['name'] ?? null) ? $cat['name'] : ''; if (empty($catName)) continue; $parentName = trim($cat['parentCategoryName'] ?? ''); if (empty($parentName)) continue; $parentCatID = $categoryMap[$parentName] ?? 0; if ($parentCatID === 0) { $response['steps'][] = "Warning: Parent category '$parentName' not found for subcategory '$catName'"; continue; } $catMenuID = $isMultiMenu ? ($categoryToMenu[$catName] ?? ($categoryToMenu[$parentName] ?? $menusToProcess[0]['id'])) : $menuID; $qCat = queryOne( "SELECT ID FROM Categories WHERE BusinessID = ? AND Name = ? AND MenuID = ? AND ParentCategoryID = ?", [$businessId, $catName, $catMenuID, $parentCatID] ); if ($qCat) { $categoryID = (int)$qCat['ID']; $response['steps'][] = "Subcategory exists: $catName under $parentName (ID: $categoryID)"; } else { queryTimed( "INSERT INTO Categories (BusinessID, MenuID, Name, ParentCategoryID, SortOrder) VALUES (?, ?, ?, ?, ?)", [$businessId, $catMenuID, $catName, $parentCatID, $catOrder] ); $categoryID = (int)lastInsertId(); $response['steps'][] = "Created subcategory: $catName under $parentName (ID: $categoryID)"; } $categoryMap[$catName] = $categoryID; $catOrder++; } // Create menu items $items = $wizardData['items'] ?? []; $response['steps'][] = 'Processing ' . count($items) . ' menu items...'; $totalItems = 0; $totalLinks = 0; $totalImages = 0; $categoryItemOrder = []; $itemIdMap = []; foreach ($items as $n => $item) { if (!is_array($item)) continue; $itemName = is_string($item['name'] ?? null) ? $item['name'] : ''; if (empty($itemName)) { $response['steps'][] = "Warning: Skipping item with no name at index $n"; continue; } $itemDesc = is_string($item['description'] ?? null) ? $item['description'] : ''; $itemPrice = is_numeric($item['price'] ?? null) ? (float)$item['price'] : 0; $itemCategory = is_string($item['category'] ?? null) ? $item['category'] : ''; $itemModifiers = is_array($item['modifiers'] ?? null) ? $item['modifiers'] : []; if (empty($itemCategory) || !isset($categoryMap[$itemCategory])) { $response['steps'][] = "Warning: Item '$itemName' has unknown category - skipping"; continue; } $categoryID = $categoryMap[$itemCategory]; // Track sort order within category if (!isset($categoryItemOrder[$itemCategory])) $categoryItemOrder[$itemCategory] = 1; $itemOrder = $categoryItemOrder[$itemCategory]++; // Check if item exists $qItem = queryOne( "SELECT ID FROM Items WHERE BusinessID = ? AND Name = ? AND CategoryID = ?", [$businessId, $itemName, $categoryID] ); if ($qItem) { $menuItemID = (int)$qItem['ID']; queryTimed( "UPDATE Items SET Description = ?, Price = ?, SortOrder = ? WHERE ID = ?", [$itemDesc, $itemPrice, $itemOrder, $menuItemID] ); } else { queryTimed( "INSERT INTO Items (BusinessID, Name, Description, ParentItemID, CategoryID, Price, IsActive, SortOrder) VALUES (?, ?, ?, 0, ?, ?, 1, ?)", [$businessId, $itemName, $itemDesc, $categoryID, $itemPrice, $itemOrder] ); $menuItemID = (int)lastInsertId(); } $totalItems++; // Track mapping for image uploads $frontendId = is_string($item['id'] ?? null) ? $item['id'] : ''; if (strlen($frontendId)) $itemIdMap[$frontendId] = $menuItemID; $itemIdMap[$itemName] = $menuItemID; // Link modifier templates $modOrder = 1; foreach ($itemModifiers as $modRef) { if (is_string($modRef)) { $modName = $modRef; } elseif (is_array($modRef) && isset($modRef['name'])) { $modName = $modRef['name']; } else { continue; } if (isset($templateMap[$modName])) { $templateItemID = $templateMap[$modName]; $qLink = queryOne( "SELECT 1 FROM lt_ItemID_TemplateItemID WHERE ItemID = ? AND TemplateItemID = ?", [$menuItemID, $templateItemID] ); if (!$qLink) { queryTimed( "INSERT INTO lt_ItemID_TemplateItemID (ItemID, TemplateItemID, SortOrder) VALUES (?, ?, ?)", [$menuItemID, $templateItemID, $modOrder] ); $totalLinks++; } $modOrder++; } } // Download item image if URL provided $itemImageUrl = is_string($item['imageUrl'] ?? null) ? trim($item['imageUrl']) : ''; if (strlen($itemImageUrl)) { if (!is_dir($itemsDir)) mkdir($itemsDir, 0755, true); if (downloadItemImage($menuItemID, $itemImageUrl, $itemsDir)) { $totalImages++; } } } $imageNote = $totalImages > 0 ? " and $totalImages images downloaded" : ''; $response['steps'][] = "Created/updated $totalItems items with $totalLinks modifier links$imageNote"; $response['OK'] = true; $response['summary'] = [ 'businessId' => $businessId, 'categoriesProcessed' => count($categories), 'templatesProcessed' => count($modTemplates), 'itemsProcessed' => $totalItems, 'linksCreated' => $totalLinks, 'imagesDownloaded' => $totalImages, 'itemIdMap' => $itemIdMap, ]; // Clean up temp folder from ZIP upload $tempFolder = is_string($data['tempFolder'] ?? null) ? trim($data['tempFolder']) : ''; if (strlen($tempFolder) && preg_match('/^[a-f0-9]{32}$/', $tempFolder)) { $tempFolderPath = "/opt/payfrit-api/temp/menu-import/$tempFolder"; if (is_dir($tempFolderPath)) { exec("rm -rf " . escapeshellarg($tempFolderPath)); $response['steps'][] = "Cleaned up temp folder: $tempFolder"; } } } catch (Exception $e) { $response['errors'][] = $e->getMessage(); } jsonResponse($response);