Add menu setup wizard with Claude Vision integration
- New setup-wizard.html: conversational wizard for uploading menu images - analyzeMenuImages.cfm: sends images to Claude API, returns structured menu data - saveWizard.cfm: saves extracted menu to database (categories, items, modifiers) - Added Setup Wizard button to portal Menu page - Added .gitignore for config files with secrets Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d61d883a2
commit
8d5c0cc6ac
5 changed files with 2100 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Config files with secrets
|
||||
config/claude.json
|
||||
|
||||
# Temp files
|
||||
*.tmp
|
||||
*.bak
|
||||
272
api/setup/analyzeMenuImages.cfm
Normal file
272
api/setup/analyzeMenuImages.cfm
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
/**
|
||||
* Analyze Menu Images using Claude Vision API
|
||||
*
|
||||
* Accepts uploaded menu images (JPG, PNG, PDF) and sends them to Claude
|
||||
* to extract structured menu data including:
|
||||
* - Business info (name, address, phone, hours)
|
||||
* - Categories
|
||||
* - Modifier templates
|
||||
* - Menu items with prices and descriptions
|
||||
*
|
||||
* Returns structured JSON for the setup wizard to display
|
||||
*/
|
||||
|
||||
response = { "OK": false };
|
||||
|
||||
// Claude API key - should be in environment or config
|
||||
CLAUDE_API_KEY = application.claudeApiKey ?: "";
|
||||
|
||||
// If not in application scope, try to read from config
|
||||
if (!len(CLAUDE_API_KEY)) {
|
||||
try {
|
||||
configPath = expandPath("/biz.payfrit.com/config/claude.json");
|
||||
if (fileExists(configPath)) {
|
||||
configData = deserializeJSON(fileRead(configPath));
|
||||
CLAUDE_API_KEY = configData.apiKey ?: "";
|
||||
}
|
||||
} catch (any e) {
|
||||
// Config file doesn't exist or is invalid
|
||||
}
|
||||
}
|
||||
|
||||
if (!len(CLAUDE_API_KEY)) {
|
||||
response.MESSAGE = "Claude API key not configured";
|
||||
writeOutput(serializeJSON(response));
|
||||
abort;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for uploaded files
|
||||
uploadedFiles = [];
|
||||
|
||||
// Get form fields for file uploads (file0, file1, file2, etc.)
|
||||
formFields = form.keyArray();
|
||||
for (fieldName in formFields) {
|
||||
if (reFindNoCase("^file\d+$", fieldName) && len(form[fieldName])) {
|
||||
// This is a file field
|
||||
arrayAppend(uploadedFiles, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
if (arrayLen(uploadedFiles) == 0) {
|
||||
throw(message="No files uploaded");
|
||||
}
|
||||
|
||||
// Process uploaded files - convert to base64
|
||||
imageDataArray = [];
|
||||
|
||||
// Create temp directory for uploads if needed
|
||||
uploadDir = getTempDirectory() & "payfrit_menu_uploads/";
|
||||
if (!directoryExists(uploadDir)) {
|
||||
directoryCreate(uploadDir);
|
||||
}
|
||||
|
||||
for (fieldName in uploadedFiles) {
|
||||
// Upload the file
|
||||
uploadResult = fileUpload(
|
||||
uploadDir,
|
||||
fieldName,
|
||||
"image/jpeg,image/png,image/gif,image/webp,application/pdf",
|
||||
"makeunique"
|
||||
);
|
||||
|
||||
if (uploadResult.fileWasSaved) {
|
||||
filePath = uploadResult.serverDirectory & "/" & uploadResult.serverFile;
|
||||
fileExt = lCase(uploadResult.serverFileExt);
|
||||
|
||||
// Read file and convert to base64
|
||||
fileContent = fileReadBinary(filePath);
|
||||
base64Content = toBase64(fileContent);
|
||||
|
||||
// Determine media type
|
||||
mediaType = "image/jpeg";
|
||||
if (fileExt == "png") mediaType = "image/png";
|
||||
else if (fileExt == "gif") mediaType = "image/gif";
|
||||
else if (fileExt == "webp") mediaType = "image/webp";
|
||||
else if (fileExt == "pdf") mediaType = "application/pdf";
|
||||
|
||||
arrayAppend(imageDataArray, {
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": mediaType,
|
||||
"data": base64Content
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up temp file
|
||||
fileDelete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (arrayLen(imageDataArray) == 0) {
|
||||
throw(message="No valid images could be processed");
|
||||
}
|
||||
|
||||
// Build the prompt for Claude
|
||||
systemPrompt = "You are an expert at extracting structured menu data from restaurant menu images. Your task is to analyze menu images and return the data in a specific JSON format. Be thorough and extract ALL items, categories, and any modifier patterns you can identify.";
|
||||
|
||||
userPrompt = "Please analyze these menu images and extract all the information into the following JSON structure:
|
||||
|
||||
{
|
||||
""business"": {
|
||||
""name"": ""Restaurant name if visible"",
|
||||
""address"": ""Full address if visible"",
|
||||
""phone"": ""Phone number(s) if visible"",
|
||||
""hours"": ""Business hours if visible""
|
||||
},
|
||||
""categories"": [
|
||||
{
|
||||
""name"": ""Category name"",
|
||||
""itemCount"": 0
|
||||
}
|
||||
],
|
||||
""modifiers"": [
|
||||
{
|
||||
""name"": ""Modifier template name (e.g., 'Size', 'Protein Choice', 'Toast Type')"",
|
||||
""required"": true or false,
|
||||
""options"": [
|
||||
{ ""name"": ""Option name"", ""price"": 0.00 }
|
||||
]
|
||||
}
|
||||
],
|
||||
""items"": [
|
||||
{
|
||||
""name"": ""Item name"",
|
||||
""description"": ""Item description if any"",
|
||||
""price"": 0.00,
|
||||
""category"": ""Category this item belongs to"",
|
||||
""modifiers"": [""Name of applicable modifier template""]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Important guidelines:
|
||||
1. Extract EVERY menu item you can see, with accurate prices
|
||||
2. Group items into their categories as shown on the menu
|
||||
3. Look for modifier patterns like:
|
||||
- Size options (Small/Regular/Large)
|
||||
- Protein choices (Chicken/Beef/Shrimp)
|
||||
- Bread choices (Wheat/Sourdough/White/Rye)
|
||||
- Add-ons with prices (Add bacon +$2.00)
|
||||
- Required choices (served with choice of side)
|
||||
4. For modifiers that appear on multiple items, create a reusable template
|
||||
5. Assign the appropriate modifier templates to each item
|
||||
6. If a price has a '+' it's usually an upcharge modifier option
|
||||
7. Pay attention to category headers and section dividers
|
||||
|
||||
Return ONLY the JSON object, no other text.";
|
||||
|
||||
// Build the messages array with images
|
||||
messagesContent = [];
|
||||
|
||||
// Add each image
|
||||
for (imgData in imageDataArray) {
|
||||
arrayAppend(messagesContent, imgData);
|
||||
}
|
||||
|
||||
// Add the text prompt
|
||||
arrayAppend(messagesContent, {
|
||||
"type": "text",
|
||||
"text": userPrompt
|
||||
});
|
||||
|
||||
// Call Claude API
|
||||
httpService = new http();
|
||||
httpService.setMethod("POST");
|
||||
httpService.setUrl("https://api.anthropic.com/v1/messages");
|
||||
httpService.addParam(type="header", name="Content-Type", value="application/json");
|
||||
httpService.addParam(type="header", name="x-api-key", value=CLAUDE_API_KEY);
|
||||
httpService.addParam(type="header", name="anthropic-version", value="2023-06-01");
|
||||
|
||||
requestBody = {
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"max_tokens": 8192,
|
||||
"system": systemPrompt,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": messagesContent
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
httpService.addParam(type="body", value=serializeJSON(requestBody));
|
||||
|
||||
httpResult = httpService.send().getPrefix();
|
||||
|
||||
if (httpResult.statusCode != "200 OK" && httpResult.statusCode != 200) {
|
||||
throw(message="Claude API error: " & httpResult.statusCode, detail=httpResult.fileContent);
|
||||
}
|
||||
|
||||
// Parse Claude's response
|
||||
claudeResponse = deserializeJSON(httpResult.fileContent);
|
||||
|
||||
if (!structKeyExists(claudeResponse, "content") || !arrayLen(claudeResponse.content)) {
|
||||
throw(message="Empty response from Claude");
|
||||
}
|
||||
|
||||
// Extract the text content
|
||||
responseText = "";
|
||||
for (block in claudeResponse.content) {
|
||||
if (block.type == "text") {
|
||||
responseText = block.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the JSON from Claude's response
|
||||
// Claude might wrap it in markdown code blocks, so strip those
|
||||
responseText = trim(responseText);
|
||||
if (left(responseText, 7) == "```json") {
|
||||
responseText = mid(responseText, 8, len(responseText) - 7);
|
||||
}
|
||||
if (left(responseText, 3) == "```") {
|
||||
responseText = mid(responseText, 4, len(responseText) - 3);
|
||||
}
|
||||
if (right(responseText, 3) == "```") {
|
||||
responseText = left(responseText, len(responseText) - 3);
|
||||
}
|
||||
responseText = trim(responseText);
|
||||
|
||||
extractedData = deserializeJSON(responseText);
|
||||
|
||||
// Update category item counts
|
||||
if (structKeyExists(extractedData, "categories") && structKeyExists(extractedData, "items")) {
|
||||
for (i = 1; i <= arrayLen(extractedData.categories); i++) {
|
||||
catName = extractedData.categories[i].name;
|
||||
itemCount = 0;
|
||||
for (item in extractedData.items) {
|
||||
if (structKeyExists(item, "category") && item.category == catName) {
|
||||
itemCount++;
|
||||
}
|
||||
}
|
||||
extractedData.categories[i].itemCount = itemCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Add unique IDs to items
|
||||
if (structKeyExists(extractedData, "items")) {
|
||||
for (i = 1; i <= arrayLen(extractedData.items); i++) {
|
||||
extractedData.items[i].id = "item_" & i;
|
||||
}
|
||||
}
|
||||
|
||||
response.OK = true;
|
||||
response.DATA = extractedData;
|
||||
response.imagesProcessed = arrayLen(imageDataArray);
|
||||
|
||||
} catch (any e) {
|
||||
response.MESSAGE = e.message;
|
||||
if (len(e.detail)) {
|
||||
response.DETAIL = e.detail;
|
||||
}
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
309
api/setup/saveWizard.cfm
Normal file
309
api/setup/saveWizard.cfm
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfsetting requesttimeout="300">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfscript>
|
||||
/**
|
||||
* Save Wizard Data
|
||||
*
|
||||
* Takes the extracted menu data from the setup wizard and saves it to the database.
|
||||
* This transforms the wizard format into the format expected by the import system.
|
||||
*
|
||||
* POST JSON:
|
||||
* {
|
||||
* "businessId": "existing-business-id",
|
||||
* "data": {
|
||||
* "business": { "name": "...", "address": "...", "phone": "...", "hours": "..." },
|
||||
* "categories": [ { "name": "...", "itemCount": 0 } ],
|
||||
* "modifiers": [ { "name": "...", "required": true, "options": [...] } ],
|
||||
* "items": [ { "name": "...", "price": 0, "category": "...", "modifiers": [...] } ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
response = { "OK": false, "steps": [], "errors": [] };
|
||||
|
||||
try {
|
||||
requestBody = toString(getHttpRequestData().content);
|
||||
if (!len(requestBody)) {
|
||||
throw(message="No request body provided");
|
||||
}
|
||||
|
||||
data = deserializeJSON(requestBody);
|
||||
|
||||
businessId = structKeyExists(data, "businessId") ? val(data.businessId) : 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)");
|
||||
}
|
||||
|
||||
// Build modifier template map
|
||||
// The wizard format has modifiers as simple objects, we need to create IDs
|
||||
modTemplates = structKeyExists(wizardData, "modifiers") ? wizardData.modifiers : [];
|
||||
templateMap = {}; // Maps modifier name to database ItemID
|
||||
|
||||
response.steps.append("Processing " & arrayLen(modTemplates) & " modifier templates...");
|
||||
|
||||
for (i = 1; i <= arrayLen(modTemplates); i++) {
|
||||
tmpl = modTemplates[i];
|
||||
tmplName = tmpl.name;
|
||||
required = structKeyExists(tmpl, "required") && tmpl.required == true;
|
||||
options = structKeyExists(tmpl, "options") ? tmpl.options : [];
|
||||
|
||||
// Check if template already exists for this business
|
||||
qTmpl = queryExecute("
|
||||
SELECT i.ItemID FROM Items i
|
||||
WHERE i.ItemBusinessID = :bizID
|
||||
AND i.ItemName = :name
|
||||
AND i.ItemParentItemID = 0
|
||||
AND i.ItemIsCollapsible = 1
|
||||
", { 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
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
|
||||
ItemIsActive, ItemRequiresChildSelection, ItemMaxNumSelectionReq,
|
||||
ItemSortOrder, ItemIsCollapsible
|
||||
) VALUES (
|
||||
:bizID, :name, 0, 0, 1, :required, 1, 0, 1
|
||||
)
|
||||
", {
|
||||
bizID: businessId,
|
||||
name: tmplName,
|
||||
required: required ? 1 : 0
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
qNewTmpl = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||
templateItemID = qNewTmpl.id;
|
||||
response.steps.append("Created template: " & tmplName & " (ID: " & templateItemID & ")");
|
||||
}
|
||||
|
||||
templateMap[tmplName] = templateItemID;
|
||||
|
||||
// Create/update template options
|
||||
optionOrder = 1;
|
||||
for (opt in options) {
|
||||
optName = opt.name;
|
||||
optPrice = structKeyExists(opt, "price") ? val(opt.price) : 0;
|
||||
|
||||
qOpt = queryExecute("
|
||||
SELECT ItemID FROM Items
|
||||
WHERE ItemBusinessID = :bizID AND ItemName = :name AND ItemParentItemID = :parentID
|
||||
", { bizID: businessId, name: optName, parentID: templateItemID }, { datasource: "payfrit" });
|
||||
|
||||
if (qOpt.recordCount == 0) {
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
|
||||
ItemIsActive, ItemSortOrder
|
||||
) VALUES (
|
||||
:bizID, :name, :parentID, :price, 1, :sortOrder
|
||||
)
|
||||
", {
|
||||
bizID: businessId,
|
||||
name: optName,
|
||||
parentID: templateItemID,
|
||||
price: optPrice,
|
||||
sortOrder: optionOrder
|
||||
}, { datasource: "payfrit" });
|
||||
}
|
||||
optionOrder++;
|
||||
}
|
||||
}
|
||||
|
||||
// Build category map
|
||||
categories = structKeyExists(wizardData, "categories") ? wizardData.categories : [];
|
||||
categoryMap = {}; // Maps category name to ItemID
|
||||
|
||||
response.steps.append("Processing " & arrayLen(categories) & " categories...");
|
||||
|
||||
catOrder = 1;
|
||||
for (cat in categories) {
|
||||
catName = cat.name;
|
||||
|
||||
// Check if category exists (Item at ParentID=0, not a template)
|
||||
qCat = queryExecute("
|
||||
SELECT ItemID FROM Items
|
||||
WHERE ItemBusinessID = :bizID
|
||||
AND ItemName = :name
|
||||
AND ItemParentItemID = 0
|
||||
AND (ItemIsCollapsible IS NULL OR ItemIsCollapsible = 0)
|
||||
", { bizID: businessId, name: catName }, { datasource: "payfrit" });
|
||||
|
||||
if (qCat.recordCount > 0) {
|
||||
categoryItemID = qCat.ItemID;
|
||||
response.steps.append("Category exists: " & catName);
|
||||
} else {
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemName, ItemParentItemID, ItemPrice,
|
||||
ItemIsActive, ItemSortOrder, ItemIsCollapsible
|
||||
) VALUES (
|
||||
:bizID, :name, 0, 0, 1, :sortOrder, 0
|
||||
)
|
||||
", {
|
||||
bizID: businessId,
|
||||
name: catName,
|
||||
sortOrder: catOrder
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
qNewCat = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||
categoryItemID = qNewCat.id;
|
||||
response.steps.append("Created category: " & catName & " (ID: " & categoryItemID & ")");
|
||||
}
|
||||
|
||||
categoryMap[catName] = categoryItemID;
|
||||
catOrder++;
|
||||
}
|
||||
|
||||
// Create menu items
|
||||
items = structKeyExists(wizardData, "items") ? wizardData.items : [];
|
||||
|
||||
response.steps.append("Processing " & arrayLen(items) & " menu items...");
|
||||
|
||||
totalItems = 0;
|
||||
totalLinks = 0;
|
||||
|
||||
// Track item order within each category
|
||||
categoryItemOrder = {};
|
||||
|
||||
for (item in items) {
|
||||
itemName = item.name;
|
||||
itemDesc = structKeyExists(item, "description") ? item.description : "";
|
||||
itemPrice = structKeyExists(item, "price") ? val(item.price) : 0;
|
||||
itemCategory = structKeyExists(item, "category") ? item.category : "";
|
||||
itemModifiers = structKeyExists(item, "modifiers") ? item.modifiers : [];
|
||||
|
||||
// Get category ID
|
||||
if (!len(itemCategory) || !structKeyExists(categoryMap, itemCategory)) {
|
||||
response.steps.append("Warning: Item '" & itemName & "' has unknown category '" & itemCategory & "' - skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
categoryItemID = categoryMap[itemCategory];
|
||||
|
||||
// Track sort order within category
|
||||
if (!structKeyExists(categoryItemOrder, itemCategory)) {
|
||||
categoryItemOrder[itemCategory] = 1;
|
||||
}
|
||||
itemOrder = categoryItemOrder[itemCategory];
|
||||
categoryItemOrder[itemCategory]++;
|
||||
|
||||
// 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" });
|
||||
|
||||
if (qItem.recordCount > 0) {
|
||||
menuItemID = qItem.ItemID;
|
||||
// Update existing item
|
||||
queryExecute("
|
||||
UPDATE Items SET
|
||||
ItemDescription = :desc,
|
||||
ItemPrice = :price,
|
||||
ItemSortOrder = :sortOrder
|
||||
WHERE ItemID = :id
|
||||
", {
|
||||
desc: itemDesc,
|
||||
price: itemPrice,
|
||||
sortOrder: itemOrder,
|
||||
id: menuItemID
|
||||
}, { datasource: "payfrit" });
|
||||
} else {
|
||||
queryExecute("
|
||||
INSERT INTO Items (
|
||||
ItemBusinessID, ItemName, ItemDescription, ItemParentItemID,
|
||||
ItemPrice, ItemIsActive, ItemSortOrder
|
||||
) VALUES (
|
||||
:bizID, :name, :desc, :parentID, :price, 1, :sortOrder
|
||||
)
|
||||
", {
|
||||
bizID: businessId,
|
||||
name: itemName,
|
||||
desc: itemDesc,
|
||||
parentID: categoryItemID,
|
||||
price: itemPrice,
|
||||
sortOrder: itemOrder
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
qNewItem = queryExecute("SELECT LAST_INSERT_ID() as id", {}, { datasource: "payfrit" });
|
||||
menuItemID = qNewItem.id;
|
||||
}
|
||||
|
||||
totalItems++;
|
||||
|
||||
// Link modifier templates to this item
|
||||
modOrder = 1;
|
||||
for (modName in itemModifiers) {
|
||||
if (structKeyExists(templateMap, modName)) {
|
||||
templateItemID = templateMap[modName];
|
||||
|
||||
// Check if link exists
|
||||
qLink = queryExecute("
|
||||
SELECT 1 FROM ItemTemplateLinks
|
||||
WHERE ItemID = :itemID AND TemplateItemID = :templateID
|
||||
", { itemID: menuItemID, templateID: templateItemID }, { datasource: "payfrit" });
|
||||
|
||||
if (qLink.recordCount == 0) {
|
||||
queryExecute("
|
||||
INSERT INTO ItemTemplateLinks (ItemID, TemplateItemID, SortOrder)
|
||||
VALUES (:itemID, :templateID, :sortOrder)
|
||||
", {
|
||||
itemID: menuItemID,
|
||||
templateID: templateItemID,
|
||||
sortOrder: modOrder
|
||||
}, { datasource: "payfrit" });
|
||||
totalLinks++;
|
||||
}
|
||||
modOrder++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.steps.append("Created/updated " & totalItems & " items with " & totalLinks & " modifier links");
|
||||
|
||||
response.OK = true;
|
||||
response.summary = {
|
||||
"businessId": businessId,
|
||||
"categoriesProcessed": arrayLen(categories),
|
||||
"templatesProcessed": arrayLen(modTemplates),
|
||||
"itemsProcessed": totalItems,
|
||||
"linksCreated": totalLinks
|
||||
};
|
||||
|
||||
} catch (any e) {
|
||||
response.errors.append(e.message);
|
||||
if (len(e.detail)) {
|
||||
response.errors.append(e.detail);
|
||||
}
|
||||
}
|
||||
|
||||
writeOutput(serializeJSON(response));
|
||||
</cfscript>
|
||||
|
|
@ -245,6 +245,14 @@
|
|||
<!-- Menu Page -->
|
||||
<section class="page" id="page-menu">
|
||||
<div class="page-header">
|
||||
<a href="setup-wizard.html" class="btn btn-secondary" style="margin-right: auto;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
Setup Wizard
|
||||
</a>
|
||||
<button class="btn btn-primary" onclick="Portal.showAddCategoryModal()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
|
|
|
|||
1505
portal/setup-wizard.html
Normal file
1505
portal/setup-wizard.html
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue