payfrit-api/api/setup/saveWizard.php
John Mizerek 5d34d8378b Fix: set ImageExtension after downloading item images in saveWizard
downloadItemImage() saved files to disk but never updated the DB column,
so items appeared to have no images despite files existing on disk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 01:57:39 -07:00

703 lines
29 KiB
PHP

<?php
require_once __DIR__ . '/../helpers.php';
runAuth();
/**
* Save Wizard Data
*
* Takes extracted menu data from the setup wizard and saves to database.
* Creates business, address, task types/categories, stations, hours,
* menu, categories (with subcategories), items with modifier template links,
* and downloads item images.
*
* POST JSON: { businessId, userId, menuId, data: { business, categories, modifiers, items }, tempFolder }
*/
set_time_limit(300);
$response = ['OK' => 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++;
$pdo->prepare("UPDATE Items SET ImageExtension = 'jpg' WHERE ID = ?")->execute([$menuItemID]);
}
}
}
$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 = appRoot() . "/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);