- 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>
702 lines
29 KiB
PHP
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);
|