payfrit-api/api/setup/saveWizard.php
John Mizerek 280394f5e0 Move app from /opt to /var/www/payfrit-api (standard Linux web dir)
- Moved directory on both dev and biz servers
- Updated nginx configs on both servers
- Added appRoot() helper, uploadsRoot() uses it
- No more hardcoded /opt/payfrit-api paths in codebase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:32:55 -07:00

702 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++;
}
}
}
$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);