Fix wizard: remove CategoryID column ref, add analysis time message

- Fixed Items table INSERT to remove CategoryID column (only exists in Categories table)
- Templates now correctly use ItemCategoryID=0 instead of incorrect CategoryID
- Added user message that analysis may take several minutes
- Updated localStorage with new businessId after save to keep user logged in
- Redirect to visual menu builder with new businessId in URL

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-15 15:08:53 -08:00
parent 261bab2bb6
commit d34d88e46a
2 changed files with 478 additions and 71 deletions

View file

@ -33,29 +33,120 @@ try {
data = deserializeJSON(requestBody);
businessId = structKeyExists(data, "businessId") ? val(data.businessId) : 0;
userId = structKeyExists(data, "userId") ? val(data.userId) : 0;
wizardData = structKeyExists(data, "data") ? data.data : {};
if (businessId == 0) {
throw(message="businessId is required");
}
// Verify business exists
qBiz = queryExecute("
SELECT BusinessID, BusinessName FROM Businesses WHERE BusinessID = :id
", { id: businessId }, { datasource: "payfrit" });
if (qBiz.recordCount == 0) {
throw(message="Business not found: " & businessId);
}
response.steps.append("Found business: " & qBiz.BusinessName);
// Update business info if provided
biz = structKeyExists(wizardData, "business") ? wizardData.business : {};
if (structKeyExists(biz, "name") && len(biz.name)) {
// Optionally update business name and other info
// For now we'll skip updating existing business - just add menu
response.steps.append("Business info available (not updating existing)");
// If no businessId, create a new business
if (businessId == 0) {
response.steps.append("No businessId provided - creating new business");
if (!structKeyExists(biz, "name") || !len(biz.name)) {
throw(message="Business name is required to create new business");
}
if (userId == 0) {
throw(message="userId is required to create new business");
}
// Create address record first (use extracted address fields)
addressLine1 = structKeyExists(biz, "addressLine1") ? biz.addressLine1 : "";
city = structKeyExists(biz, "city") ? biz.city : "";
state = structKeyExists(biz, "state") ? biz.state : "";
zip = structKeyExists(biz, "zip") ? biz.zip : "";
// Look up state ID from state abbreviation
stateID = 0;
if (len(state)) {
qState = queryExecute("
SELECT tt_StateID FROM tt_States WHERE tt_StateAbbreviation = :abbr
", { abbr: uCase(state) }, { datasource: "payfrit" });
if (qState.recordCount > 0) {
stateID = qState.tt_StateID;
}
}
queryExecute("
INSERT INTO Addresses (AddressLine1, AddressCity, AddressStateID, AddressZIPCode, AddressUserID, AddressTypeID, AddressAddedOn)
VALUES (:line1, :city, :stateID, :zip, :userID, :typeID, NOW())
", {
line1: len(addressLine1) ? addressLine1 : "Address pending",
city: len(city) ? city : "",
stateID: stateID,
zip: len(zip) ? zip : "",
userID: userId,
typeID: 2
}, { datasource: "payfrit" });
qNewAddr = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
addressId = qNewAddr.id;
response.steps.append("Created address record (ID: " & addressId & ")");
// Create new business with address link
queryExecute("
INSERT INTO Businesses (BusinessName, BusinessUserID, BusinessAddressID, BusinessDeliveryZipCodes, BusinessAddedOn)
VALUES (:name, :userId, :addressId, :deliveryZips, NOW())
", {
name: biz.name,
userId: userId,
addressId: addressId,
deliveryZips: len(zip) ? zip : ""
}, { datasource: "payfrit" });
qNewBiz = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
businessId = qNewBiz.id;
response.steps.append("Created new business: " & biz.name & " (ID: " & businessId & ")");
// Update address with business ID link
queryExecute("
UPDATE Addresses SET AddressBusinessID = :businessID WHERE AddressID = :addressID
", {
businessID: businessId,
addressID: addressId
}, { datasource: "payfrit" });
response.steps.append("Linked address to business");
// Save business hours from structured schedule
if (structKeyExists(biz, "hoursSchedule") && isArray(biz.hoursSchedule)) {
hoursSchedule = biz.hoursSchedule;
response.steps.append("Processing " & arrayLen(hoursSchedule) & " days of hours");
for (i = 1; i <= arrayLen(hoursSchedule); i++) {
dayData = hoursSchedule[i];
dayID = structKeyExists(dayData, "dayId") ? val(dayData.dayId) : 0;
openTime = structKeyExists(dayData, "open") ? dayData.open : "09:00";
closeTime = structKeyExists(dayData, "close") ? dayData.close : "17:00";
// Convert HH:MM to HH:MM:SS if needed
if (len(openTime) == 5) openTime = openTime & ":00";
if (len(closeTime) == 5) closeTime = closeTime & ":00";
// Insert hours record for this day
queryExecute("
INSERT INTO Hours (HoursBusinessID, HoursDayID, HoursOpenTime, HoursClosingTime)
VALUES (:bizID, :dayID, :openTime, :closeTime)
", {
bizID: businessId,
dayID: dayID,
openTime: openTime,
closeTime: closeTime
}, { datasource: "payfrit" });
}
response.steps.append("Created " & arrayLen(hoursSchedule) & " hours records");
}
} else {
// Verify existing business exists
qBiz = queryExecute("
SELECT BusinessID, BusinessName FROM Businesses WHERE BusinessID = :id
", { id: businessId }, { datasource: "payfrit" });
if (qBiz.recordCount == 0) {
throw(message="Business not found: " & businessId);
}
response.steps.append("Found existing business: " & qBiz.BusinessName);
}
// Build modifier template map
@ -77,21 +168,21 @@ try {
WHERE i.ItemBusinessID = :bizID
AND i.ItemName = :name
AND i.ItemParentItemID = 0
AND i.ItemIsCollapsible = 1
AND i.ItemCategoryID = 0
", { bizID: businessId, name: tmplName }, { datasource: "payfrit" });
if (qTmpl.recordCount > 0) {
templateItemID = qTmpl.ItemID;
response.steps.append("Template exists: " & tmplName & " (ID: " & templateItemID & ")");
} else {
// Create template as Item with ItemIsCollapsible=1 to mark as template
// Create template as Item with ItemCategoryID=0 to mark as template
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
ItemBusinessID, ItemName, ItemParentItemID, ItemCategoryID, ItemPrice,
ItemIsActive, ItemRequiresChildSelection, ItemMaxNumSelectionReq,
ItemSortOrder, ItemIsCollapsible
ItemSortOrder
) VALUES (
:bizID, :name, 0, 0, 1, :required, 1, 0, 1
:bizID, :name, 0, 0, 0, 1, :required, 1, 0
)
", {
bizID: businessId,
@ -109,6 +200,11 @@ try {
// Create/update template options
optionOrder = 1;
for (opt in options) {
// Safety check: ensure opt is a struct with a name
if (!isStruct(opt) || !structKeyExists(opt, "name") || !len(opt.name)) {
continue;
}
optName = opt.name;
optPrice = structKeyExists(opt, "price") ? val(opt.price) : 0;
@ -120,10 +216,10 @@ try {
if (qOpt.recordCount == 0) {
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder
ItemBusinessID, ItemName, ItemParentItemID, ItemCategoryID,
ItemPrice, ItemIsActive, ItemSortOrder
) VALUES (
:bizID, :name, :parentID, :price, 1, :sortOrder
:bizID, :name, :parentID, 0, :price, 1, :sortOrder
)
", {
bizID: businessId,
@ -139,7 +235,7 @@ try {
// Build category map
categories = structKeyExists(wizardData, "categories") ? wizardData.categories : [];
categoryMap = {}; // Maps category name to ItemID
categoryMap = {}; // Maps category name to CategoryID
response.steps.append("Processing " & arrayLen(categories) & " categories...");
@ -147,25 +243,22 @@ try {
for (cat in categories) {
catName = cat.name;
// Check if category exists (Item at ParentID=0, not a template)
// Check if category exists in Categories table
qCat = queryExecute("
SELECT ItemID FROM Items
WHERE ItemBusinessID = :bizID
AND ItemName = :name
AND ItemParentItemID = 0
AND (ItemIsCollapsible IS NULL OR ItemIsCollapsible = 0)
SELECT CategoryID FROM Categories
WHERE CategoryBusinessID = :bizID AND CategoryName = :name
", { bizID: businessId, name: catName }, { datasource: "payfrit" });
if (qCat.recordCount > 0) {
categoryItemID = qCat.ItemID;
response.steps.append("Category exists: " & catName);
categoryID = qCat.CategoryID;
response.steps.append("Category exists: " & catName & " (ID: " & categoryID & ")");
} else {
// Create category in Categories table
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
ItemIsActive, ItemSortOrder, ItemIsCollapsible
INSERT INTO Categories (
CategoryBusinessID, CategoryName, CategorySortOrder
) VALUES (
:bizID, :name, 0, 0, 1, :sortOrder, 0
:bizID, :name, :sortOrder
)
", {
bizID: businessId,
@ -174,11 +267,11 @@ try {
}, { datasource: "payfrit" });
qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
categoryItemID = qNewCat.id;
response.steps.append("Created category: " & catName & " (ID: " & categoryItemID & ")");
categoryID = qNewCat.id;
response.steps.append("Created category: " & catName & " (ID: " & categoryID & ")");
}
categoryMap[catName] = categoryItemID;
categoryMap[catName] = categoryID;
catOrder++;
}
@ -206,7 +299,7 @@ try {
continue;
}
categoryItemID = categoryMap[itemCategory];
categoryID = categoryMap[itemCategory];
// Track sort order within category
if (!structKeyExists(categoryItemOrder, itemCategory)) {
@ -218,8 +311,10 @@ try {
// Check if item exists
qItem = queryExecute("
SELECT ItemID FROM Items
WHERE ItemBusinessID = :bizID AND ItemName = :name AND ItemParentItemID = :parentID
", { bizID: businessId, name: itemName, parentID: categoryItemID }, { datasource: "payfrit" });
WHERE ItemBusinessID = :bizID
AND ItemName = :name
AND ItemCategoryID = :catID
", { bizID: businessId, name: itemName, catID: categoryID }, { datasource: "payfrit" });
if (qItem.recordCount > 0) {
menuItemID = qItem.ItemID;
@ -240,15 +335,15 @@ try {
queryExecute("
INSERT INTO Items (
ItemBusinessID, ItemName, ItemDescription, ItemParentItemID,
ItemPrice, ItemIsActive, ItemSortOrder
ItemCategoryID, ItemPrice, ItemIsActive, ItemSortOrder
) VALUES (
:bizID, :name, :desc, :parentID, :price, 1, :sortOrder
:bizID, :name, :desc, 0, :catID, :price, 1, :sortOrder
)
", {
bizID: businessId,
name: itemName,
desc: itemDesc,
parentID: categoryItemID,
catID: categoryID,
price: itemPrice,
sortOrder: itemOrder
}, { datasource: "payfrit" });

View file

@ -651,8 +651,8 @@
<!-- Wizard Header -->
<div class="wizard-header">
<h1>Let's Set Up Your Menu</h1>
<p>Upload your menu images and I'll extract all the information for you</p>
<h1>Let's Setup Your Menu</h1>
<p>Upload your menu images or PDFs and I'll extract then input all the information for you to preview!</p>
</div>
<!-- Upload Section -->
@ -747,9 +747,9 @@
});
function initializeConfig() {
// Get business ID from URL or localStorage
// Get business ID from URL or localStorage (optional for new business setup)
const urlParams = new URLSearchParams(window.location.search);
config.businessId = urlParams.get('bid') || localStorage.getItem('payfrit_portal_business');
config.businessId = urlParams.get('bid') || localStorage.getItem('payfrit_portal_business') || null;
// Determine API base URL
const basePath = window.location.pathname.includes('/biz.payfrit.com/')
@ -757,9 +757,16 @@
: '';
config.apiBaseUrl = basePath + '/api';
if (!config.businessId) {
// Check if user is logged in
const userId = localStorage.getItem('payfrit_portal_userid');
if (!userId) {
window.location.href = 'login.html';
return;
}
config.userId = userId;
// BusinessId is optional - will be created if not provided
console.log('Wizard initialized. BusinessId:', config.businessId, 'UserId:', config.userId);
}
function setupUploadZone() {
@ -899,7 +906,7 @@
addMessage('ai', `
<div class="loading-indicator">
<div class="loading-spinner"></div>
<span>Analyzing ${config.uploadedFiles.length} menu image${config.uploadedFiles.length > 1 ? 's' : ''}...</span>
<span>Analyzing ${config.uploadedFiles.length} menu image${config.uploadedFiles.length > 1 ? 's' : ''}... This may take several minutes, please be patient.</span>
</div>
`);
@ -925,6 +932,21 @@
// Store extracted data
config.extractedData = result.DATA;
// Debug: Log raw API response
console.log('=== RAW API RESPONSE ===');
console.log('Full result:', result);
if (result.DEBUG_RAW_RESULTS) {
console.log('DEBUG_RAW_RESULTS:', result.DEBUG_RAW_RESULTS);
result.DEBUG_RAW_RESULTS.forEach((imgResult, i) => {
console.log(`Image ${i} raw result:`, imgResult);
if (imgResult.modifiers) {
console.log(` Raw modifiers from image ${i}:`, imgResult.modifiers);
}
});
}
console.log('Merged DATA:', result.DATA);
console.log('========================');
// Remove loading message and start conversation flow
document.getElementById('conversation').innerHTML = '';
@ -949,28 +971,248 @@
document.getElementById('uploadSection').style.display = 'block';
}
// Helper function to parse hours string into 7-day schedule
function parseHoursString(hoursText) {
const schedule = [];
const dayMap = {
'monday': 0, 'mon': 0,
'tuesday': 1, 'tue': 1, 'tues': 1,
'wednesday': 2, 'wed': 2,
'thursday': 3, 'thu': 3, 'thur': 3, 'thurs': 3,
'friday': 4, 'fri': 4,
'saturday': 5, 'sat': 5,
'sunday': 6, 'sun': 6
};
// Initialize all days as closed
for (let i = 0; i < 7; i++) {
schedule.push({ open: '09:00', close: '17:00', closed: true });
}
if (!hoursText || !hoursText.trim()) {
return schedule;
}
hoursText = hoursText.toLowerCase();
// Extract time range - find first time pattern
const timePattern = /(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?\s*(?:to|[-])\s*(\d{1,2}):?(\d{2})?\s*(am|pm|a\.m\.|p\.m\.)?/i;
const timeMatch = hoursText.match(timePattern);
let openTime = '09:00';
let closeTime = '17:00';
if (timeMatch) {
const convertTo24Hour = (hour, minute, ampm) => {
let h = parseInt(hour);
const m = minute ? parseInt(minute) : 0;
if (ampm) {
ampm = ampm.replace(/\./g, '').toLowerCase();
if (ampm === 'pm' && h < 12) h += 12;
if (ampm === 'am' && h === 12) h = 0;
}
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
};
openTime = convertTo24Hour(timeMatch[1], timeMatch[2], timeMatch[3]);
closeTime = convertTo24Hour(timeMatch[4], timeMatch[5], timeMatch[6]);
}
// Find which days are mentioned
const dayPattern = /(mon|monday|tue|tuesday|tues|wed|wednesday|thu|thursday|thur|thurs|fri|friday|sat|saturday|sun|sunday)/gi;
const dayMatches = hoursText.match(dayPattern) || [];
if (dayMatches.length === 0) {
// No days mentioned, assume all days open
for (let i = 0; i < 7; i++) {
schedule[i] = { open: openTime, close: closeTime, closed: false };
}
return schedule;
}
// Process day ranges and individual days
const daysSet = new Set();
let i = 0;
while (i < dayMatches.length) {
const currentDay = dayMatches[i].toLowerCase();
const currentDayIdx = dayMap[currentDay];
// Check if next day forms a range
if (i < dayMatches.length - 1) {
const nextDay = dayMatches[i + 1].toLowerCase();
const betweenPattern = new RegExp(currentDay + '\\s*[-]\\s*' + nextDay, 'i');
if (betweenPattern.test(hoursText)) {
// It's a range
const nextDayIdx = dayMap[nextDay];
for (let d = currentDayIdx; d <= nextDayIdx; d++) {
daysSet.add(d);
}
i += 2;
continue;
}
}
// Individual day
daysSet.add(currentDayIdx);
i++;
}
// Apply times to the days found
daysSet.forEach(dayIdx => {
schedule[dayIdx] = { open: openTime, close: closeTime, closed: false };
});
return schedule;
}
// Toggle day closed/open
function toggleDayClosed(dayIdx) {
const closedCheckbox = document.getElementById(`closed_${dayIdx}`);
const openInput = document.getElementById(`open_${dayIdx}`);
const closeInput = document.getElementById(`close_${dayIdx}`);
if (closedCheckbox.checked) {
openInput.disabled = true;
closeInput.disabled = true;
} else {
openInput.disabled = false;
closeInput.disabled = false;
}
}
// Step 1: Business Info
function showBusinessInfoStep() {
updateProgress(2);
const biz = config.extractedData.business || {};
console.log('Business data:', biz);
// Parse address into components if it's a single string
let addressLine1 = biz.addressLine1 || '';
let city = biz.city || '';
let state = biz.state || '';
let zip = biz.zip || '';
if (biz.address && !addressLine1) {
console.log('Parsing address:', biz.address);
// Parse from the end forward: ZIP (5 digits), then State (2 letters), then City + AddressLine1
let remaining = biz.address.trim();
// Extract ZIP (last 5 digits, optionally with -4)
const zipMatch = remaining.match(/\b(\d{5})(?:-\d{4})?\s*$/);
if (zipMatch) {
zip = zipMatch[1];
remaining = remaining.substring(0, zipMatch.index).trim();
console.log('Found ZIP:', zip, 'Remaining:', remaining);
}
// Extract State (2 letters before ZIP)
const stateMatch = remaining.match(/\b([A-Z]{2})\s*$/i);
if (stateMatch) {
state = stateMatch[1].toUpperCase();
remaining = remaining.substring(0, stateMatch.index).trim();
console.log('Found State:', state, 'Remaining:', remaining);
}
// What's left is AddressLine1 + City
// Try to split by comma first
if (remaining.includes(',')) {
const parts = remaining.split(',').map(p => p.trim());
addressLine1 = parts[0];
city = parts.slice(1).join(', ');
} else {
// No comma - try to find last sequence of words as city
// Cities are usually 1-3 words at the end
const words = remaining.split(/\s+/);
if (words.length > 3) {
// Assume last 2 words are city, rest is address
city = words.slice(-2).join(' ');
addressLine1 = words.slice(0, -2).join(' ');
} else if (words.length > 1) {
// Assume last word is city
city = words[words.length - 1];
addressLine1 = words.slice(0, -1).join(' ');
} else {
// All in address line 1
addressLine1 = remaining;
}
}
}
console.log('Parsed address:', { addressLine1, city, state, zip });
// Parse hours into a 7-day schedule
const hoursSchedule = parseHoursString(biz.hours || '');
addMessage('ai', `
<p>I found your restaurant information:</p>
<div class="extracted-value editable">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Restaurant Name</label>
<input type="text" id="bizName" value="${biz.name || ''}" placeholder="Restaurant name">
<input type="text" id="bizName" value="${biz.name || ''}" placeholder="Restaurant name" required>
</div>
<div class="extracted-value editable">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Address</label>
<input type="text" id="bizAddress" value="${biz.address || ''}" placeholder="Address">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Address Line 1</label>
<input type="text" id="bizAddressLine1" value="${addressLine1}" placeholder="123 Main St">
</div>
<div class="extracted-value editable">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">City</label>
<input type="text" id="bizCity" value="${city}" placeholder="Los Angeles">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="extracted-value editable">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">State</label>
<input type="text" id="bizState" value="${state}" placeholder="CA" maxlength="2">
</div>
<div class="extracted-value editable">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">ZIP Code</label>
<input type="text" id="bizZip" value="${zip}" placeholder="90001">
</div>
</div>
<div class="extracted-value editable">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Phone</label>
<input type="text" id="bizPhone" value="${biz.phone || ''}" placeholder="Phone number">
</div>
<div class="extracted-value editable">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Hours</label>
<input type="text" id="bizHours" value="${biz.hours || ''}" placeholder="Business hours">
<label style="font-size:12px;color:var(--gray-500);display:block;margin-bottom:4px;">Business Hours</label>
<table style="width:100%;border-collapse:collapse;margin-top:8px;">
<thead>
<tr style="border-bottom:2px solid var(--gray-300);">
<th style="text-align:left;padding:8px;font-size:12px;color:var(--gray-600);">Day</th>
<th style="text-align:left;padding:8px;font-size:12px;color:var(--gray-600);">Open</th>
<th style="text-align:left;padding:8px;font-size:12px;color:var(--gray-600);">Close</th>
<th style="text-align:center;padding:8px;font-size:12px;color:var(--gray-600);">Closed</th>
</tr>
</thead>
<tbody>
${['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'].map((day, idx) => {
const dayData = hoursSchedule[idx];
return `
<tr style="border-bottom:1px solid var(--gray-200);">
<td style="padding:8px;font-weight:500;">${day}</td>
<td style="padding:8px;">
<input type="time" id="open_${idx}" value="${dayData.open}"
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;"
${dayData.closed ? 'disabled' : ''}>
</td>
<td style="padding:8px;">
<input type="time" id="close_${idx}" value="${dayData.close}"
style="padding:4px 8px;border:1px solid var(--gray-300);border-radius:4px;font-size:14px;"
${dayData.closed ? 'disabled' : ''}>
</td>
<td style="padding:8px;text-align:center;">
<input type="checkbox" id="closed_${idx}" ${dayData.closed ? 'checked' : ''}
onchange="toggleDayClosed(${idx})"
style="width:18px;height:18px;cursor:pointer;">
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
<p>Is this information correct?</p>
<div class="action-buttons">
@ -985,12 +1227,32 @@
}
function confirmBusinessInfo() {
// Collect hours from the table
const hoursSchedule = [];
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
for (let i = 0; i < 7; i++) {
const openTime = document.getElementById(`open_${i}`).value;
const closeTime = document.getElementById(`close_${i}`).value;
const isClosed = document.getElementById(`closed_${i}`).checked;
hoursSchedule.push({
day: dayNames[i],
dayId: i + 1, // 1=Monday, 7=Sunday
open: isClosed ? openTime : openTime, // If closed, both times will be the same
close: isClosed ? openTime : closeTime // Set close = open when closed
});
}
// Update stored data with any edits
config.extractedData.business = {
name: document.getElementById('bizName').value,
address: document.getElementById('bizAddress').value,
addressLine1: document.getElementById('bizAddressLine1').value,
city: document.getElementById('bizCity').value,
state: document.getElementById('bizState').value,
zip: document.getElementById('bizZip').value,
phone: document.getElementById('bizPhone').value,
hours: document.getElementById('bizHours').value
hoursSchedule: hoursSchedule // Send the structured schedule instead of the raw hours string
};
// Move to categories
@ -1115,6 +1377,22 @@
updateProgress(4);
const modifiers = config.extractedData.modifiers || [];
// Debug: Log the raw modifier data
console.log('=== MODIFIERS DEBUG ===');
console.log('Total modifiers:', modifiers.length);
modifiers.forEach((mod, i) => {
console.log(`Modifier ${i}:`, mod);
console.log(` - name:`, mod.name);
console.log(` - options type:`, typeof mod.options);
console.log(` - options:`, mod.options);
if (mod.options && mod.options.length > 0) {
mod.options.forEach((opt, j) => {
console.log(` Option ${j}:`, opt, `(type: ${typeof opt})`);
});
}
});
console.log('======================');
if (modifiers.length === 0) {
addMessage('ai', `
<p>I didn't detect any modifier templates (like size options, add-ons, etc.).</p>
@ -1131,11 +1409,11 @@
<div class="modifier-template">
<div class="modifier-header">
<input type="checkbox" checked data-index="${i}">
<span class="modifier-name">${mod.name}</span>
<span class="modifier-name">${mod.name || 'Unnamed'}</span>
<span class="modifier-type">${mod.required ? 'Required' : 'Optional'}</span>
</div>
<div class="modifier-options">
${(mod.options || []).map(opt => `
${(mod.options || []).filter(opt => opt && opt.name).map(opt => `
<span class="modifier-option">
${opt.name}${opt.price ? `<span class="price">+$${opt.price.toFixed(2)}</span>` : ''}
</span>
@ -1299,6 +1577,9 @@
}
async function saveMenu() {
console.log('=== SAVE MENU CALLED ===');
console.log('Data to save:', config.extractedData);
const saveBtn = document.querySelector('#finalActions .btn-success');
const originalText = saveBtn.innerHTML;
saveBtn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;border-width:2px;"></div> Saving...';
@ -1311,22 +1592,53 @@
'Content-Type': 'application/json'
},
body: JSON.stringify({
businessId: config.businessId,
businessId: config.businessId || 0,
userId: config.userId,
data: config.extractedData
})
});
const result = await response.json();
console.log('HTTP Status:', response.status, response.statusText);
const responseText = await response.text();
console.log('Raw response:', responseText);
let result;
try {
result = JSON.parse(responseText);
} catch (e) {
console.error('Failed to parse JSON response:', e);
throw new Error('Invalid JSON response from server: ' + responseText.substring(0, 200));
}
console.log('=== SAVE RESPONSE ===');
console.log('Full result:', result);
if (result.errors && result.errors.length > 0) {
console.error('Backend errors:', result.errors);
}
if (result.steps && result.steps.length > 0) {
console.log('Backend steps:', result.steps);
}
console.log('====================');
if (!result.OK) {
throw new Error(result.MESSAGE || 'Save failed');
const errorMsg = result.errors && result.errors.length > 0
? result.errors.join('; ')
: (result.MESSAGE || 'Save failed');
throw new Error(errorMsg);
}
showToast('Menu saved successfully!', 'success');
// Redirect to menu page after a moment
// Use the businessId from the response (in case it was newly created)
const finalBusinessId = result.summary?.businessId || config.businessId;
// Update localStorage with the new business ID to keep user logged in
localStorage.setItem('payfrit_portal_business', finalBusinessId);
// Redirect to visual menu builder after a moment
setTimeout(() => {
window.location.href = `index.html?bid=${config.businessId}#menu`;
window.location.href = `index.html?bid=${finalBusinessId}#menu`;
}, 1500);
} catch (error) {